From 987920a5e4b88ff0512eb06ef9085282fc62e712 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 28 Jan 2026 19:26:34 +0000 Subject: [PATCH] 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 --- package.json | 2 +- src/App.jsx | 14 ++ src/components/Layout/TopBarNew.jsx | 14 +- src/components/Posts/PostCreateModal.jsx | 155 ++++++++++++++++++++--- 4 files changed, 166 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8b72f3c..d0ef469 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.jsx b/src/App.jsx index 5f07d69..7d5fac5 100644 --- a/src/App.jsx +++ b/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)} />
@@ -419,6 +424,15 @@ export default function App() { {/* Floating Action Button for creating posts */} + + {/* App Store Modal */} + + {showAppStore && ( + setShowAppStore(false)} + /> + )} +
); } diff --git a/src/components/Layout/TopBarNew.jsx b/src/components/Layout/TopBarNew.jsx index 507d87e..696e207 100644 --- a/src/components/Layout/TopBarNew.jsx +++ b/src/components/Layout/TopBarNew.jsx @@ -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 )} + {/* App Store */} + authenticated ? onOpenAppStore?.() : openModal("login")} + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + title={t('topbar.appStore', 'App Store')} + > + + + {/* Separator */}
diff --git a/src/components/Posts/PostCreateModal.jsx b/src/components/Posts/PostCreateModal.jsx index e7c848a..37243a5 100644 --- a/src/components/Posts/PostCreateModal.jsx +++ b/src/components/Posts/PostCreateModal.jsx @@ -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,21 +340,54 @@ export default function PostCreateModal({ ))}
- {/* Sub-category */} -
- {SUB_CATEGORIES[category]?.map((sub) => ( - - ))} -
+ {/* Sub-category or App selector */} + {category === "Apps" ? ( +
+ {appsLoading ? ( +
+ + Chargement des apps... +
+ ) : subscribedApps.length === 0 ? ( +
+ +

Aucune app disponible

+

Abonnez-vous à une app dans l'App Store

+
+ ) : ( +
+ {subscribedApps.map((app) => ( + + ))} +
+ )} +
+ ) : ( +
+ {SUB_CATEGORIES[category]?.map((sub) => ( + + ))} +
+ )} {/* Link input (for link mode) */} {mode === "link" && ( @@ -379,6 +480,26 @@ export default function PostCreateModal({ /> )} + {/* Price input for Apps */} + {category === "Apps" && selectedApp && ( +
+ + 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" + /> +
+ )} + {/* Image gallery (for photo/text modes) */} {(mode === "photo" || mode === "text") && images.length > 0 && (