From 6aa653ab4a9e507ef30c22047a98875f7476bc1b Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 18:17:10 +0000 Subject: [PATCH] feat(CC-092): add Event Tracker page (conference/meetup/workshop, days countdown, prep flag) --- src/app/dashboard/events/page.tsx | 214 ++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/app/dashboard/events/page.tsx diff --git a/src/app/dashboard/events/page.tsx b/src/app/dashboard/events/page.tsx new file mode 100644 index 0000000..9b05dbe --- /dev/null +++ b/src/app/dashboard/events/page.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { id: string; title: string; description: string | null; status: string; priority: string | null; tags: string[]; targetDate: string | null; createdAt: string; } + +const EVENT_TAGS = ['eveniment','event','conferinta','conference','workshop','meetup','webinar','summit','hackathon','networking']; +const EVENT_TYPES: Record = { + conferinta:'🎤', conference:'🎤', workshop:'🛠️', meetup:'🤝', + webinar:'💻', summit:'🏔️', hackathon:'⚡', networking:'🌐', + eveniment:'📅', event:'📅', +}; + +function daysUntil(iso: string | null): number | null { + if (!iso) return null; + return Math.ceil((new Date(iso).getTime() - Date.now()) / 86_400_000); +} + +export default function EventsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [tab, setTab] = useState<'upcoming' | 'past'>('upcoming'); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ title: '', type: 'conferinta', date: '', location: '', url: '', prep: false }); + + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ['events', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const events = useMemo(() => + tasks.filter((t) => t.tags.some((tag) => EVENT_TAGS.includes(tag.toLowerCase()))), + [tasks]); + + const upcoming = useMemo(() => + events + .filter((e) => !e.targetDate || new Date(e.targetDate) >= new Date(new Date().toDateString())) + .filter((e) => e.status !== 'cancelled') + .sort((a, b) => (a.targetDate ?? '9999').localeCompare(b.targetDate ?? '9999')), + [events]); + + const past = useMemo(() => + events + .filter((e) => e.targetDate && new Date(e.targetDate) < new Date(new Date().toDateString())) + .sort((a, b) => b.targetDate!.localeCompare(a.targetDate!)), + [events]); + + const urgent = upcoming.filter((e) => { + const d = daysUntil(e.targetDate); + return d !== null && d <= 14; + }); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: form.title, + description: [form.location && `📍 ${form.location}`, form.url && `🔗 ${form.url}`].filter(Boolean).join('\n') || undefined, + tags: ['eveniment', form.type, ...(form.prep ? ['prep-needed'] : [])], + status: 'todo', priority: 'normal', + targetDate: form.date || undefined, + }}), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['events', tenantId] }); + setShowAdd(false); + setForm({ title: '', type: 'conferinta', date: '', location: '', url: '', prep: false }); + }, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['events', tenantId] }), + }); + + function eventType(t: Task): string { + return t.tags.find((tag) => EVENT_TYPES[tag.toLowerCase()]) ?? 'event'; + } + + const displayList = tab === 'upcoming' ? upcoming : past; + + return ( +
+
+
+

Event Tracker

+

{upcoming.length} viitoare · {past.length} trecute

+
+ +
+ + {urgent.length > 0 && ( +
+

⚡ În următoarele 14 zile

+ {urgent.map((e) => { + const d = daysUntil(e.targetDate); + return ( +
+

{EVENT_TYPES[eventType(e)] ?? '📅'} {e.title}

+ + {d === 0 ? 'Azi!' : d === 1 ? 'Mâine' : `${d} zile`} + +
+ ); + })} +
+ )} + + {showAdd && ( +
+

Eveniment nou

+ setForm((p) => ({ ...p, title: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + setForm((p) => ({ ...p, date: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, location: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, url: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm font-mono text-xs text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ +
+ + +
+
+ )} + + {/* Tabs */} +
+ {(['upcoming', 'past'] as const).map((t) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : displayList.length === 0 ? ( +
+

🗓️

+

{tab === 'upcoming' ? 'Niciun eveniment viitor.' : 'Niciun eveniment trecut.'}

+
+ ) : ( +
+ {displayList.map((e) => { + const d = daysUntil(e.targetDate); + const icon = EVENT_TYPES[eventType(e)] ?? '📅'; + const needsPrep = e.tags.includes('prep-needed'); + const [loc, url] = (e.description ?? '').split('\n'); + return ( +
+ {icon} +
+

{e.title}

+
+ {e.targetDate &&

📅 {new Date(e.targetDate).toLocaleDateString('ro-RO', { weekday:'short', day:'numeric', month:'short' })}

} + {loc?.startsWith('📍') &&

{loc}

} + {needsPrep && 🛠️ Prep needed} +
+ {url?.startsWith('🔗') && ( +

{url.replace('🔗 ', '')}

+ )} +
+
+ {d !== null && tab === 'upcoming' && ( +

+ {d === 0 ? 'Azi' : d === 1 ? 'Mâine' : `${d}z`} +

+ )} + {tab === 'past' && e.status !== 'completed' && ( + + )} + {tab === 'past' && e.status === 'completed' && ( + ✓ Participat + )} +
+
+ ); + })} +
+ )} +
+ ); +}