Add commenter avatars & category colors to comments (v0.0.89)

- Fetch profile avatars for commenters
- Apply post category gradient to comment avatars
- Add events, friends, market to CATEGORY_CONFIG
- Fix first comment race condition with setTimeout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-20 06:53:52 +00:00
parent 0119174969
commit d051a9337c
2 changed files with 30 additions and 5 deletions

View File

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

View File

@ -138,6 +138,10 @@ const CATEGORY_CONFIG = {
traffic: { icon: "fa-car", gradient: "from-yellow-500 to-amber-500", color: "#fbbf24" }, traffic: { icon: "fa-car", gradient: "from-yellow-500 to-amber-500", color: "#fbbf24" },
live: { icon: "fa-broadcast-tower", gradient: "from-red-500 to-rose-500", color: "#ef4444" }, live: { icon: "fa-broadcast-tower", gradient: "from-red-500 to-rose-500", color: "#ef4444" },
camera: { icon: "fa-video", gradient: "from-red-500 to-rose-500", color: "#ef4444" }, camera: { icon: "fa-video", gradient: "from-red-500 to-rose-500", color: "#ef4444" },
events: { icon: "fa-calendar", gradient: "from-yellow-500 to-orange-500", color: "#eab308" },
event: { icon: "fa-calendar", gradient: "from-yellow-500 to-orange-500", color: "#eab308" },
friends: { icon: "fa-user-group", gradient: "from-green-500 to-emerald-500", color: "#22c55e" },
market: { icon: "fa-store", gradient: "from-amber-500 to-orange-500", color: "#f59e0b" },
default: { icon: "fa-newspaper", gradient: "from-blue-500 to-cyan-500", color: "#60a5fa" }, default: { icon: "fa-newspaper", gradient: "from-blue-500 to-cyan-500", color: "#60a5fa" },
}; };
@ -486,6 +490,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
const [liked, setLiked] = useState(false); const [liked, setLiked] = useState(false);
const [truth, setTruth] = useState({ score: 50, count: 0 }); const [truth, setTruth] = useState({ score: 50, count: 0 });
const [authorAvatar, setAuthorAvatar] = useState(""); const [authorAvatar, setAuthorAvatar] = useState("");
const [commenterAvatars, setCommenterAvatars] = useState({});
const [comments, setComments] = useState([]); const [comments, setComments] = useState([]);
const [commentInput, setCommentInput] = useState(""); const [commentInput, setCommentInput] = useState("");
const [commentSending, setCommentSending] = useState(false); const [commentSending, setCommentSending] = useState(false);
@ -535,6 +540,22 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
}); });
}, [post?.author, post?.username]); }, [post?.author, post?.username]);
// Fetch commenter avatars
useEffect(() => {
if (!comments.length) return;
const usernames = [...new Set(comments.map(c => c.username).filter(Boolean))];
// Only fetch for usernames we don't already have
const toFetch = usernames.filter(u => !commenterAvatars[u]);
if (!toFetch.length) return;
toFetch.forEach(username => {
fetchProfile(username).then(res => {
if (res?.avatar_url) {
setCommenterAvatars(prev => ({ ...prev, [username]: res.avatar_url }));
}
});
});
}, [comments]);
// Fetch comments // Fetch comments
const refreshComments = useCallback(async (autoOpen = false) => { const refreshComments = useCallback(async (autoOpen = false) => {
if (!postId) return; if (!postId) return;
@ -592,8 +613,8 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
} else { } else {
setComments(prev => prev.map(c => c.id === tempId ? { ...c, _pending: false } : c)); setComments(prev => prev.map(c => c.id === tempId ? { ...c, _pending: false } : c));
} }
// Refresh comments to get the real list // Delay refresh to let backend commit
refreshComments(); setTimeout(() => refreshComments(), 500);
} else { } else {
setComments(prev => prev.map(c => c.id === tempId ? { ...c, _failed: true, _pending: false } : c)); setComments(prev => prev.map(c => c.id === tempId ? { ...c, _failed: true, _pending: false } : c));
} }
@ -874,8 +895,12 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
<motion.div key={c.id} className={`p-3 rounded-xl bg-themed-primary border border-themed ${c._pending ? "opacity-50" : ""} ${c._failed ? "border-red-500/30" : ""}`} <motion.div key={c.id} className={`p-3 rounded-xl bg-themed-primary border border-themed ${c._pending ? "opacity-50" : ""} ${c._failed ? "border-red-500/30" : ""}`}
initial={{ opacity: 0, y: 10 }} animate={{ opacity: c._pending ? 0.5 : 1, y: 0 }}> initial={{ opacity: 0, y: 10 }} animate={{ opacity: c._pending ? 0.5 : 1, y: 0 }}>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-accent to-blue-500 flex items-center justify-center text-white text-xs font-bold flex-shrink-0"> <div className={`w-8 h-8 rounded-full bg-gradient-to-br ${config.gradient} flex items-center justify-center text-white text-xs font-bold flex-shrink-0 overflow-hidden`}>
{(c.username || "?")[0]?.toUpperCase()} {commenterAvatars[c.username] ? (
<img src={commenterAvatars[c.username]} alt={c.username} className="w-full h-full object-cover" />
) : (
(c.username || "?")[0]?.toUpperCase()
)}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5"> <div className="flex items-center gap-2 mb-0.5">