import { useState, useCallback, useEffect } from 'react'; import { useAuth, getToken } from '../../contexts/AuthContext'; import { formatDateTime } from '../../utils/format'; import type { EventPost, EventType, Poll, UserRole } from '../../types'; const EVENT_TYPE_COLORS: Record = { announcement: 'var(--color-yellow)', update: 'var(--color-blue)', milestone: 'var(--color-green)', poll: 'var(--color-amber)', }; const ROLE_COLORS: Record = { dev: 'var(--color-green)', com: 'var(--color-amber)', user: 'var(--color-text-muted)', }; const EVENT_TYPE_LABELS: Record = { announcement: 'ANNOUNCEMENT', update: 'DEV UPDATE', milestone: 'MILESTONE', poll: 'COMMUNITY POLL', }; // ── Poll Component ───────────────────────────────────────────────────────────── function PollCard({ poll, onVote }: { poll: Poll; onVote: (pollId: string, optionId: string) => void }) { const { user } = useAuth(); const totalVotes = poll.options.reduce((sum, opt) => sum + opt.votes, 0); const isEnded = poll.endsAt ? new Date(poll.endsAt) < new Date() : false; return (
{poll.question}
{poll.options.map((option) => { const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0; const userVoted = option.votedUserIds.includes(user?.id || ''); return (
{ if (!isEnded && poll.isActive && user) { onVote(poll.id, option.id); } }} > {/* Progress bar */}
{userVoted && '✓ '} {option.text} {option.votes} ({percentage}%)
); })}
{totalVotes} total votes {poll.allowMultipleVotes && ' • Multiple votes allowed'} {poll.endsAt && ( {isEnded ? 'Poll Ended' : `Ends ${formatDateTime(poll.endsAt)}`} )}
); } // ── Event Card Component ─────────────────────────────────────────────────────── function EventCard({ event, poll, onVote, }: { event: EventPost; poll?: Poll; onVote: (pollId: string, optionId: string) => void; }) { return (
{/* Header */}
{EVENT_TYPE_LABELS[event.type]} {event.isPublic && ( PUBLIC )}

{event.title}

{event.authorName} {formatDateTime(event.createdAt)}
{/* Content */}
{event.content}
{/* Poll if exists */} {poll && }
); } // ── Main Component ───────────────────────────────────────────────────────────── function apiFetch(path: string, options: RequestInit = {}): Promise { const token = getToken(); return fetch(`/api${path}`, { ...options, headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(options.headers as Record ?? {}), }, }).then(async (res) => { if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error ?? `Request failed (${res.status})`); } return res.json() as Promise; }); } export default function IntranetEvents() { const { user } = useAuth(); const [events, setEvents] = useState([]); const [showCreateForm, setShowCreateForm] = useState(false); // Form state const [eventType, setEventType] = useState('announcement'); const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [isPublic, setIsPublic] = useState(true); const [createPoll, setCreatePoll] = useState(false); const [pollQuestion, setPollQuestion] = useState(''); const [pollOptions, setPollOptions] = useState(['', '']); const [error, setError] = useState(''); const [posting, setPosting] = useState(false); useEffect(() => { apiFetch<{ events: EventPost[] }>('/events?limit=50') .then((res) => setEvents(res.events)) .catch(() => setEvents([])); }, []); const handleVote = useCallback( async (pollId: string, optionId: string) => { const event = events.find((e) => e.pollId === pollId || e.poll?.id === pollId); if (!event || !user) return; try { const updated = await apiFetch(`/events/${event.id}/vote`, { method: 'POST', body: JSON.stringify({ optionIds: [optionId] }), }); setEvents((prev) => prev.map((e) => (e.id === updated.id ? updated : e))); } catch { // silently ignore } }, [events, user] ); const handleSubmit = useCallback(async () => { if (!title.trim()) { setError('Title is required.'); return; } if (!content.trim()) { setError('Content is required.'); return; } if (createPoll) { if (!pollQuestion.trim()) { setError('Poll question is required.'); return; } const validOptions = pollOptions.filter((opt) => opt.trim()); if (validOptions.length < 2) { setError('Poll must have at least 2 options.'); return; } } if (!user) return; setError(''); setPosting(true); try { const body: Record = { type: createPoll ? 'poll' : eventType, title: title.trim(), content: content.trim(), isPublic, }; if (createPoll) { body.poll = { question: pollQuestion.trim(), options: pollOptions.filter((o) => o.trim()).map((o) => ({ text: o.trim() })), }; } const created = await apiFetch('/events', { method: 'POST', body: JSON.stringify(body), }); setEvents((prev) => [created, ...prev]); // Reset form setTitle(''); setContent(''); setEventType('announcement'); setIsPublic(true); setCreatePoll(false); setPollQuestion(''); setPollOptions(['', '']); setShowCreateForm(false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create event.'); } finally { setPosting(false); } }, [title, content, eventType, isPublic, createPoll, pollQuestion, pollOptions, user]); return (
INTRANET / EVENTS

COMMUNITY EVENTS

Post game development updates, announcements, and community polls. Public events are visible to all users.

{/* Create Event Button */} {!showCreateForm && ( )} {/* Create Event Form */} {showCreateForm && (
CREATE NEW EVENT
{/* Event Type */}
{/* Title */}
setTitle(e.target.value)} style={{ fontSize: '0.85rem' }} />
{/* Content */}