modified: client/pages/Activity.tsx

This commit is contained in:
MrPiglr 2026-01-02 04:07:15 -07:00
parent abdb8943d5
commit dc9f7ea66e

View file

@ -1,6 +1,7 @@
import { useEffect, useState, useCallback, useRef, useMemo, type MouseEvent } from "react"; import { useEffect, useState, useCallback, useRef, useMemo, type MouseEvent } from "react";
import { useDiscordActivity } from "@/contexts/DiscordActivityContext"; import { useDiscordActivity } from "@/contexts/DiscordActivityContext";
import LoadingScreen from "@/components/LoadingScreen"; import LoadingScreen from "@/components/LoadingScreen";
import { supabase } from "@/lib/supabase";
import { import {
Heart, Heart,
MessageCircle, MessageCircle,
@ -972,37 +973,81 @@ function PollsTab({ userId, username }: { userId?: string; username?: string })
const [newOptions, setNewOptions] = useState(['', '']); const [newOptions, setNewOptions] = useState(['', '']);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
useEffect(() => { const fetchPolls = useCallback(async () => {
const fetchPolls = async () => { try {
try { const response = await fetch('/api/activity/polls');
const response = await fetch('/api/activity/polls'); if (response.ok) {
if (response.ok) { const data = await response.json();
const data = await response.json(); const mapped = (data.data || data || []).map((p: any) => {
const mapped = (data.data || data || []).map((p: any) => ({ const voteCounts = p.vote_counts || {};
const options = (p.options || []).map((opt: any, i: number) => ({
id: opt.id || `opt-${i}`,
text: opt.text || opt.option_text || String(opt),
votes: voteCounts[i] || 0,
}));
const expiresAt = p.expires_at ? new Date(p.expires_at).getTime() : Date.now() + 24 * 60 * 60 * 1000;
return {
id: p.id, id: p.id,
question: p.question, question: p.question,
options: (p.options || []).map((opt: any, i: number) => ({ options,
id: opt.id || `opt-${i}`,
text: opt.text || opt.option_text,
votes: opt.votes || opt.vote_count || 0,
})),
createdBy: p.creator_id || p.created_by || 'system', createdBy: p.creator_id || p.created_by || 'system',
createdByName: p.creator_name || 'Anonymous', createdByName: p.creator_name || 'Anonymous',
createdAt: new Date(p.created_at).getTime(), createdAt: new Date(p.created_at || Date.now()).getTime(),
expiresAt: new Date(p.expires_at).getTime(), expiresAt,
votedUsers: p.voted_users || [], votedUsers: p.voted_users || [],
})); } as Poll;
setPolls(mapped.filter((p: Poll) => p.expiresAt > Date.now())); });
} setPolls(mapped.filter((p: Poll) => p.expiresAt > Date.now()));
} catch {
setPolls([]);
} finally {
setLoading(false);
} }
}; } catch {
fetchPolls(); setPolls([]);
} finally {
setLoading(false);
}
}, []); }, []);
useEffect(() => {
fetchPolls();
const channel = supabase.channel("activity-polls");
channel.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "activity_polls" },
() => {
fetchPolls();
},
);
channel.on(
"postgres_changes",
{ event: "INSERT", schema: "public", table: "activity_poll_votes" },
(payload) => {
const vote = payload.new as any;
if (!vote?.poll_id || vote.option_index === undefined) return;
setPolls((prev) => {
const exists = prev.some((p) => p.id === vote.poll_id);
if (!exists) return prev;
return prev.map((p) => {
if (p.id !== vote.poll_id) return p;
const options = p.options.map((opt, idx) =>
idx === vote.option_index ? { ...opt, votes: opt.votes + 1 } : opt,
);
const votedUsers = p.votedUsers.includes(vote.user_id)
? p.votedUsers
: [...p.votedUsers, vote.user_id];
return { ...p, options, votedUsers };
});
});
},
);
channel.subscribe().catch(() => {});
return () => {
supabase.removeChannel(channel);
};
}, [fetchPolls]);
const createPoll = async () => { const createPoll = async () => {
if (!userId || !newQuestion.trim() || newOptions.filter(o => o.trim()).length < 2) return; if (!userId || !newQuestion.trim() || newOptions.filter(o => o.trim()).length < 2) return;
@ -1050,6 +1095,8 @@ function PollsTab({ userId, username }: { userId?: string; username?: string })
const poll = polls.find(p => p.id === pollId); const poll = polls.find(p => p.id === pollId);
if (!poll || poll.votedUsers.includes(userId)) return; if (!poll || poll.votedUsers.includes(userId)) return;
const optionIndex = poll.options.findIndex(opt => opt.id === optionId);
if (optionIndex === -1) return;
setPolls(prev => prev.map(p => { setPolls(prev => prev.map(p => {
if (p.id !== pollId) return p; if (p.id !== pollId) return p;
@ -1066,7 +1113,7 @@ function PollsTab({ userId, username }: { userId?: string; username?: string })
await fetch(`/api/activity/polls/${pollId}/vote`, { await fetch(`/api/activity/polls/${pollId}/vote`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: userId, option_id: optionId }), body: JSON.stringify({ user_id: userId, option_index: optionIndex }),
}); });
} catch {} } catch {}
}; };
@ -2548,35 +2595,75 @@ function ChatTab({
}) { }) {
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [hasLoaded, setHasLoaded] = useState(false);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { const fetchMessages = useCallback(async () => {
const fetchMessages = async () => { if (!hasLoaded) setLoading(true);
try { try {
const response = await fetch('/api/activity/chat'); const response = await fetch('/api/activity/chat');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
const mapped = (data.data || data || []).map((m: any) => ({ const mapped = (data.data || data || []).map((m: any) => ({
id: m.id, id: m.id,
userId: m.user_id || m.userId, userId: m.user_id || m.userId,
username: m.username || m.user_name || 'Anonymous', username: m.username || m.user_name || 'Anonymous',
avatar: m.avatar_url || m.avatar || null, avatar: m.avatar_url || m.avatar || null,
content: m.content || m.message, content: m.content || m.message,
timestamp: new Date(m.created_at || m.timestamp).getTime(), timestamp: new Date(m.created_at || m.timestamp).getTime(),
})); }));
setMessages(mapped); setMessages(mapped);
}
} catch {
setMessages([]);
} finally {
setLoading(false);
} }
}; } catch {
setMessages([]);
} finally {
setLoading(false);
setHasLoaded(true);
}
}, [hasLoaded]);
useEffect(() => {
fetchMessages(); fetchMessages();
}, []); const interval = setInterval(fetchMessages, 5000);
const channel = supabase.channel("activity-chat");
channel.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "activity_chat_messages",
},
(payload) => {
const m: any = payload.new || {};
const incoming: ChatMessage = {
id: m.id || `${Date.now()}`,
userId: m.user_id || m.userId,
username: m.username || m.user_name || "Anonymous",
avatar: m.avatar_url || m.avatar || null,
content: m.content || m.message,
timestamp: new Date(m.created_at || m.timestamp || Date.now()).getTime(),
};
setMessages((prev) => {
if (prev.some((p) => p.id === incoming.id)) return prev;
const next = [...prev, incoming].sort((a, b) => a.timestamp - b.timestamp);
return next.slice(-200);
});
setHasLoaded(true);
setLoading(false);
},
);
channel.subscribe().catch(() => {});
return () => {
clearInterval(interval);
supabase.removeChannel(channel);
};
}, [fetchMessages]);
useEffect(() => { useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@ -2609,11 +2696,13 @@ function ChatTab({
content: inputValue.trim(), content: inputValue.trim(),
}), }),
}); });
// Refresh from server to keep order aligned
await fetchMessages();
} catch {} } catch {}
setSending(false); setSending(false);
inputRef.current?.focus(); inputRef.current?.focus();
}, [inputValue, userId, username, avatar, sending]); }, [inputValue, userId, username, avatar, sending, fetchMessages]);
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {