feat: Cleaner topbar + new post creation modal
- Simplified topbar design (cleaner, more minimal) - Created PostCreateModal.jsx for single-screen post creation - Removed complex animated gradients from topbar - Made logo and brand text cleaner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1ea2b58066
commit
f2992bd791
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "sociowire-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.48",
|
||||
"version": "0.0.49",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0",
|
||||
|
|
|
|||
|
|
@ -480,88 +480,31 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
|
|||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
|
||||
>
|
||||
{/* Animated gradient background */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-surface-950 via-surface-900 to-surface-950" />
|
||||
<motion.div
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, transparent, rgba(139,92,246,0.1), rgba(236,72,153,0.1), transparent)",
|
||||
backgroundSize: "200% 100%",
|
||||
}}
|
||||
animate={{ backgroundPosition: ["0% 0%", "200% 0%"] }}
|
||||
transition={{ duration: 8, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
{/* Glass overlay */}
|
||||
<div className="absolute inset-0 backdrop-blur-xl bg-black/20" />
|
||||
{/* Bottom glow line */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-accent/50 to-transparent" />
|
||||
{/* Clean dark background */}
|
||||
<div className="absolute inset-0 bg-surface-950/95 backdrop-blur-xl" />
|
||||
{/* Subtle bottom border */}
|
||||
<div className="absolute bottom-0 left-0 right-0 h-px bg-surface-700/50" />
|
||||
|
||||
<div className="relative flex items-center justify-between gap-2 px-3 py-2.5 sm:px-5 sm:py-3">
|
||||
{/* Left: Logo & Brand with glow */}
|
||||
<motion.div
|
||||
className="flex items-center gap-2 sm:gap-3 cursor-pointer"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{/* Logo with glow effect */}
|
||||
<div className="relative">
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-2xl bg-gradient-to-br from-blue-500/40 to-cyan-500/40 blur-xl"
|
||||
animate={{ opacity: [0.5, 0.8, 0.5] }}
|
||||
transition={{ duration: 3, repeat: Infinity }}
|
||||
/>
|
||||
<div className="relative w-10 h-10 sm:w-11 sm:h-11 rounded-2xl
|
||||
bg-gradient-to-br from-blue-500 to-cyan-500 p-0.5
|
||||
shadow-lg shadow-accent/30">
|
||||
<div className="w-full h-full rounded-[14px] bg-surface-900 p-1.5 flex items-center justify-center">
|
||||
<img src="/logo.png" alt="SocioWire" className="w-full h-full object-contain" />
|
||||
{/* Left: Clean Logo */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-blue-500 to-cyan-500 p-0.5">
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Brand text */}
|
||||
<div className="flex flex-col leading-none">
|
||||
<motion.span
|
||||
className="text-base sm:text-lg font-black tracking-tight"
|
||||
style={{
|
||||
background: "linear-gradient(135deg, #fff 0%, #60a5fa 50%, #38bdf8 100%)",
|
||||
WebkitBackgroundClip: "text",
|
||||
WebkitTextFillColor: "transparent",
|
||||
}}
|
||||
>
|
||||
SOCIOWIRE
|
||||
</motion.span>
|
||||
<span className="flex items-center gap-1.5 text-[10px] font-medium text-surface-400">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-base font-bold text-white tracking-tight">sociowire</span>
|
||||
<span className="flex items-center gap-1 text-[9px] text-surface-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
{t('topbar.wiredToLife')}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Center: Theme selector - desktop */}
|
||||
<div className="hidden lg:flex items-center gap-1 p-1 rounded-2xl bg-surface-900/60 border border-white/5 backdrop-blur-sm">
|
||||
{THEMES.map((themeKey, idx) => (
|
||||
<motion.button
|
||||
key={themeKey}
|
||||
type="button"
|
||||
className={`relative px-4 py-2 rounded-xl text-xs font-bold uppercase tracking-wider transition-all
|
||||
${theme === themeKey ? "text-white" : "text-surface-500 hover:text-surface-300"}`}
|
||||
onClick={() => onChangeTheme?.(themeKey)}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
{theme === themeKey && (
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500"
|
||||
layoutId="themeIndicator"
|
||||
transition={{ type: "spring", stiffness: 500, damping: 35 }}
|
||||
/>
|
||||
)}
|
||||
<span className="relative">{t(`theme.${themeKey}`)}</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Center: Spacer for balance */}
|
||||
<div className="hidden lg:block flex-1" />
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center gap-2 sm:gap-2.5">
|
||||
{/* PWA Install */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { uploadImage } from "../../api/client";
|
||||
|
||||
const CATEGORIES = [
|
||||
{ id: "News", icon: "fa-newspaper", color: "from-blue-500 to-cyan-500" },
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
const SUB_CATEGORIES = {
|
||||
News: ["World", "Local", "Politics", "Tech", "Finance", "Sports"],
|
||||
Friends: ["Nearby", "Stories", "Hangout", "Help"],
|
||||
Events: ["Today", "Weekend", "Music", "Sports", "Food"],
|
||||
Market: ["Deals", "Services", "Jobs", "Housing"],
|
||||
};
|
||||
|
||||
export default function PostCreateModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
coords,
|
||||
initialCategory = "News",
|
||||
isSaving = false,
|
||||
saveError = "",
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [category, setCategory] = useState(initialCategory);
|
||||
const [subCategory, setSubCategory] = useState(SUB_CATEGORIES[initialCategory]?.[0] || "");
|
||||
const [title, setTitle] = useState("");
|
||||
const [body, setBody] = useState("");
|
||||
const [images, setImages] = useState([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef(null);
|
||||
const cameraInputRef = useRef(null);
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
// Reset on open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTitle("");
|
||||
setBody("");
|
||||
setImages([]);
|
||||
setCategory(initialCategory);
|
||||
setSubCategory(SUB_CATEGORIES[initialCategory]?.[0] || "");
|
||||
}
|
||||
}, [isOpen, initialCategory]);
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = "auto";
|
||||
textareaRef.current.style.height = textareaRef.current.scrollHeight + "px";
|
||||
}
|
||||
}, [body]);
|
||||
|
||||
const handleFileChange = useCallback(async (e) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
if (files.length === 0) return;
|
||||
|
||||
setUploading(true);
|
||||
try {
|
||||
for (const file of files) {
|
||||
const result = await uploadImage(file);
|
||||
if (result?.url) {
|
||||
setImages((prev) => [...prev, result.url]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Upload failed:", err);
|
||||
}
|
||||
setUploading(false);
|
||||
e.target.value = "";
|
||||
}, []);
|
||||
|
||||
const removeImage = (index) => {
|
||||
setImages((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!title.trim() && !body.trim() && images.length === 0) return;
|
||||
|
||||
onSubmit?.({
|
||||
title: title.trim(),
|
||||
body: body.trim(),
|
||||
category,
|
||||
subCategory,
|
||||
images,
|
||||
coords,
|
||||
});
|
||||
};
|
||||
|
||||
const canSubmit = (title.trim() || body.trim() || images.length > 0) && !isSaving && !uploading;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="fixed inset-0 z-[1200] flex items-end sm:items-center justify-center"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
className="relative w-full max-w-lg mx-4 mb-0 sm:mb-0
|
||||
bg-gradient-to-br from-surface-900 via-surface-950 to-surface-900
|
||||
rounded-t-3xl sm:rounded-3xl border border-white/10
|
||||
shadow-2xl overflow-hidden"
|
||||
initial={{ y: "100%", opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: "100%", opacity: 0 }}
|
||||
transition={{ type: "spring", damping: 25, stiffness: 300 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-white/10">
|
||||
<motion.button
|
||||
className="w-9 h-9 rounded-full bg-surface-800/80 border border-white/10
|
||||
flex items-center justify-center text-surface-300 hover:text-white
|
||||
hover:bg-red-500/20 hover:border-red-500/40 transition-all"
|
||||
whileHover={{ scale: 1.1 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={onClose}
|
||||
>
|
||||
<i className="fa-solid fa-times" />
|
||||
</motion.button>
|
||||
|
||||
<h2 className="text-lg font-bold text-surface-100">Create Post</h2>
|
||||
|
||||
<motion.button
|
||||
className={`px-5 py-2 rounded-full font-bold text-sm transition-all
|
||||
${canSubmit
|
||||
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg shadow-blue-500/30"
|
||||
: "bg-surface-700/50 text-surface-400 cursor-not-allowed"}`}
|
||||
whileHover={canSubmit ? { scale: 1.05 } : {}}
|
||||
whileTap={canSubmit ? { scale: 0.95 } : {}}
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
>
|
||||
{isSaving ? (
|
||||
<><i className="fa-solid fa-spinner fa-spin mr-2" />Posting...</>
|
||||
) : (
|
||||
"Post"
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-5 max-h-[70vh] overflow-y-auto">
|
||||
{/* Category chips */}
|
||||
<div className="flex gap-2 mb-4 overflow-x-auto pb-2">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<motion.button
|
||||
key={cat.id}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium whitespace-nowrap
|
||||
transition-all border ${category === cat.id
|
||||
? `bg-gradient-to-r ${cat.color} text-white border-white/20 shadow-lg`
|
||||
: "bg-surface-800/50 text-surface-300 border-white/5 hover:border-white/20"}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => {
|
||||
setCategory(cat.id);
|
||||
setSubCategory(SUB_CATEGORIES[cat.id]?.[0] || "");
|
||||
}}
|
||||
>
|
||||
<i className={`fa-solid ${cat.icon}`} />
|
||||
{cat.id}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sub-category chips */}
|
||||
<div className="flex gap-2 mb-5 overflow-x-auto pb-2">
|
||||
{SUB_CATEGORIES[category]?.map((sub) => (
|
||||
<motion.button
|
||||
key={sub}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium whitespace-nowrap
|
||||
transition-all border ${subCategory === sub
|
||||
? "bg-accent/20 text-accent border-accent/40"
|
||||
: "bg-surface-800/30 text-surface-400 border-white/5 hover:border-white/20"}`}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setSubCategory(sub)}
|
||||
>
|
||||
{sub}
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Title input */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="What's happening?"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
className="w-full px-4 py-3 mb-3 rounded-xl
|
||||
bg-surface-800/50 border border-white/10
|
||||
text-surface-100 text-lg font-medium
|
||||
placeholder:text-surface-500
|
||||
focus:outline-none focus:border-accent/50
|
||||
transition-colors"
|
||||
/>
|
||||
|
||||
{/* Body textarea */}
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
placeholder="Add more details..."
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={2}
|
||||
className="w-full px-4 py-3 mb-4 rounded-xl
|
||||
bg-surface-800/50 border border-white/10
|
||||
text-surface-200 text-sm
|
||||
placeholder:text-surface-500
|
||||
focus:outline-none focus:border-accent/50
|
||||
transition-colors resize-none"
|
||||
/>
|
||||
|
||||
{/* Image gallery */}
|
||||
{images.length > 0 && (
|
||||
<div className="grid grid-cols-3 gap-2 mb-4">
|
||||
{images.map((url, idx) => (
|
||||
<div key={idx} className="relative aspect-square rounded-xl overflow-hidden group">
|
||||
<img src={url} alt="" className="w-full h-full object-cover" />
|
||||
<motion.button
|
||||
className="absolute top-2 right-2 w-6 h-6 rounded-full bg-black/60
|
||||
flex items-center justify-center text-white text-xs
|
||||
opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
whileTap={{ scale: 0.9 }}
|
||||
onClick={() => removeImage(idx)}
|
||||
>
|
||||
<i className="fa-solid fa-times" />
|
||||
</motion.button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload progress */}
|
||||
{uploading && (
|
||||
<div className="flex items-center gap-2 mb-4 text-surface-400 text-sm">
|
||||
<i className="fa-solid fa-spinner fa-spin" />
|
||||
Uploading...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{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">
|
||||
<i className="fa-solid fa-exclamation-circle" />
|
||||
{saveError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location indicator */}
|
||||
{coords && (
|
||||
<div className="flex items-center gap-2 mb-4 text-surface-400 text-sm">
|
||||
<i className="fa-solid fa-location-dot text-accent" />
|
||||
<span>Location selected on map</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom toolbar */}
|
||||
<div className="flex items-center gap-3 px-5 py-4 border-t border-white/10 bg-surface-900/50">
|
||||
<motion.button
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl
|
||||
bg-surface-800/50 border border-white/10
|
||||
text-surface-300 text-sm font-medium
|
||||
hover:bg-surface-700/50 hover:text-white transition-all"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<i className="fa-solid fa-image" />
|
||||
Photo
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl
|
||||
bg-surface-800/50 border border-white/10
|
||||
text-surface-300 text-sm font-medium
|
||||
hover:bg-surface-700/50 hover:text-white transition-all"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => cameraInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
>
|
||||
<i className="fa-solid fa-camera" />
|
||||
Camera
|
||||
</motion.button>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<span className="text-xs text-surface-500">
|
||||
{title.length + body.length}/500
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Hidden file inputs */}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
hidden
|
||||
/>
|
||||
<input
|
||||
ref={cameraInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={handleFileChange}
|
||||
hidden
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue