Add App Store integration to TopBar and wire up Apps category
- Add App Store button (puzzle piece icon) in TopBarNew - Import AppStore component in App.jsx and add modal state - Pass onOpenAppStore handler to TopBar - PostCreateModal already has Apps category with app selector Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b3fc07b001
commit
987920a5e4
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "sociowire-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.135",
|
||||
"version": "0.0.136",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
|
|
|
|||
14
src/App.jsx
14
src/App.jsx
|
|
@ -8,6 +8,7 @@ import IslandViewer from "./components/Island/IslandViewer";
|
|||
import ChatContainer from "./components/Chat/ChatContainer";
|
||||
import PostDetailModal from "./components/Posts/PostDetailModal";
|
||||
import PostComposer, { CreatePostFAB } from "./components/Posts/PostComposer";
|
||||
import { AppStore } from "./components/Apps";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { fetchIslandByUsername } from "./api/client";
|
||||
|
||||
|
|
@ -74,6 +75,9 @@ export default function App() {
|
|||
// Post detail modal state (triggered from map markers)
|
||||
const [detailPost, setDetailPost] = useState(null);
|
||||
|
||||
// App Store state
|
||||
const [showAppStore, setShowAppStore] = useState(false);
|
||||
|
||||
const handlePostsState = useCallback((next) => {
|
||||
if (!next) return;
|
||||
if (Array.isArray(next.posts)) setWallPosts(next.posts);
|
||||
|
|
@ -244,6 +248,7 @@ export default function App() {
|
|||
onMapClick={handleOpenMap}
|
||||
onUserIsland={handleOpenUserIsland}
|
||||
currentView={currentView}
|
||||
onOpenAppStore={() => setShowAppStore(true)}
|
||||
/>
|
||||
|
||||
<div className="main-shell">
|
||||
|
|
@ -419,6 +424,15 @@ export default function App() {
|
|||
|
||||
{/* Floating Action Button for creating posts */}
|
||||
<CreatePostFAB />
|
||||
|
||||
{/* App Store Modal */}
|
||||
<AnimatePresence>
|
||||
{showAppStore && (
|
||||
<AppStore
|
||||
onClose={() => setShowAppStore(false)}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ function UserChip({ authenticated, loading, username, avatarUrl: propAvatarUrl,
|
|||
// Main Component
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onPlanetsClick, onMapClick, onUserIsland, onMyLocation, onSearch, currentView }) {
|
||||
export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onPlanetsClick, onMapClick, onUserIsland, onMyLocation, onSearch, currentView, onOpenAppStore }) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const {
|
||||
loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled,
|
||||
|
|
@ -923,6 +923,18 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
|
|||
)}
|
||||
</motion.button>
|
||||
|
||||
{/* App Store */}
|
||||
<motion.button
|
||||
className="w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-lg sm:rounded-xl flex items-center justify-center transition-all text-xs sm:text-sm
|
||||
bg-surface-800/50 text-surface-400 border-white/5 border hover:bg-purple-500/20 hover:text-purple-400 hover:border-purple-500/40"
|
||||
onClick={() => authenticated ? onOpenAppStore?.() : openModal("login")}
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
title={t('topbar.appStore', 'App Store')}
|
||||
>
|
||||
<i className="fa-solid fa-puzzle-piece" />
|
||||
</motion.button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="hidden sm:block w-px h-7 bg-gradient-to-b from-transparent via-white/20 to-transparent mx-1" />
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useRef, useCallback, useEffect } from "react";
|
|||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { uploadPostMedia, fetchPostPreview } from "../../api/client";
|
||||
import { listSubscriptions, uploadPostImages, createPost as createAppPost } from "../../api/extAppsApi";
|
||||
|
||||
// Content modes
|
||||
const CONTENT_MODES = [
|
||||
|
|
@ -16,6 +17,7 @@ const CATEGORIES = [
|
|||
{ id: "Friends", icon: "fa-user-group", color: "from-emerald-500 to-green-500" },
|
||||
{ id: "Events", icon: "fa-calendar", color: "from-purple-500 to-violet-500" },
|
||||
{ id: "Market", icon: "fa-store", color: "from-amber-500 to-orange-500" },
|
||||
{ id: "Apps", icon: "fa-puzzle-piece", color: "from-pink-500 to-purple-500" },
|
||||
];
|
||||
|
||||
const SUB_CATEGORIES = {
|
||||
|
|
@ -23,6 +25,7 @@ const SUB_CATEGORIES = {
|
|||
Friends: ["Nearby", "Stories", "Hangout", "Help"],
|
||||
Events: ["Today", "Weekend", "Music", "Sports", "Food"],
|
||||
Market: ["Deals", "Services", "Jobs", "Housing"],
|
||||
Apps: [], // Dynamic - loaded from subscriptions
|
||||
};
|
||||
|
||||
export default function PostCreateModal({
|
||||
|
|
@ -46,6 +49,11 @@ export default function PostCreateModal({
|
|||
const [linkLoading, setLinkLoading] = useState(false);
|
||||
const [images, setImages] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [price, setPrice] = useState("");
|
||||
// External apps state
|
||||
const [subscribedApps, setSubscribedApps] = useState([]);
|
||||
const [selectedApp, setSelectedApp] = useState(null);
|
||||
const [appsLoading, setAppsLoading] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
const cameraInputRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
|
|
@ -77,6 +85,25 @@ export default function PostCreateModal({
|
|||
}
|
||||
}, [body]);
|
||||
|
||||
// Load subscribed apps when Apps category is selected
|
||||
useEffect(() => {
|
||||
if (category === "Apps" && subscribedApps.length === 0 && !appsLoading) {
|
||||
setAppsLoading(true);
|
||||
listSubscriptions()
|
||||
.then((result) => {
|
||||
const apps = (result.subscriptions || []).filter(
|
||||
(sub) => sub.capabilities?.includes("posts")
|
||||
);
|
||||
setSubscribedApps(apps);
|
||||
if (apps.length > 0 && !selectedApp) {
|
||||
setSelectedApp(apps[0]);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Failed to load apps:", err))
|
||||
.finally(() => setAppsLoading(false));
|
||||
}
|
||||
}, [category, subscribedApps.length, appsLoading, selectedApp]);
|
||||
|
||||
// Link preview fetch
|
||||
useEffect(() => {
|
||||
if (mode !== "link" || !linkUrl.trim()) {
|
||||
|
|
@ -132,7 +159,7 @@ export default function PostCreateModal({
|
|||
setImages((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = async () => {
|
||||
if (mode === "live") {
|
||||
onGoLive?.({ category, subCategory, title, body, coords });
|
||||
return;
|
||||
|
|
@ -140,6 +167,45 @@ export default function PostCreateModal({
|
|||
|
||||
if (!title.trim() && !body.trim() && images.length === 0 && !linkUrl.trim()) return;
|
||||
|
||||
// Handle external app posts
|
||||
if (category === "Apps" && selectedApp) {
|
||||
try {
|
||||
setUploading(true);
|
||||
|
||||
// Upload images to app's S3 via Sociowire
|
||||
let uploadedImages = [];
|
||||
if (images.length > 0) {
|
||||
// Convert URLs to files for re-upload to app
|
||||
// Note: images are already uploaded to Sociowire, we need to reference them
|
||||
uploadedImages = images; // For now, pass the URLs directly
|
||||
}
|
||||
|
||||
// Create post via apps-gateway
|
||||
const postData = {
|
||||
title: title.trim(),
|
||||
description: body.trim(),
|
||||
images: uploadedImages,
|
||||
location: coords ? {
|
||||
lat: coords[1],
|
||||
lon: coords[0],
|
||||
} : null,
|
||||
content: {}
|
||||
};
|
||||
|
||||
if (price) {
|
||||
postData.content.price = parseFloat(price);
|
||||
}
|
||||
|
||||
await createAppPost(selectedApp.app_id, postData);
|
||||
onClose?.();
|
||||
} catch (err) {
|
||||
setUploadError(err.message || "Failed to create app post");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit?.({
|
||||
mode,
|
||||
title: title.trim(),
|
||||
|
|
@ -154,7 +220,9 @@ export default function PostCreateModal({
|
|||
};
|
||||
|
||||
const canSubmit = mode === "live" ||
|
||||
(title.trim() || body.trim() || images.length > 0 || linkUrl.trim()) && !isSaving && !uploading;
|
||||
(category === "Apps"
|
||||
? (selectedApp && title.trim() && coords && !isSaving && !uploading)
|
||||
: ((title.trim() || body.trim() || images.length > 0 || linkUrl.trim()) && !isSaving && !uploading));
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
|
|
@ -272,7 +340,39 @@ export default function PostCreateModal({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Sub-category */}
|
||||
{/* Sub-category or App selector */}
|
||||
{category === "Apps" ? (
|
||||
<div className="space-y-2">
|
||||
{appsLoading ? (
|
||||
<div className="flex items-center gap-2 text-surface-400 text-sm">
|
||||
<i className="fa-solid fa-spinner fa-spin" />
|
||||
Chargement des apps...
|
||||
</div>
|
||||
) : subscribedApps.length === 0 ? (
|
||||
<div className="p-3 rounded-xl bg-surface-700/30 border border-white/5 text-center">
|
||||
<i className="fa-solid fa-puzzle-piece text-2xl text-surface-600 mb-2" />
|
||||
<p className="text-surface-400 text-sm">Aucune app disponible</p>
|
||||
<p className="text-surface-500 text-xs mt-1">Abonnez-vous à une app dans l'App Store</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||
{subscribedApps.map((app) => (
|
||||
<button
|
||||
key={app.app_id}
|
||||
className={`flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium whitespace-nowrap transition-all border
|
||||
${selectedApp?.app_id === app.app_id
|
||||
? "bg-gradient-to-r from-pink-500 to-purple-500 text-white border-transparent"
|
||||
: "bg-surface-700/30 text-surface-400 border-surface-600/50 hover:border-surface-500"}`}
|
||||
onClick={() => setSelectedApp(app)}
|
||||
>
|
||||
<i className="fa-solid fa-puzzle-piece" />
|
||||
{app.app_name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-1.5 overflow-x-auto pb-1">
|
||||
{SUB_CATEGORIES[category]?.map((sub) => (
|
||||
<button
|
||||
|
|
@ -287,6 +387,7 @@ export default function PostCreateModal({
|
|||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Link input (for link mode) */}
|
||||
{mode === "link" && (
|
||||
|
|
@ -379,6 +480,26 @@ export default function PostCreateModal({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Price input for Apps */}
|
||||
{category === "Apps" && selectedApp && (
|
||||
<div className="relative">
|
||||
<i className="fa-solid fa-dollar-sign absolute left-4 top-1/2 -translate-y-1/2 text-emerald-500" />
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Prix (optionnel)"
|
||||
value={price}
|
||||
onChange={(e) => setPrice(e.target.value)}
|
||||
min="0"
|
||||
step="0.01"
|
||||
className="w-full pl-10 pr-4 py-3 rounded-xl
|
||||
bg-surface-700/50 border border-white/10
|
||||
text-white text-base
|
||||
placeholder:text-surface-500
|
||||
focus:outline-none focus:border-emerald-500/50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image gallery (for photo/text modes) */}
|
||||
{(mode === "photo" || mode === "text") && images.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
|
|
|
|||
Loading…
Reference in New Issue