diff --git a/.env.dist b/.env.dist
index 161ea29..74fd130 100644
--- a/.env.dist
+++ b/.env.dist
@@ -15,3 +15,6 @@ MAILHOG_WEB_PORT=8025
# phpMyAdmin Configuration
PHPMYADMIN_PORT=8080
+
+# .env.local
+DATABASE_URL="postgresql://goofyuser:goofypassword@localhost:5432/goofytrack"
diff --git a/app/components/header.tsx b/app/components/header.tsx
index 7c774ba..52246a4 100644
--- a/app/components/header.tsx
+++ b/app/components/header.tsx
@@ -1,5 +1,44 @@
import { Button } from '@/components/ui/button';
import { signOut, useSession } from 'next-auth/react';
+import { Moon, Sun } from 'lucide-react';
+import { useEffect, useState } from 'react';
+
+function DarkModeButton() {
+ const [isDark, setIsDark] = useState(false);
+
+ useEffect(() => {
+ // On mount, check localStorage or system preference
+ const stored = localStorage.getItem('theme');
+ if (
+ stored === 'dark' ||
+ (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)
+ ) {
+ document.documentElement.classList.add('dark');
+ setIsDark(true);
+ } else {
+ document.documentElement.classList.remove('dark');
+ setIsDark(false);
+ }
+ }, []);
+
+ const toggleDark = () => {
+ const next = !isDark;
+ setIsDark(next);
+ if (next) {
+ document.documentElement.classList.add('dark');
+ localStorage.setItem('theme', 'dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ localStorage.setItem('theme', 'light');
+ }
+ };
+
+ return (
+
+ );
+}
export default function Header() {
const { data: session, status } = useSession();
@@ -11,7 +50,8 @@ export default function Header() {
return (
Goofy Talk
-
+
+
{status === 'unauthenticated' ? (
-
-
-
- Changer le statut
-
-
-
- onChangeTalkStatus(talk.id, 'accepted')}>
- Accepter
-
- onChangeTalkStatus(talk.id, 'refused')}>
- Refuser
-
-
-
+ {/* Bouton pour ouvrir la modale de changement de statut - sans condition */}
+
handleChangeStatus(talk)}
+ >
+ Changer le statut
+
))}
@@ -177,6 +210,14 @@ export default function PendingTalksList({
talk={talkToDelete}
onConfirm={confirmDeleteTalk}
/>
+
+ {/* Dialog for status change */}
+
);
}
diff --git a/app/components/talks/PlanningOverview.tsx b/app/components/talks/PlanningOverview.tsx
index 7c4d124..2d5f02d 100644
--- a/app/components/talks/PlanningOverview.tsx
+++ b/app/components/talks/PlanningOverview.tsx
@@ -1,91 +1,83 @@
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Room, ScheduledTalk } from '@/lib/types';
+'use client';
+
+import { useEffect, useState } from 'react';
+import type { Talk } from '@/lib/types';
+
+interface Room {
+ roomId: number;
+ name: string;
+}
+
+interface ScheduledSlot {
+ id: number;
+ roomId: number;
+ startTime: string;
+ endTime: string;
+ talk: Talk;
+}
interface PlanningOverviewProps {
rooms: Room[];
- scheduledTalks: ScheduledTalk[];
+ date: Date;
}
-export default function PlanningOverview({ rooms, scheduledTalks }: PlanningOverviewProps) {
- const timeSlots = ['9:00', '10:15', '11:30', '13:30', '14:45', '16:00'];
+export default function PlanningOverview({ rooms, date }: PlanningOverviewProps) {
+ const [scheduledSlots, setScheduledSlots] = useState
([]);
+ const hours = Array.from({ length: 9 }, (_, i) => 9 + i); // 09–10 … 17–18
- return (
-
-
- Vue d'ensemble du planning
-
- Visualisez tous les talks programmés par jour et par salle
-
-
-
-
-
-
-
- | Salle / Heure |
- {timeSlots.map((time, index) => {
- let endTime;
- switch (index) {
- case 0:
- endTime = '10:00';
- break;
- case 1:
- endTime = '11:15';
- break;
- case 2:
- endTime = '12:30';
- break;
- case 3:
- endTime = '14:30';
- break;
- case 4:
- endTime = '15:45';
- break;
- default:
- endTime = '17:00';
- }
- return (
-
- {time} - {endTime}
- |
- );
- })}
-
-
-
- {rooms.map((room) => (
-
- | {room.name} |
- {timeSlots.map((time, index) => {
- // Trouver un talk programmé pour cette salle à ce créneau
- const startTime = index === 0 ? '09:00' : time;
- const scheduledTalk = scheduledTalks.find(
- (st) => st.room.id === room.id && st.slot.startTime === startTime,
- );
+ useEffect(() => {
+ const dateParam = date.toISOString().slice(0, 10); // "YYYY-MM-DD"
+ fetch(`/api/schedules?date=${dateParam}`)
+ .then((res) => {
+ if (!res.ok) throw new Error('Impossible de charger le planning');
+ return res.json();
+ })
+ .then((data: { schedules: ScheduledSlot[] }) => {
+ // console.log('schedules from API:', data.schedules);
+ setScheduledSlots(data.schedules);
+ })
+ .catch((err) => {
+ console.error(err);
+ alert(err.message);
+ });
+ }, [date]);
- return (
-
- {scheduledTalk ? (
-
- {scheduledTalk.talk.title}
-
- {scheduledTalk.talk.durationMinutes} min
-
-
- ) : (
-
- Disponible
-
- )}
- |
- );
- })}
-
- ))}
-
-
-
-
-
+ return (
+
+
+
+
+ | Salle / Heure |
+ {hours.map((h) => (
+
+ {`${h}:00–${h + 1}:00`}
+ |
+ ))}
+
+
+
+ {rooms.map((room) => (
+
+ | {room.name} |
+ {hours.map((h) => {
+ const slot = scheduledSlots.find(
+ (s) => s.roomId === room.roomId && new Date(s.startTime).getHours() === h,
+ );
+ return (
+
+ {slot ? (
+
+ {slot.talk.title}
+ Speaker #{slot.talk.speakerId}
+
+ ) : null}
+ |
+ );
+ })}
+
+ ))}
+
+
+
);
}
diff --git a/app/components/talks/StatusBadge.tsx b/app/components/talks/StatusBadge.tsx
index 6c8423a..5ff8e20 100644
--- a/app/components/talks/StatusBadge.tsx
+++ b/app/components/talks/StatusBadge.tsx
@@ -19,25 +19,25 @@ export default function StatusBadge({ status }: StatusBadgeProps) {
const statusConfig: Record = {
pending: {
variant: 'outline',
- className: 'bg-yellow-100',
+ className: 'bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-100',
icon: ,
label: 'En attente',
},
accepted: {
variant: 'outline',
- className: 'bg-blue-100',
+ className: 'bg-blue-100 dark:bg-blue-900 dark:text-blue-100',
icon: ,
label: 'Accepté',
},
- refused: {
+ rejected: {
variant: 'outline',
- className: 'bg-red-100',
+ className: 'bg-red-100 dark:bg-red-900 dark:text-red-100',
icon: ,
label: 'Refusé',
},
scheduled: {
variant: 'outline',
- className: 'bg-green-100',
+ className: 'bg-green-100 dark:bg-green-900 dark:text-green-100',
icon: ,
label: 'Programmé',
},
diff --git a/app/components/talks/StatusDialog.tsx b/app/components/talks/StatusDialog.tsx
new file mode 100644
index 0000000..45959fd
--- /dev/null
+++ b/app/components/talks/StatusDialog.tsx
@@ -0,0 +1,87 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import { Talk, TalkStatus } from '@/lib/types';
+import { cn } from '@/lib/utils';
+import { Check, X } from 'lucide-react';
+import { useState } from 'react';
+
+interface StatusDialogProps {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+ talk: Talk | null;
+ onChangeTalkStatus: (talkId: string, newStatus: TalkStatus) => void;
+}
+
+export default function StatusDialog({
+ isOpen,
+ setIsOpen,
+ talk,
+ onChangeTalkStatus,
+}: StatusDialogProps) {
+ // const [selectedStartDate, setSelectedStartDate] = useState(undefined);
+ // const [selectedEndDate, setSelectedEndDate] = useState(undefined);
+ // const [selectedRoom, setSelectedRoom] = useState('');
+ const [selectedStatus, setSelectedStatus] = useState('pending');
+
+ const handleStatusChange = (status: TalkStatus) => {
+ setSelectedStatus(status);
+ };
+
+ const handleSave = () => {
+ if (talk) {
+ onChangeTalkStatus(talk.id, selectedStatus);
+ setIsOpen(false);
+ // Reset state
+ // setSelectedStartDate(undefined);
+ // setSelectedEndDate(undefined);
+ // setSelectedRoom('');
+ setSelectedStatus('pending');
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/app/components/talks/TalkDialog.tsx b/app/components/talks/TalkDialog.tsx
index 2a49ab2..bed465f 100644
--- a/app/components/talks/TalkDialog.tsx
+++ b/app/components/talks/TalkDialog.tsx
@@ -1,7 +1,6 @@
-// components/talks/TalkDialog.tsx
'use client';
-import { useState, useEffect, FormEvent } from 'react';
+import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@@ -10,9 +9,7 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
-import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
-import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import {
Select,
@@ -21,40 +18,84 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
-import { topics, levels, durations, emptyTalk } from '@/lib/mock-data';
+import { Textarea } from '@/components/ui/textarea';
+import { durations, emptyTalk, levels } from '@/lib/mock-data';
import { Talk, TalkLevel } from '@/lib/types';
import { useSession } from 'next-auth/react';
+import { FormEvent, useEffect, useState } from 'react';
+
+// form subjects (must match your DB subjects.name)
+export const subjects = [
+ 'JavaScript',
+ 'TypeScript',
+ 'React',
+ 'Next.js',
+ 'Node.js',
+ 'Prisma',
+ 'GraphQL',
+ 'DevOps',
+ 'Architecture',
+ 'UX/UI',
+ 'Mobile',
+ 'Security',
+ 'Testing',
+ 'Performance',
+ 'Accessibility',
+];
interface TalkDialogProps {
isOpen: boolean;
- setIsOpen: (isOpen: boolean) => void;
+ setIsOpen: (open: boolean) => void;
talk: Talk | null;
isNew: boolean;
- onSave: (talk: Omit & { id?: string }) => void;
+ onSave: (talk: Talk) => void;
}
export default function TalkDialog({ isOpen, setIsOpen, talk, isNew, onSave }: TalkDialogProps) {
- const session = useSession();
- const [currentTalk, setCurrentTalk] = useState & { id?: string }>(emptyTalk);
+ const { data: session } = useSession();
+ const [currentTalk, setCurrentTalk] = useState>(emptyTalk);
useEffect(() => {
- if (isOpen) {
- if (!session.data?.user.id) throw new Error('User ID is required');
+ if (!isOpen) return;
+ if (!session?.user?.id) throw new Error('User ID is required');
+ setCurrentTalk(isNew ? { ...emptyTalk, speakerId: session.user.id } : { ...(talk as Talk) });
+ }, [isOpen, isNew, talk, session]);
- setCurrentTalk(isNew ? { ...emptyTalk, speakerId: session.data.user.id } : { ...talk! });
- }
- }, [isOpen, isNew, talk]);
-
- const handleInputChange = (field: keyof Talk, value: string | number | TalkLevel) => {
- setCurrentTalk({
- ...currentTalk,
- [field]: value,
- });
+ const handleInputChange = (field: keyof Omit, value: string | number | TalkLevel) => {
+ setCurrentTalk({ ...currentTalk, [field]: value });
};
- const handleSubmit = (e: FormEvent) => {
+ const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
- onSave(currentTalk);
+
+ const payload = {
+ title: currentTalk.title,
+ description: currentTalk.description,
+ topic: currentTalk.topic,
+ durationMinutes: currentTalk.durationMinutes,
+ level: currentTalk.level,
+ };
+
+ // Choose POST for new, PUT for existing
+ const url = isNew ? '/api/talks' : `/api/talks/${talk?.id}`;
+ const method = isNew ? 'POST' : 'PUT';
+
+ try {
+ const response = await fetch(url, {
+ method,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ const err = await response.json();
+ throw new Error(err.error || 'Unknown error');
+ }
+ const saved = await response.json();
+ onSave(saved);
+ setIsOpen(false);
+ } catch (err) {
+ console.error('Failed to save talk:', err);
+ }
};
return (
@@ -83,19 +124,19 @@ export default function TalkDialog({ isOpen, setIsOpen, talk, isNew, onSave }: T
-
+