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 && (