diff --git a/nest-intra/nginx.conf b/nest-intra/nginx.conf index 6385a07..ad301b9 100644 --- a/nest-intra/nginx.conf +++ b/nest-intra/nginx.conf @@ -3,8 +3,12 @@ server { root /usr/share/nginx/html; index index.html; + # Use Docker's embedded DNS resolver; defer resolution to request time + resolver 127.0.0.11 valid=30s; + location /api/ { - proxy_pass http://api:3000/api/; + set $api_upstream http://api:3000; + proxy_pass $api_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } diff --git a/nest-intra/src/pages/intranet/IntranetEvents.tsx b/nest-intra/src/pages/intranet/IntranetEvents.tsx index 4803822..c17fc3c 100644 --- a/nest-intra/src/pages/intranet/IntranetEvents.tsx +++ b/nest-intra/src/pages/intranet/IntranetEvents.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback } from 'react'; -import { useAuth } from '../../contexts/AuthContext'; +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'; @@ -244,10 +244,27 @@ function EventCard({ // ── 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 [polls, setPolls] = useState([]); const [showCreateForm, setShowCreateForm] = useState(false); // Form state @@ -261,124 +278,78 @@ export default function IntranetEvents() { 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( - (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; - }), - }; - }) - ); + 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 + } }, - [user] + [events, user] ); const handleSubmit = useCallback(async () => { - // Validation - if (!title.trim()) { - setError('Title is required.'); - return; - } - if (!content.trim()) { - setError('Content is required.'); - return; - } + 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; - } + 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 (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(), + try { + const body: Record = { + type: createPoll ? 'poll' : eventType, + title: title.trim(), + content: content.trim(), + isPublic, }; - setPolls((prev) => [newPoll, ...prev]); + + 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); } - - // 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 ( @@ -711,10 +682,9 @@ export default function IntranetEvents() { {/* Events List */}
- {events.map((event) => { - const poll = event.pollId ? polls.find((p) => p.id === event.pollId) : undefined; - return ; - })} + {events.map((event) => ( + + ))}
); diff --git a/nest-intra/src/types/index.ts b/nest-intra/src/types/index.ts index db9ca04..472ee22 100644 --- a/nest-intra/src/types/index.ts +++ b/nest-intra/src/types/index.ts @@ -133,7 +133,8 @@ export interface EventPost { createdAt: string; updatedAt?: string; isPublic: boolean; // whether visible to community - pollId?: string; // reference to poll if type is 'poll' + pollId?: string | null; // reference to poll if type is 'poll' + poll?: Poll | null; // embedded poll data from API } export interface PollOption {