feat : Initial commit

This commit is contained in:
Thibault Pouch
2026-02-26 16:26:16 +01:00
commit 4bfc10fd98
8 changed files with 1885 additions and 0 deletions

View File

@@ -0,0 +1,722 @@
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 ROLE_COLORS: Record<UserRole, string> = {
dev: 'var(--color-green)',
com: 'var(--color-amber)',
user: 'var(--color-text-muted)',
};
const EVENT_TYPE_LABELS: Record<EventType, string> = {
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 (
<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',
}}
>
{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 ? 'pointer' : 'default',
opacity: isEnded || !poll.isActive ? 0.7 : 1,
}}
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',
}}
>
<span>
{totalVotes} total votes
{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>
</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.25rem',
}}
>
{/* Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '1rem',
marginBottom: '0.75rem',
flexWrap: 'wrap',
}}
>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.4rem' }}>
<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.15rem 0.4rem',
letterSpacing: '0.08em',
}}
>
{EVENT_TYPE_LABELS[event.type]}
</span>
{event.isPublic && (
<span
style={{
fontFamily: 'var(--font-mono)',
background: 'rgba(34,197,94,0.1)',
border: '1px solid rgba(34,197,94,0.25)',
color: 'var(--color-green)',
fontSize: '0.6rem',
padding: '0.15rem 0.4rem',
letterSpacing: '0.08em',
}}
>
PUBLIC
</span>
)}
</div>
<h3
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-text)',
fontSize: '1.1rem',
marginBottom: '0.25rem',
}}
>
{event.title}
</h3>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.68rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
}}
>
<span style={{ color: ROLE_COLORS[event.authorRole] }}>
{event.authorName}
</span>
<span></span>
<span>{formatDateTime(event.createdAt)}</span>
</div>
</div>
</div>
{/* Content */}
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-dim)',
fontSize: '0.85rem',
lineHeight: 1.75,
whiteSpace: 'pre-wrap',
}}
>
{event.content}
</div>
{/* Poll if exists */}
{poll && <PollCard poll={poll} onVote={onVote} />}
</div>
);
}
// ── Main Component ─────────────────────────────────────────────────────────────
export default function IntranetEvents() {
const { user } = useAuth();
const [events, setEvents] = useState<EventPost[]>(MOCK_EVENTS);
const [polls, setPolls] = useState<Poll[]>(MOCK_POLLS);
const [showCreateForm, setShowCreateForm] = useState(false);
// Form state
const [eventType, setEventType] = useState<EventType>('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<string[]>(['', '']);
const [error, setError] = useState('');
const [posting, setPosting] = useState(false);
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]
);
const handleSubmit = useCallback(async () => {
// Validation
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);
await new Promise((r) => setTimeout(r, 300));
const newEventId = `evt${Date.now()}`;
let newPollId: string | undefined;
// Create poll if needed
if (createPoll) {
newPollId = `poll${Date.now()}`;
const validOptions = pollOptions.filter((opt) => opt.trim());
const newPoll: Poll = {
id: newPollId,
eventId: newEventId,
question: pollQuestion.trim(),
options: validOptions.map((opt, idx) => ({
id: `opt${Date.now()}_${idx}`,
text: opt.trim(),
votes: 0,
votedUserIds: [],
})),
isActive: true,
allowMultipleVotes: false,
createdAt: new Date().toISOString(),
};
setPolls((prev) => [newPoll, ...prev]);
}
// Create event
const newEvent: EventPost = {
id: newEventId,
type: createPoll ? 'poll' : eventType,
title: title.trim(),
content: content.trim(),
authorId: user.id,
authorName: user.username,
authorRole: user.role,
createdAt: new Date().toISOString(),
isPublic,
pollId: newPollId,
};
setEvents((prev) => [newEvent, ...prev]);
// Reset form
setTitle('');
setContent('');
setEventType('announcement');
setIsPublic(true);
setCreatePoll(false);
setPollQuestion('');
setPollOptions(['', '']);
setPosting(false);
setShowCreateForm(false);
}, [title, content, eventType, isPublic, createPoll, pollQuestion, pollOptions, user]);
return (
<div style={{ maxWidth: '800px' }}>
<div style={{ marginBottom: '2rem' }}>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.7rem',
letterSpacing: '0.15em',
marginBottom: '0.5rem',
}}
>
INTRANET / EVENTS
</div>
<h1
style={{
fontFamily: 'var(--font-heading)',
color: 'var(--color-text)',
fontSize: '1.8rem',
}}
>
COMMUNITY EVENTS
</h1>
<p
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-text-muted)',
fontSize: '0.78rem',
marginTop: '0.4rem',
}}
>
Post game development updates, announcements, and community polls. Public events are
visible to all users.
</p>
</div>
{/* Create Event Button */}
{!showCreateForm && (
<button
className="btn-terminal btn-amber"
onClick={() => setShowCreateForm(true)}
style={{ marginBottom: '1.5rem' }}
>
+ Create New Event
</button>
)}
{/* Create Event Form */}
{showCreateForm && (
<div
style={{
background: 'var(--color-surface)',
border: '1px solid var(--color-border)',
padding: '1.25rem',
marginBottom: '1.5rem',
}}
>
<div
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--color-amber)',
fontSize: '0.7rem',
letterSpacing: '0.1em',
marginBottom: '1rem',
}}
>
CREATE NEW EVENT
</div>
{/* Event Type */}
<div style={{ marginBottom: '1rem' }}>
<label
style={{
display: 'block',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
marginBottom: '0.4rem',
}}
>
EVENT TYPE
</label>
<select
className="input-terminal"
value={eventType}
onChange={(e) => setEventType(e.target.value as EventType)}
style={{ fontSize: '0.8rem' }}
disabled={createPoll}
>
<option value="announcement">Announcement</option>
<option value="update">Development Update</option>
<option value="milestone">Milestone</option>
</select>
</div>
{/* Title */}
<div style={{ marginBottom: '1rem' }}>
<label
style={{
display: 'block',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
marginBottom: '0.4rem',
}}
>
TITLE
</label>
<input
type="text"
className="input-terminal"
placeholder="Event title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
style={{ fontSize: '0.85rem' }}
/>
</div>
{/* Content */}
<div style={{ marginBottom: '1rem' }}>
<label
style={{
display: 'block',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
marginBottom: '0.4rem',
}}
>
CONTENT
</label>
<textarea
className="input-terminal"
rows={4}
placeholder="Event description and details..."
value={content}
onChange={(e) => setContent(e.target.value)}
style={{ resize: 'vertical', fontSize: '0.85rem' }}
/>
</div>
{/* Public Toggle */}
<div style={{ marginBottom: '1rem' }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontFamily: 'var(--font-mono)',
fontSize: '0.75rem',
color: 'var(--color-text-dim)',
cursor: 'pointer',
}}
>
<input
type="checkbox"
checked={isPublic}
onChange={(e) => setIsPublic(e.target.checked)}
style={{ cursor: 'pointer' }}
/>
Make event visible to public
</label>
</div>
{/* Poll Toggle */}
<div style={{ marginBottom: '1rem' }}>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontFamily: 'var(--font-mono)',
fontSize: '0.75rem',
color: 'var(--color-text-dim)',
cursor: 'pointer',
}}
>
<input
type="checkbox"
checked={createPoll}
onChange={(e) => setCreatePoll(e.target.checked)}
style={{ cursor: 'pointer' }}
/>
Include a community poll
</label>
</div>
{/* Poll Form */}
{createPoll && (
<div
style={{
background: 'var(--color-bg-alt)',
border: '1px solid var(--color-border)',
padding: '1rem',
marginBottom: '1rem',
}}
>
<div
style={{
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-amber)',
letterSpacing: '0.1em',
marginBottom: '0.75rem',
}}
>
POLL DETAILS
</div>
{/* Poll Question */}
<div style={{ marginBottom: '0.75rem' }}>
<label
style={{
display: 'block',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
marginBottom: '0.4rem',
}}
>
QUESTION
</label>
<input
type="text"
className="input-terminal"
placeholder="What do you want to ask?"
value={pollQuestion}
onChange={(e) => setPollQuestion(e.target.value)}
style={{ fontSize: '0.8rem' }}
/>
</div>
{/* Poll Options */}
<div style={{ marginBottom: '0.5rem' }}>
<label
style={{
display: 'block',
fontFamily: 'var(--font-mono)',
fontSize: '0.7rem',
color: 'var(--color-text-muted)',
marginBottom: '0.4rem',
}}
>
OPTIONS
</label>
{pollOptions.map((option, idx) => (
<div key={idx} style={{ marginBottom: '0.4rem', display: 'flex', gap: '0.5rem' }}>
<input
type="text"
className="input-terminal"
placeholder={`Option ${idx + 1}`}
value={option}
onChange={(e) => {
const newOptions = [...pollOptions];
newOptions[idx] = e.target.value;
setPollOptions(newOptions);
}}
style={{ fontSize: '0.8rem', flex: 1 }}
/>
{pollOptions.length > 2 && (
<button
className="btn-terminal"
onClick={() => {
setPollOptions(pollOptions.filter((_, i) => i !== idx));
}}
style={{ padding: '0.4rem 0.6rem', fontSize: '0.7rem' }}
>
×
</button>
)}
</div>
))}
{pollOptions.length < 6 && (
<button
className="btn-terminal"
onClick={() => setPollOptions([...pollOptions, ''])}
style={{ fontSize: '0.7rem', marginTop: '0.4rem' }}
>
+ Add Option
</button>
)}
</div>
</div>
)}
{/* Error */}
{error && (
<div
style={{
color: 'var(--color-red)',
fontFamily: 'var(--font-mono)',
fontSize: '0.72rem',
marginBottom: '0.75rem',
}}
>
{error}
</div>
)}
{/* Actions */}
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
className="btn-terminal btn-amber"
onClick={handleSubmit}
disabled={posting}
style={{ opacity: posting ? 0.6 : 1 }}
>
{posting ? 'Creating...' : '> Create Event'}
</button>
<button
className="btn-terminal"
onClick={() => {
setShowCreateForm(false);
setError('');
setTitle('');
setContent('');
setCreatePoll(false);
setPollQuestion('');
setPollOptions(['', '']);
}}
disabled={posting}
>
Cancel
</button>
</div>
</div>
)}
{/* Events List */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{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>
);
}