feat: Hot topbar + full-featured post creation modal

TopBar:
- Animated gradient accent line
- Glowing logo with pulse effect
- Theme selector pills on desktop (visible again)
- Cleaner action buttons with color states

PostCreateModal:
- 4 content modes: Text, Photo, Link, Live
- Link mode with auto-preview
- Live mode with Go Live button
- Category and sub-category selection
- Image upload with gallery
- All features from old wizard in single screen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-19 17:07:48 +00:00
parent f2992bd791
commit 77e376465d
3 changed files with 335 additions and 171 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "sociowire-frontend", "name": "sociowire-frontend",
"private": true, "private": true,
"version": "0.0.49", "version": "0.0.50",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",

View File

@ -473,107 +473,136 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</div> </div>
)} )}
{/* Header - Premium Social Network Style */} {/* Header - Hot Social Network Style */}
<motion.header <motion.header
className="sticky top-0 z-[1000] w-full overflow-hidden" className="sticky top-0 z-[1000] w-full"
initial={{ y: -60, opacity: 0 }} initial={{ y: -60, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }} transition={{ duration: 0.4, ease: "easeOut" }}
> >
{/* Clean dark background */} {/* Glassmorphism background with gradient */}
<div className="absolute inset-0 bg-surface-950/95 backdrop-blur-xl" /> <div className="absolute inset-0 bg-gradient-to-r from-surface-950/98 via-surface-900/95 to-surface-950/98 backdrop-blur-2xl" />
{/* Subtle bottom border */} {/* Animated accent line */}
<div className="absolute bottom-0 left-0 right-0 h-px bg-surface-700/50" /> <motion.div
className="absolute bottom-0 left-0 right-0 h-[2px]"
style={{ background: "linear-gradient(90deg, #3b82f6, #06b6d4, #10b981, #06b6d4, #3b82f6)", backgroundSize: "200% 100%" }}
animate={{ backgroundPosition: ["0% 0%", "200% 0%"] }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
/>
<div className="relative flex items-center justify-between gap-2 px-3 py-2.5 sm:px-5 sm:py-3"> <div className="relative flex items-center justify-between gap-3 px-3 py-2 sm:px-4 sm:py-2.5">
{/* Left: Clean Logo */} {/* Left: Logo with pulse effect */}
<div className="flex items-center gap-2.5"> <motion.div
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 p-0.5"> className="flex items-center gap-3 cursor-pointer"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="relative">
<motion.div
className="absolute -inset-1 rounded-2xl bg-gradient-to-r from-blue-500 to-cyan-500 opacity-50 blur-lg"
animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 2, repeat: Infinity }}
/>
<div className="relative w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 via-cyan-500 to-emerald-500 p-[2px] shadow-lg shadow-cyan-500/25">
<div className="w-full h-full rounded-[10px] bg-surface-900 flex items-center justify-center"> <div className="w-full h-full rounded-[10px] bg-surface-900 flex items-center justify-center">
<img src="/logo.png" alt="SocioWire" className="w-6 h-6 object-contain" /> <img src="/logo.png" alt="S" className="w-6 h-6 object-contain" />
</div> </div>
</div> </div>
<div className="flex flex-col"> </div>
<span className="text-base font-bold text-white tracking-tight">sociowire</span> <div className="hidden sm:flex flex-col">
<span className="flex items-center gap-1 text-[9px] text-surface-400"> <span className="text-lg font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-blue-100 to-cyan-200">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" /> SOCIOWIRE
</span>
<span className="flex items-center gap-1.5 text-[10px] font-medium text-surface-400">
<motion.span
className="w-2 h-2 rounded-full bg-emerald-500"
animate={{ scale: [1, 1.3, 1], opacity: [1, 0.7, 1] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
{t('topbar.wiredToLife')} {t('topbar.wiredToLife')}
</span> </span>
</div> </div>
</div> </motion.div>
{/* Center: Spacer for balance */} {/* Center: Theme pills (desktop) */}
<div className="hidden lg:block flex-1" /> <div className="hidden md:flex items-center gap-1 px-1.5 py-1 rounded-full bg-surface-800/40 border border-white/5">
{THEMES.map((themeKey) => (
{/* Right: Actions */}
<div className="flex items-center gap-2 sm:gap-2.5">
{/* PWA Install */}
{showInstallBtn && (
<ActionButton
icon="fa-solid fa-download"
label={t('topbar.install')}
color="default"
onClick={() => promptInstall()}
title={t('topbar.installApp')}
/>
)}
{/* Theme - mobile */}
<div className="lg:hidden" ref={themeBtnRef}>
<motion.button <motion.button
type="button" key={themeKey}
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all
${showThemeMenu ${theme === themeKey
? "bg-accent/20 text-accent border border-accent/30" ? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg shadow-cyan-500/25"
: "bg-surface-800/50 text-surface-400 border border-white/5 hover:text-surface-200 hover:bg-surface-800" : "text-surface-400 hover:text-white hover:bg-white/5"}`}
}`} onClick={() => onChangeTheme?.(themeKey)}
onClick={toggleThemeMenu}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
<i className="fa-solid fa-palette text-sm" /> {t(`theme.${themeKey}`)}
</motion.button>
))}
</div>
{/* Right: Action buttons */}
<div className="flex items-center gap-1.5 sm:gap-2">
{/* Install PWA */}
{showInstallBtn && (
<motion.button
className="flex items-center gap-2 px-3 py-2 rounded-xl
bg-gradient-to-r from-emerald-500/20 to-cyan-500/20
border border-emerald-500/30 text-emerald-400
text-xs font-bold hover:from-emerald-500/30 hover:to-cyan-500/30 transition-all"
onClick={() => promptInstall()}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<i className="fa-solid fa-download" />
<span className="hidden sm:inline">{t('topbar.install')}</span>
</motion.button>
)}
{/* Theme (mobile) */}
<div className="md:hidden" ref={themeBtnRef}>
<motion.button
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all
${showThemeMenu ? "bg-cyan-500/20 text-cyan-400 border-cyan-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
onClick={toggleThemeMenu}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<i className="fa-solid fa-palette" />
</motion.button> </motion.button>
</div> </div>
{/* Language */} {/* Language */}
<div ref={langBtnRef}> <div ref={langBtnRef}>
<motion.button <motion.button
type="button"
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all
${showLangMenu ${showLangMenu ? "bg-blue-500/20 text-blue-400 border-blue-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
? "bg-accent/20 text-accent border border-accent/30"
: "bg-surface-800/50 text-surface-400 border border-white/5 hover:text-surface-200 hover:bg-surface-800"
}`}
onClick={toggleLangMenu} onClick={toggleLangMenu}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.9 }}
> >
<i className="fa-solid fa-globe text-sm" /> <i className="fa-solid fa-globe" />
</motion.button> </motion.button>
</div> </div>
{/* Friends */} {/* Friends */}
{authenticated && ( {authenticated && (
<motion.button <motion.button
type="button"
className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all
${showFriends ${showFriends ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
: "bg-surface-800/50 text-surface-400 border border-white/5 hover:text-amber-400 hover:bg-amber-500/10"
}`}
onClick={() => setShowFriends(!showFriends)} onClick={() => setShowFriends(!showFriends)}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.9 }}
> >
<i className="fa-solid fa-user-group text-sm" /> <i className="fa-solid fa-user-group" />
{pendingRequestsCount > 0 && ( {pendingRequestsCount > 0 && (
<motion.span <motion.span
className="absolute -top-1 -right-1 min-w-[18px] h-[18px] px-1 className="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1
rounded-full bg-red-500 text-white text-[10px] font-bold rounded-full bg-gradient-to-r from-red-500 to-pink-500 text-white text-[10px] font-bold
flex items-center justify-center border-2 border-surface-900" flex items-center justify-center shadow-lg shadow-red-500/50"
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 500 }}
> >
{pendingRequestsCount} {pendingRequestsCount}
</motion.span> </motion.span>
@ -581,8 +610,8 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</motion.button> </motion.button>
)} )}
{/* Divider */} {/* Separator */}
<div className="hidden sm:block w-px h-6 bg-gradient-to-b from-transparent via-surface-600/50 to-transparent" /> <div className="hidden sm:block w-px h-7 bg-gradient-to-b from-transparent via-white/20 to-transparent mx-1" />
{/* User/Auth */} {/* User/Auth */}
{authenticated ? ( {authenticated ? (

View File

@ -1,7 +1,15 @@
import React, { useState, useRef, useCallback, useEffect } from "react"; import React, { useState, useRef, useCallback, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { uploadImage } from "../../api/client"; import { uploadImage, fetchLinkPreview } from "../../api/client";
// Content modes
const CONTENT_MODES = [
{ id: "text", icon: "fa-pen", label: "Text", color: "from-blue-500 to-cyan-500" },
{ 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" },
];
const CATEGORIES = [ const CATEGORIES = [
{ id: "News", icon: "fa-newspaper", color: "from-blue-500 to-cyan-500" }, { id: "News", icon: "fa-newspaper", color: "from-blue-500 to-cyan-500" },
@ -21,27 +29,36 @@ export default function PostCreateModal({
isOpen, isOpen,
onClose, onClose,
onSubmit, onSubmit,
onGoLive,
coords, coords,
initialCategory = "News", initialCategory = "News",
isSaving = false, isSaving = false,
saveError = "", saveError = "",
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [mode, setMode] = useState("text");
const [category, setCategory] = useState(initialCategory); const [category, setCategory] = useState(initialCategory);
const [subCategory, setSubCategory] = useState(SUB_CATEGORIES[initialCategory]?.[0] || ""); const [subCategory, setSubCategory] = useState(SUB_CATEGORIES[initialCategory]?.[0] || "");
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [body, setBody] = useState(""); const [body, setBody] = useState("");
const [linkUrl, setLinkUrl] = useState("");
const [linkPreview, setLinkPreview] = useState(null);
const [linkLoading, setLinkLoading] = useState(false);
const [images, setImages] = useState([]); const [images, setImages] = useState([]);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const cameraInputRef = useRef(null); const cameraInputRef = useRef(null);
const textareaRef = useRef(null); const textareaRef = useRef(null);
const linkTimeoutRef = useRef(null);
// Reset on open // Reset on open
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setMode("text");
setTitle(""); setTitle("");
setBody(""); setBody("");
setLinkUrl("");
setLinkPreview(null);
setImages([]); setImages([]);
setCategory(initialCategory); setCategory(initialCategory);
setSubCategory(SUB_CATEGORIES[initialCategory]?.[0] || ""); setSubCategory(SUB_CATEGORIES[initialCategory]?.[0] || "");
@ -52,10 +69,33 @@ export default function PostCreateModal({
useEffect(() => { useEffect(() => {
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.style.height = "auto"; textareaRef.current.style.height = "auto";
textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"; textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 200) + "px";
} }
}, [body]); }, [body]);
// Link preview fetch
useEffect(() => {
if (mode !== "link" || !linkUrl.trim()) {
setLinkPreview(null);
return;
}
clearTimeout(linkTimeoutRef.current);
linkTimeoutRef.current = setTimeout(async () => {
try {
setLinkLoading(true);
const preview = await fetchLinkPreview(linkUrl.trim());
if (preview) {
setLinkPreview(preview);
if (!title && preview.title) setTitle(preview.title);
}
} catch {}
setLinkLoading(false);
}, 500);
return () => clearTimeout(linkTimeoutRef.current);
}, [linkUrl, mode, title]);
const handleFileChange = useCallback(async (e) => { const handleFileChange = useCallback(async (e) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
if (files.length === 0) return; if (files.length === 0) return;
@ -80,19 +120,28 @@ export default function PostCreateModal({
}; };
const handleSubmit = () => { const handleSubmit = () => {
if (!title.trim() && !body.trim() && images.length === 0) return; if (mode === "live") {
onGoLive?.({ category, subCategory, title, body, coords });
return;
}
if (!title.trim() && !body.trim() && images.length === 0 && !linkUrl.trim()) return;
onSubmit?.({ onSubmit?.({
mode,
title: title.trim(), title: title.trim(),
body: body.trim(), body: body.trim(),
category, category,
subCategory, subCategory,
images, images,
linkUrl: linkUrl.trim(),
linkPreview,
coords, coords,
}); });
}; };
const canSubmit = (title.trim() || body.trim() || images.length > 0) && !isSaving && !uploading; const canSubmit = mode === "live" ||
(title.trim() || body.trim() || images.length > 0 || linkUrl.trim()) && !isSaving && !uploading;
if (!isOpen) return null; if (!isOpen) return null;
@ -106,70 +155,94 @@ export default function PostCreateModal({
> >
{/* Backdrop */} {/* Backdrop */}
<motion.div <motion.div
className="absolute inset-0 bg-black/70 backdrop-blur-sm" className="absolute inset-0 bg-black/80 backdrop-blur-md"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose} onClick={onClose}
/> />
{/* Modal */} {/* Modal */}
<motion.div <motion.div
className="relative w-full max-w-lg mx-4 mb-0 sm:mb-0 className="relative w-full max-w-xl mx-2 sm:mx-4
bg-gradient-to-br from-surface-900 via-surface-950 to-surface-900 bg-gradient-to-b from-surface-800 to-surface-900
rounded-t-3xl sm:rounded-3xl border border-white/10 rounded-t-3xl sm:rounded-3xl border border-white/10
shadow-2xl overflow-hidden" shadow-2xl shadow-black/50 overflow-hidden"
initial={{ y: "100%", opacity: 0 }} initial={{ y: "100%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
exit={{ y: "100%", opacity: 0 }} exit={{ y: "100%", opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }} transition={{ type: "spring", damping: 30, stiffness: 400 }}
> >
{/* Glowing top border */}
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-blue-500 via-cyan-500 to-emerald-500" />
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-white/10"> <div className="flex items-center justify-between px-4 py-3 border-b border-white/5">
<motion.button <motion.button
className="w-9 h-9 rounded-full bg-surface-800/80 border border-white/10 className="w-8 h-8 rounded-full bg-surface-700/50 border border-white/10
flex items-center justify-center text-surface-300 hover:text-white flex items-center justify-center text-surface-400 hover:text-white
hover:bg-red-500/20 hover:border-red-500/40 transition-all" hover:bg-red-500/20 hover:border-red-500/30 transition-all"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
onClick={onClose} onClick={onClose}
> >
<i className="fa-solid fa-times" /> <i className="fa-solid fa-times text-sm" />
</motion.button> </motion.button>
<h2 className="text-lg font-bold text-surface-100">Create Post</h2> <h2 className="text-base font-bold text-white">Create Post</h2>
<motion.button <motion.button
className={`px-5 py-2 rounded-full font-bold text-sm transition-all className={`px-4 py-1.5 rounded-full font-bold text-sm transition-all
${canSubmit ${canSubmit
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg shadow-blue-500/30" ? mode === "live"
: "bg-surface-700/50 text-surface-400 cursor-not-allowed"}`} ? "bg-gradient-to-r from-red-500 to-rose-500 text-white shadow-lg shadow-red-500/30"
: "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg shadow-cyan-500/30"
: "bg-surface-700/50 text-surface-500 cursor-not-allowed"}`}
whileHover={canSubmit ? { scale: 1.05 } : {}} whileHover={canSubmit ? { scale: 1.05 } : {}}
whileTap={canSubmit ? { scale: 0.95 } : {}} whileTap={canSubmit ? { scale: 0.95 } : {}}
onClick={handleSubmit} onClick={handleSubmit}
disabled={!canSubmit} disabled={!canSubmit}
> >
{isSaving ? ( {isSaving ? (
<><i className="fa-solid fa-spinner fa-spin mr-2" />Posting...</> <i className="fa-solid fa-spinner fa-spin" />
) : mode === "live" ? (
<>
<i className="fa-solid fa-broadcast-tower mr-1.5" />
Go Live
</>
) : ( ) : (
"Post" "Post"
)} )}
</motion.button> </motion.button>
</div> </div>
{/* Content */} {/* Content modes */}
<div className="p-5 max-h-[70vh] overflow-y-auto"> <div className="flex gap-1 px-4 py-3 border-b border-white/5 overflow-x-auto">
{/* Category chips */} {CONTENT_MODES.map((m) => (
<div className="flex gap-2 mb-4 overflow-x-auto pb-2"> <motion.button
key={m.id}
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-semibold whitespace-nowrap
transition-all ${mode === m.id
? `bg-gradient-to-r ${m.color} text-white shadow-lg`
: "bg-surface-700/30 text-surface-400 hover:text-white hover:bg-surface-700/50"}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => setMode(m.id)}
>
<i className={`fa-solid ${m.icon}`} />
{m.label}
</motion.button>
))}
</div>
{/* Scrollable content */}
<div className="p-4 max-h-[60vh] overflow-y-auto space-y-4">
{/* Category row */}
<div className="flex gap-2 overflow-x-auto pb-1">
{CATEGORIES.map((cat) => ( {CATEGORIES.map((cat) => (
<motion.button <motion.button
key={cat.id} key={cat.id}
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-bold whitespace-nowrap
transition-all border ${category === cat.id transition-all border ${category === cat.id
? `bg-gradient-to-r ${cat.color} text-white border-white/20 shadow-lg` ? `bg-gradient-to-r ${cat.color} text-white border-transparent`
: "bg-surface-800/50 text-surface-300 border-white/5 hover:border-white/20"}`} : "bg-transparent text-surface-400 border-surface-600/50 hover:border-surface-500"}`}
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.95 }}
whileTap={{ scale: 0.98 }}
onClick={() => { onClick={() => {
setCategory(cat.id); setCategory(cat.id);
setSubCategory(SUB_CATEGORIES[cat.id]?.[0] || ""); setSubCategory(SUB_CATEGORIES[cat.id]?.[0] || "");
@ -181,60 +254,122 @@ export default function PostCreateModal({
))} ))}
</div> </div>
{/* Sub-category chips */} {/* Sub-category */}
<div className="flex gap-2 mb-5 overflow-x-auto pb-2"> <div className="flex gap-1.5 overflow-x-auto pb-1">
{SUB_CATEGORIES[category]?.map((sub) => ( {SUB_CATEGORIES[category]?.map((sub) => (
<motion.button <button
key={sub} key={sub}
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap className={`px-2.5 py-1 rounded-lg text-[11px] font-medium whitespace-nowrap transition-all
transition-all border ${subCategory === sub ${subCategory === sub
? "bg-accent/20 text-accent border-accent/40" ? "bg-accent/20 text-accent border border-accent/40"
: "bg-surface-800/30 text-surface-400 border-white/5 hover:border-white/20"}`} : "bg-surface-700/30 text-surface-500 border border-transparent hover:text-surface-300"}`}
whileTap={{ scale: 0.95 }}
onClick={() => setSubCategory(sub)} onClick={() => setSubCategory(sub)}
> >
{sub} {sub}
</motion.button> </button>
))} ))}
</div> </div>
{/* Link input (for link mode) */}
{mode === "link" && (
<div className="space-y-2">
<div className="relative">
<i className="fa-solid fa-link absolute left-3 top-1/2 -translate-y-1/2 text-surface-500 text-sm" />
<input
type="url"
placeholder="Paste a link..."
value={linkUrl}
onChange={(e) => setLinkUrl(e.target.value)}
className="w-full pl-10 pr-4 py-3 rounded-xl
bg-surface-700/50 border border-white/10
text-surface-100 text-sm
placeholder:text-surface-500
focus:outline-none focus:border-cyan-500/50"
/>
{linkLoading && (
<i className="fa-solid fa-spinner fa-spin absolute right-3 top-1/2 -translate-y-1/2 text-surface-400" />
)}
</div>
{/* Link preview card */}
{linkPreview && (
<motion.div
className="rounded-xl bg-surface-700/30 border border-white/5 overflow-hidden"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
{linkPreview.image && (
<img src={linkPreview.image} alt="" className="w-full h-32 object-cover" />
)}
<div className="p-3">
<p className="text-xs text-surface-500 mb-1">{linkPreview.site_name || new URL(linkUrl).hostname}</p>
<p className="text-sm font-medium text-surface-200 line-clamp-2">{linkPreview.title}</p>
</div>
</motion.div>
)}
</div>
)}
{/* Live mode info */}
{mode === "live" && (
<motion.div
className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-rose-500/10 border border-red-500/20"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="flex items-center gap-3 mb-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-red-500 to-rose-500 flex items-center justify-center">
<i className="fa-solid fa-broadcast-tower text-white text-xl" />
</div>
<div>
<p className="text-white font-bold">Start a Live Stream</p>
<p className="text-surface-400 text-sm">Broadcast to your followers in real-time</p>
</div>
</div>
<p className="text-surface-400 text-xs">
Your location will be visible to viewers. The stream will appear on the map.
</p>
</motion.div>
)}
{/* Title input */} {/* Title input */}
<input <input
type="text" type="text"
placeholder="What's happening?" placeholder={mode === "live" ? "Stream title..." : "What's happening?"}
value={title} value={title}
onChange={(e) => setTitle(e.target.value)} onChange={(e) => setTitle(e.target.value)}
className="w-full px-4 py-3 mb-3 rounded-xl className="w-full px-4 py-3 rounded-xl
bg-surface-800/50 border border-white/10 bg-surface-700/50 border border-white/10
text-surface-100 text-lg font-medium text-white text-base font-medium
placeholder:text-surface-500 placeholder:text-surface-500
focus:outline-none focus:border-accent/50 focus:outline-none focus:border-cyan-500/50"
transition-colors"
/> />
{/* Body textarea */} {/* Body textarea (hidden for live mode) */}
{mode !== "live" && (
<textarea <textarea
ref={textareaRef} ref={textareaRef}
placeholder="Add more details..." placeholder="Add more details..."
value={body} value={body}
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
rows={2} rows={2}
className="w-full px-4 py-3 mb-4 rounded-xl className="w-full px-4 py-3 rounded-xl
bg-surface-800/50 border border-white/10 bg-surface-700/50 border border-white/10
text-surface-200 text-sm text-surface-200 text-sm leading-relaxed
placeholder:text-surface-500 placeholder:text-surface-500
focus:outline-none focus:border-accent/50 focus:outline-none focus:border-cyan-500/50
transition-colors resize-none" resize-none"
/> />
)}
{/* Image gallery */} {/* Image gallery (for photo/text modes) */}
{images.length > 0 && ( {(mode === "photo" || mode === "text") && images.length > 0 && (
<div className="grid grid-cols-3 gap-2 mb-4"> <div className="grid grid-cols-3 gap-2">
{images.map((url, idx) => ( {images.map((url, idx) => (
<div key={idx} className="relative aspect-square rounded-xl overflow-hidden group"> <div key={idx} className="relative aspect-square rounded-xl overflow-hidden group">
<img src={url} alt="" className="w-full h-full object-cover" /> <img src={url} alt="" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity" />
<motion.button <motion.button
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-black/60 className="absolute top-2 right-2 w-6 h-6 rounded-full bg-red-500/80
flex items-center justify-center text-white text-xs flex items-center justify-center text-white text-xs
opacity-0 group-hover:opacity-100 transition-opacity" opacity-0 group-hover:opacity-100 transition-opacity"
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
@ -249,56 +384,55 @@ export default function PostCreateModal({
{/* Upload progress */} {/* Upload progress */}
{uploading && ( {uploading && (
<div className="flex items-center gap-2 mb-4 text-surface-400 text-sm"> <div className="flex items-center justify-center gap-2 py-3 text-cyan-400 text-sm">
<i className="fa-solid fa-spinner fa-spin" /> <i className="fa-solid fa-cloud-arrow-up animate-bounce" />
Uploading... Uploading...
</div> </div>
)} )}
{/* Error */} {/* Error */}
{saveError && ( {saveError && (
<div className="flex items-center gap-2 mb-4 p-3 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm"> <div className="flex items-center gap-2 p-3 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm">
<i className="fa-solid fa-exclamation-circle" /> <i className="fa-solid fa-exclamation-circle" />
{saveError} {saveError}
</div> </div>
)} )}
{/* Location indicator */} {/* Location */}
{coords && ( {coords && (
<div className="flex items-center gap-2 mb-4 text-surface-400 text-sm"> <div className="flex items-center gap-2 text-surface-400 text-sm">
<i className="fa-solid fa-location-dot text-accent" /> <i className="fa-solid fa-location-dot text-emerald-500" />
<span>Location selected on map</span> <span>Location pinned on map</span>
</div> </div>
)} )}
</div> </div>
{/* Bottom toolbar */} {/* Bottom toolbar */}
<div className="flex items-center gap-3 px-5 py-4 border-t border-white/10 bg-surface-900/50"> {(mode === "photo" || mode === "text") && (
<div className="flex items-center gap-2 px-4 py-3 border-t border-white/5 bg-surface-800/50">
<motion.button <motion.button
className="flex items-center gap-2 px-4 py-2.5 rounded-xl className="flex items-center gap-2 px-3 py-2 rounded-xl
bg-surface-800/50 border border-white/10 bg-surface-700/50 border border-white/10
text-surface-300 text-sm font-medium text-surface-300 text-sm font-medium
hover:bg-surface-700/50 hover:text-white transition-all" hover:text-white hover:bg-surface-600/50 transition-all"
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.95 }}
whileTap={{ scale: 0.98 }}
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
disabled={uploading} disabled={uploading}
> >
<i className="fa-solid fa-image" /> <i className="fa-solid fa-image text-emerald-400" />
Photo Photo
</motion.button> </motion.button>
<motion.button <motion.button
className="flex items-center gap-2 px-4 py-2.5 rounded-xl className="flex items-center gap-2 px-3 py-2 rounded-xl
bg-surface-800/50 border border-white/10 bg-surface-700/50 border border-white/10
text-surface-300 text-sm font-medium text-surface-300 text-sm font-medium
hover:bg-surface-700/50 hover:text-white transition-all" hover:text-white hover:bg-surface-600/50 transition-all"
whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.95 }}
whileTap={{ scale: 0.98 }}
onClick={() => cameraInputRef.current?.click()} onClick={() => cameraInputRef.current?.click()}
disabled={uploading} disabled={uploading}
> >
<i className="fa-solid fa-camera" /> <i className="fa-solid fa-camera text-blue-400" />
Camera Camera
</motion.button> </motion.button>
@ -308,6 +442,7 @@ export default function PostCreateModal({
{title.length + body.length}/500 {title.length + body.length}/500
</span> </span>
</div> </div>
)}
{/* Hidden file inputs */} {/* Hidden file inputs */}
<input <input