chore : move all to root

This commit is contained in:
Thibault Pouch
2026-02-26 16:16:44 +01:00
parent 308a758e79
commit c2d94a349c
44 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
import { useState, useCallback } from 'react';
import { MOCK_EVENTS, MOCK_POLLS } from '../../data/mockData';
import { useAuth } from '../../contexts/AuthContext';
import { formatDateTime } from '../../utils/format';
import type { EventPost, EventType, Poll, UserRole } from '../../types';
const EVENT_TYPE_COLORS: Record<EventType, string> = {
announcement: 'var(--color-yellow)',
update: 'var(--color-blue)',
milestone: 'var(--color-green)',
poll: 'var(--color-amber)',
};
const EVENT_TYPE_LABELS: Record<EventType, string> = {
announcement: 'ANNOUNCEMENT',
update: 'DEV UPDATE',
milestone: 'MILESTONE',
poll: 'COMMUNITY POLL',
};
const ROLE_COLORS: Record<UserRole, string> = {
dev: 'var(--color-green)',
com: 'var(--color-amber)',
user: 'var(--color-text-muted)',
};
// ── 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 (
<div
style={{
background: 'var(--color-bg-alt)',
border: '1px solid var(--color-border)',
padding: '1rem',
marginTop: '0.75rem',
}}
>
<div
style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.85rem',
color: 'var(--color-text)',
marginBottom: '0.85rem',
fontWeight: 500,
}}
>
{poll.question}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{poll.options.map((option) => {
const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0;
const userVoted = option.votedUserIds.includes(user?.id || '');
return (
<div
key={option.id}
style={{
position: 'relative',
background: 'var(--color-surface)',
border: `1px solid ${userVoted ? 'var(--color-amber)' : 'var(--color-border)'}`,
padding: '0.6rem 0.75rem',
cursor: !isEnded && poll.isActive && user ? 'pointer' : 'default',
opacity: isEnded || !poll.isActive ? 0.7 : 1,
transition: 'border-color 0.2s',
}}
onClick={() => {
if (!isEnded && poll.isActive && user) {
onVote(poll.id, option.id);
}
}}
>
{/* Progress bar */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
width: `${percentage}%`,
background: userVoted
? 'rgba(217,119,6,0.15)'
: 'rgba(59,130,246,0.1)',
transition: 'width 0.3s ease',
pointerEvents: 'none',
}}
/>
<div
style={{
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
fontFamily: 'var(--font-mono)',
fontSize: '0.75rem',
}}
>
<span style={{ color: 'var(--color-text-dim)' }}>
{userVoted && '✓ '}
{option.text}
</span>
<span style={{ color: 'var(--color-text-muted)', fontSize: '0.7rem' }}>
{option.votes} ({percentage}%)
</span>
</div>
</div>
);
})}
</div>
<div
style={{
marginTop: '0.75rem',
fontFamily: 'var(--font-mono)',
fontSize: '0.65rem',
color: 'var(--color-text-muted)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'wrap',
gap: '0.5rem',
}}
>
<span>
{totalVotes} total vote{totalVotes !== 1 ? 's' : ''}
{poll.allowMultipleVotes && ' • Multiple votes allowed'}
</span>
{poll.endsAt && (
<span style={{ color: isEnded ? 'var(--color-red)' : 'var(--color-amber)' }}>
{isEnded ? 'Poll Ended' : `Ends ${formatDateTime(poll.endsAt)}`}
</span>
)}
</div>
{!user && !isEnded && poll.isActive && (
<div
style={{
marginTop: '0.75rem',
padding: '0.5rem',
background: 'rgba(217,119,6,0.1)',
border: '1px solid rgba(217,119,6,0.25)',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-amber)',
textAlign: 'center',
}}
>
Please <a href="/login" style={{ color: 'var(--color-amber)', textDecoration: 'underline' }}>log in</a> to vote
</div>
)}
</div>
);
}
// ── Event Card Component ───────────────────────────────────────────────────────
function EventCard({
event,
poll,
onVote,
}: {
event: EventPost;
poll?: Poll;
onVote: (pollId: string, optionId: string) => void;
}) {
return (
<div
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '1.5rem',
}}
>
{/* Header */}
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
marginBottom: '1rem',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<span
style={{
fontFamily: 'var(--font-mono)',
background: `${EVENT_TYPE_COLORS[event.type]}15`,
border: `1px solid ${EVENT_TYPE_COLORS[event.type]}40`,
color: EVENT_TYPE_COLORS[event.type],
fontSize: '0.6rem',
padding: '0.2rem 0.5rem',
letterSpacing: '0.08em',
}}
>
{EVENT_TYPE_LABELS[event.type]}
</span>
</div>
<h2
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-text)',
fontSize: 'clamp(1.25rem, 4vw, 1.5rem)',
lineHeight: 1.2,
}}
>
{event.title}
</h2>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
flexWrap: 'wrap',
}}
>
<span style={{ color: ROLE_COLORS[event.authorRole] }}>
{event.authorName}
</span>
<span></span>
<span>{formatDateTime(event.createdAt)}</span>
</div>
</div>
{/* Content */}
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-dim)',
fontSize: '0.88rem',
lineHeight: 1.75,
whiteSpace: 'pre-wrap',
}}
>
{event.content}
</div>
{/* Poll if exists */}
{poll && <PollCard poll={poll} onVote={onVote} />}
</div>
);
}
// ── Main Component ─────────────────────────────────────────────────────────────
export default function EventsPage() {
const { user } = useAuth();
// Filter to show only public events
const publicEvents = MOCK_EVENTS.filter((event) => event.isPublic);
const [events] = useState<EventPost[]>(publicEvents);
const [polls, setPolls] = useState<Poll[]>(MOCK_POLLS);
const handleVote = useCallback(
(pollId: string, optionId: string) => {
if (!user) return;
setPolls((prevPolls) =>
prevPolls.map((poll) => {
if (poll.id !== pollId) return poll;
const hasVotedForOption = poll.options.some((opt) =>
opt.votedUserIds.includes(user.id)
);
return {
...poll,
options: poll.options.map((opt) => {
if (opt.id === optionId) {
// Add vote to this option
return {
...opt,
votes: opt.votedUserIds.includes(user.id) ? opt.votes : opt.votes + 1,
votedUserIds: opt.votedUserIds.includes(user.id)
? opt.votedUserIds
: [...opt.votedUserIds, user.id],
};
} else if (!poll.allowMultipleVotes && hasVotedForOption) {
// Remove vote from other options if single vote
return {
...opt,
votes: opt.votedUserIds.includes(user.id) ? opt.votes - 1 : opt.votes,
votedUserIds: opt.votedUserIds.filter((id) => id !== user.id),
};
}
return opt;
}),
};
})
);
},
[user]
);
return (
<div className="page-wrapper">
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-8 sm:py-12">
{/* Header */}
<div style={{ marginBottom: '3rem', textAlign: 'center' }}>
<div
className="section-label"
style={{ marginBottom: '0.75rem' }}
>
DEVELOPMENT UPDATES
</div>
<h1
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-text)',
fontSize: 'clamp(2rem, 6vw, 3rem)',
marginBottom: '1rem',
letterSpacing: '0.05em',
}}
>
COMMUNITY EVENTS
</h1>
<p
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.85rem',
maxWidth: '600px',
margin: '0 auto',
lineHeight: 1.6,
}}
>
Stay up to date with the latest game development news, announcements, and participate
in community polls to help shape the future of Headless Hazard.
</p>
</div>
{/* Events Grid */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{events.length === 0 ? (
<div
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '3rem 2rem',
textAlign: 'center',
}}
>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.85rem',
}}
>
No events available at the moment. Check back soon!
</div>
</div>
) : (
events.map((event) => {
const poll = event.pollId ? polls.find((p) => p.id === event.pollId) : undefined;
return <EventCard key={event.id} event={event} poll={poll} onVote={handleVote} />;
})
)}
</div>
</div>
</div>
);
}