diff --git a/src/api/client.js b/src/api/client.js index ea8b197..32d39c7 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -320,6 +320,41 @@ export async function createPost(payload, token) { return res.json().catch(() => ({})); } +/** + * Create a news post (text + image format) + * Goes directly to core-api's /api/news/post endpoint + */ +export async function createNewsPost(payload) { + const headers = { + "Content-Type": "application/json", + ...authHeaders(), + }; + + const res = await fetch(`${apiBase()}/news/post`, { + method: "POST", + headers, + credentials: "include", + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + let json = null; + try { json = text ? JSON.parse(text) : null; } catch {} + + const errMsg = + (json && (json.message || json.error_description || json.error)) || + (text && text.slice(0, 300)) || + `HTTP ${res.status}`; + + const err = new Error(errMsg); + err.status = res.status; + throw err; + } + + return res.json().catch(() => ({})); +} + /** * Fetch link preview data for a URL (auth required). */ @@ -379,10 +414,27 @@ export async function fetchPostByUrl(url) { } } -export async function fetchPostTruth(postId) { - if (!postId) return null; +export async function fetchPostByGuid(guid) { + const raw = (guid || "").toString().trim(); + if (!raw) return null; const qs = new URLSearchParams(); - qs.set("post_id", String(postId)); + qs.set("guid", raw); + try { + const data = await fetchJsonCached(`${apiBase()}/post?${qs.toString()}`, { ttlMs: 15000 }); + if (!data || typeof data !== "object") return null; + const resolved = await resolveAssetRefsInPosts([data]); + return resolved && resolved[0] ? resolved[0] : data; + } catch { + return null; + } +} + +export async function fetchPostTruth(postId, guid = null, appId = null) { + if (!postId && !guid) return null; + const qs = new URLSearchParams(); + if (postId) qs.set("post_id", String(postId)); + if (guid) qs.set("guid", guid); + if (appId) qs.set("app_id", appId); try { const res = await fetch(`${apiBase()}/post/truth?${qs.toString()}`, { credentials: "include", @@ -444,7 +496,7 @@ export async function votePostTruth(postId, vote, guid = null, appId = null) { truth_score: truthScore, // Main display score - the final calculated value total_votes: voteCount, vote_count: voteCount, - user_vote: vote, // The vote that was just cast + user_vote: truth.user_vote || vote, // Use API response, fallback to submitted vote }; } diff --git a/src/components/Posts/PostCreateModal.jsx b/src/components/Posts/PostCreateModal.jsx index 37243a5..52986ec 100644 --- a/src/components/Posts/PostCreateModal.jsx +++ b/src/components/Posts/PostCreateModal.jsx @@ -1,8 +1,8 @@ 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"; +import { fetchPostPreview, uploadPostMedia, createNewsPost } from "../../api/client"; +import { listSubscriptions, createPost as createAppPost } from "../../api/extAppsApi"; // Content modes const CONTENT_MODES = [ @@ -10,6 +10,7 @@ const CONTENT_MODES = [ { id: "photo", icon: "fa-image", label: "Photo", color: "from-emerald-500 to-green-500" }, { id: "link", icon: "fa-link", label: "Link", color: "from-purple-500 to-violet-500" }, { id: "live", icon: "fa-broadcast-tower", label: "Live", color: "from-red-500 to-rose-500" }, + { id: "news", icon: "fa-newspaper", label: "News", color: "from-amber-500 to-orange-500" }, ]; const CATEGORIES = [ @@ -17,7 +18,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" }, + { id: "Apps", icon: "fa-puzzle-piece", color: "from-pink-500 to-rose-500" }, ]; const SUB_CATEGORIES = { @@ -25,7 +26,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 + Apps: [], // Dynamic - filled from subscribed apps }; export default function PostCreateModal({ @@ -49,34 +50,55 @@ 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 [uploadError, setUploadError] = useState(""); const fileInputRef = useRef(null); const cameraInputRef = useRef(null); const textareaRef = useRef(null); const linkTimeoutRef = useRef(null); - // Reset only when modal opens (not on every initialCategory change) - const wasOpenRef = useRef(false); + // Apps state + const [subscribedApps, setSubscribedApps] = useState([]); + const [selectedApp, setSelectedApp] = useState(null); + const [appsLoading, setAppsLoading] = useState(false); + + // Reset on open useEffect(() => { - if (isOpen && !wasOpenRef.current) { - // Modal just opened - reset everything + if (isOpen) { setMode("text"); setTitle(""); setBody(""); setLinkUrl(""); setLinkPreview(null); setImages([]); - setUploadError(""); setCategory(initialCategory); setSubCategory(SUB_CATEGORIES[initialCategory]?.[0] || ""); + setSelectedApp(null); + setUploadError(""); } - wasOpenRef.current = isOpen; }, [isOpen, initialCategory]); + // Load apps when Apps category is selected + useEffect(() => { + if (category === "Apps" && subscribedApps.length === 0) { + loadApps(); + } + }, [category]); + + const loadApps = async () => { + try { + setAppsLoading(true); + const result = await listSubscriptions(); + const apps = (result.subscriptions || []).filter( + (sub) => sub.capabilities?.includes("posts") + ); + setSubscribedApps(apps); + } catch (err) { + console.error("Failed to load apps:", err); + } finally { + setAppsLoading(false); + } + }; + // Auto-resize textarea useEffect(() => { if (textareaRef.current) { @@ -85,28 +107,9 @@ export default function PostCreateModal({ } }, [body]); - // Load subscribed apps when Apps category is selected + // Link preview fetch (for link and news modes) 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()) { + if ((mode !== "link" && mode !== "news") || !linkUrl.trim()) { setLinkPreview(null); return; } @@ -119,16 +122,13 @@ export default function PostCreateModal({ if (preview) { setLinkPreview(preview); if (!title && preview.title) setTitle(preview.title); - if (!body && preview.description) setBody(preview.description); } } catch {} setLinkLoading(false); }, 500); return () => clearTimeout(linkTimeoutRef.current); - }, [linkUrl, mode, title, body]); - - const [uploadError, setUploadError] = useState(""); + }, [linkUrl, mode, title]); const handleFileChange = useCallback(async (e) => { const files = Array.from(e.target.files || []); @@ -140,20 +140,18 @@ export default function PostCreateModal({ for (const file of files) { const result = await uploadPostMedia(file); if (result?.url) { - setImages((prev) => { - const newImages = [...prev, result.url]; - return newImages; - }); - } else { - setUploadError(result?.error || "Upload failed - no URL returned"); + setImages((prev) => [...prev, result.url].slice(0, 5)); + } else if (result?.error) { + setUploadError(result.error); } } } catch (err) { + console.error("Upload failed:", err); setUploadError(err.message || "Upload failed"); } setUploading(false); e.target.value = ""; - }, []); + }, [category]); const removeImage = (index) => { setImages((prev) => prev.filter((_, i) => i !== index)); @@ -165,47 +163,52 @@ export default function PostCreateModal({ return; } - if (!title.trim() && !body.trim() && images.length === 0 && !linkUrl.trim()) return; - - // Handle external app posts - if (category === "Apps" && selectedApp) { + // Handle News mode - post to core-api /api/news/post (text + optional image) + if (mode === "news") { + if (!title.trim() && !body.trim()) return; 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(), + title: title.trim() || body.trim().slice(0, 100), description: body.trim(), - images: uploadedImages, - location: coords ? { - lat: coords[1], - lon: coords[0], - } : null, - content: {} + url: linkUrl.trim() || null, + images: images, + location: coords ? { lat: coords[1], lon: coords[0] } : null, }; - - if (price) { - postData.content.price = parseFloat(price); - } - - await createAppPost(selectedApp.app_id, postData); + await createNewsPost(postData); onClose?.(); } catch (err) { - setUploadError(err.message || "Failed to create app post"); + setUploadError(err.message || "Failed to create news post"); } finally { setUploading(false); } return; } + // Handle Apps category - post to external app + if (category === "Apps" && selectedApp) { + try { + setUploading(true); + const postData = { + title: title.trim(), + description: body.trim(), + images, + location: coords ? { lat: coords[1], lon: coords[0] } : null, + content: {} + }; + await createAppPost(selectedApp.app_id, postData); + onClose?.(); + } catch (err) { + setUploadError(err.message || "Failed to create post"); + } finally { + setUploading(false); + } + return; + } + + // Regular post + if (!title.trim() && !body.trim() && images.length === 0 && !linkUrl.trim()) return; + onSubmit?.({ mode, title: title.trim(), @@ -220,37 +223,32 @@ export default function PostCreateModal({ }; const canSubmit = mode === "live" || - (category === "Apps" - ? (selectedApp && title.trim() && coords && !isSaving && !uploading) - : ((title.trim() || body.trim() || images.length > 0 || linkUrl.trim()) && !isSaving && !uploading)); + (mode === "news" && (title.trim() || body.trim()) && !isSaving && !uploading) || + (category === "Apps" && selectedApp && title.trim()) || + ((title.trim() || body.trim() || images.length > 0 || linkUrl.trim()) && !isSaving && !uploading); if (!isOpen) return null; return ( - {/* Backdrop - only covers bottom half, allows map interaction above */} + {/* Backdrop */} - {/* Invisible close trigger at top */} -
- {/* Modal - positioned at bottom, shorter to show map with crosshair */} + {/* Modal */} - {isSaving ? ( + {isSaving || uploading ? ( ) : mode === "live" ? ( <> @@ -298,28 +296,31 @@ export default function PostCreateModal({
- {/* Content modes */} -
- {CONTENT_MODES.map((m) => ( - setMode(m.id)} - > - - {m.label} - - ))} -
+ {/* Content modes (hidden for Apps category) */} + {category !== "Apps" && ( +
+ {CONTENT_MODES.map((m) => ( + setMode(m.id)} + > + + {m.label} + + ))} +
+ )} {/* Scrollable content */} -
- {/* Category row */} +
+ {/* Category row (hidden for news mode) */} + {mode !== "news" && (
{CATEGORIES.map((cat) => ( { setCategory(cat.id); setSubCategory(SUB_CATEGORIES[cat.id]?.[0] || ""); + setSelectedApp(null); }} > @@ -339,169 +341,282 @@ export default function PostCreateModal({ ))}
+ )} - {/* Sub-category or App selector */} + {/* Apps category - show subscribed apps */} {category === "Apps" ? ( -
+
{appsLoading ? ( -
- - Chargement des apps... +
+
) : subscribedApps.length === 0 ? ( -
- -

Aucune app disponible

-

Abonnez-vous à une app dans l'App Store

+
+ +

No apps available

+

+ Subscribe to apps in the App Store to post +

) : ( -
- {subscribedApps.map((app) => ( + <> + {/* App selector */} +
+ {subscribedApps.map((app) => ( + setSelectedApp(app)} + > +
+ +
+

+ {app.app_name} +

+
+ ))} +
+ + {/* App post form */} + {selectedApp && ( +
+ setTitle(e.target.value)} + className="w-full px-4 py-3 rounded-xl + bg-surface-700/50 border border-white/10 + text-white text-base font-medium + placeholder:text-surface-500 + focus:outline-none focus:border-pink-500/50" + /> +