feat: add theme switcher and refactor conference components to use new Room interface

This commit is contained in:
Rémi 2025-01-02 18:34:08 +01:00
parent bc8216fbe2
commit b4d054de36
11 changed files with 232 additions and 112 deletions

View File

@ -3,9 +3,9 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack -p 4000",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 4000",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
@ -20,6 +20,7 @@
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.0.3", "next": "15.0.3",
"next-auth": "^4.24.10", "next-auth": "^4.24.10",
"next-themes": "^0.4.4",
"react": "19.0.0-rc-66855b96-20241106", "react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106",
"zustand": "^5.0.2" "zustand": "^5.0.2"

View File

@ -2,6 +2,7 @@
import { NextUIProvider } from "@nextui-org/react"; import { NextUIProvider } from "@nextui-org/react";
import { SessionProvider } from "next-auth/react"; import { SessionProvider } from "next-auth/react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export default function AppWrapper({ export default function AppWrapper({
children, children,
@ -10,7 +11,11 @@ export default function AppWrapper({
}) { }) {
return ( return (
<SessionProvider> <SessionProvider>
<NextUIProvider>{children}</NextUIProvider> <NextUIProvider>
<NextThemesProvider attribute="class" defaultTheme="dark">
{children}
</NextThemesProvider>
</NextUIProvider>
</SessionProvider> </SessionProvider>
); );
} }

View File

@ -3,6 +3,7 @@
import { parseDate, Time } from "@internationalized/date"; import { parseDate, Time } from "@internationalized/date";
import { import {
Button,
Card, Card,
CardBody, CardBody,
CardHeader, CardHeader,
@ -10,49 +11,64 @@ import {
Divider, Divider,
TimeInput, TimeInput,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { Conference } from "./Conference"; import { Room } from "./Conference";
import moment from "moment";
import { useRouter } from "next/navigation";
export const ConferenceCard = ({ export const ConferenceCard = ({
id, id,
title, name,
author,
date, date,
hours, Times,
}: Conference) => { Presentator
}: Room) => {
const router = useRouter();
return ( return (
<Card className="max-w-[400px]"> <Card className="max-w-[600px]">
<CardHeader> <CardHeader>
<div className="flex flex-col"> <div className="flex flex-col">
<p className="text-md">{title}</p> <p className="text-md">{name}</p>
<p className="text-small text-default-500">{author}</p> <p className="text-small text-default-500">{Presentator.username}</p>
</div> </div>
</CardHeader> </CardHeader>
<Divider /> <Divider />
<CardBody className="gap-2"> <CardBody>
<DateInput isReadOnly label="Date" value={parseDate(date)} /> {Times.map((time) => (
{hours.map((hour, hourIndex) => ( <div className="flex flex-col gap-2" key={`${time.id}`}>
<div <DateInput isReadOnly label="Date" value={parseDate(moment(date).format("YYYY-MM-DD"))} />
className="flex items-center gap-2" <div className="flex items-center gap-2">
key={`${id}-${hourIndex}`} <TimeInput
> isReadOnly
<TimeInput label="Start"
isReadOnly hourCycle={24}
label="Start" value={
hourCycle={24} new Time(moment(time.startTime).hours(), moment(time.startTime).minutes())
value={ }
new Time(hour.start.hours, hour.start.minutes) />
} <span>-</span>
/> <TimeInput
<span>-</span> isReadOnly
<TimeInput label="End"
isReadOnly hourCycle={24}
label="End" value={new Time(moment(time.endTime).hours(), moment(time.endTime).minutes())}
hourCycle={24} />
value={new Time(hour.end.hours, hour.end.minutes)} </div>
/>
</div> </div>
))} ))}
</CardBody> </CardBody>
<div className="flex p-2">
<Button
className={"bg-transparent text-foreground border-default-200"}
color="primary"
radius="full"
size="sm"
variant={"bordered"}
onPress={() => {
router.push(`/room/${id}`);
}}
>Join</Button>
</div>
</Card> </Card>
); );
}; };

View File

@ -1,16 +1,17 @@
export interface Conference { export interface Room {
id: string; id: string;
title: string; name: string;
author: string;
date: string; date: string;
hours: { Times: {
start: { id: number;
hours: number; startTime: string;
minutes: number; endTime: string;
}; roomId: string;
end: {
hours: number;
minutes: number;
};
}[]; }[];
} Presentator: {
id: string;
username: string;
role: string;
createdAt: string;
}
}

View File

@ -1,21 +1,22 @@
"use client"
import { ConferenceCard } from "./Card"; import { ConferenceCard } from "./Card";
import { Conference } from "./Conference"; import { Room } from "./Conference";
export const ConferenceList = ({ export const ConferenceList = ({
conferences, rooms,
}: { }: {
conferences: Conference[]; rooms: Room[];
}) => { }) => {
return ( return (
<div className="flex gap-2"> <div className="flex gap-2">
{conferences.map((conference) => ( {rooms.map((room) => (
<ConferenceCard <ConferenceCard
key={conference.id} key={room.id}
id={conference.id} id={room.id}
title={conference.title} name={room.name}
author={conference.author} date={room.date}
date={conference.date} Presentator={room.Presentator}
hours={conference.hours} Times={room.Times}
/> />
))} ))}
</div> </div>

View File

@ -10,6 +10,7 @@ import {
NavbarContent, NavbarContent,
} from "@nextui-org/react"; } from "@nextui-org/react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
export const Header = () => { export const Header = () => {
const { data: session } = useSession(); const { data: session } = useSession();
@ -18,6 +19,7 @@ export const Header = () => {
<Navbar className="mb-2"> <Navbar className="mb-2">
<NavbarBrand> <NavbarBrand>
<p className="font-bold text-inherit">Toogether</p> <p className="font-bold text-inherit">Toogether</p>
<ThemeSwitcher />
</NavbarBrand> </NavbarBrand>
<NavbarContent as="div" justify="end"> <NavbarContent as="div" justify="end">
<Dropdown placement="bottom-end"> <Dropdown placement="bottom-end">

View File

@ -0,0 +1,23 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export const ThemeSwitcher = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return (
<div className={`fixed bottom-4 right-4 rounded-lg ${theme === "light" ? "bg-black" : "bg-white"} p-2`}>
{theme === "light" ? (
<button onClick={() => setTheme("dark")}>🌙</button>) : (
<button onClick={() => setTheme("light")}></button>
)}
</div>
);
};

View File

@ -7,7 +7,7 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="fr"> <html lang="fr" suppressHydrationWarning>
<body> <body>
<AppWrapper>{children}</AppWrapper> <AppWrapper>{children}</AppWrapper>
</body> </body>

View File

@ -1,54 +1,115 @@
"use client";
import { Card, Divider, Skeleton } from "@nextui-org/react";
import moment from "moment";
import { useEffect, useState } from "react";
import { Room } from "./components/Conference/Conference";
import { ConferenceList } from "./components/Conference/List"; import { ConferenceList } from "./components/Conference/List";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { axiosInstance } from "./lib/axios";
const RandomConferenceData = [ const SkeletonCard = () => {
{ return <Card className="w-[200px] space-y-5 p-4" radius="lg">
id: "1", <div className="flex flex-col gap-2">
title: "Conference sur les conférences", <Skeleton className="w-4/5 rounded-lg">
author: "Rémi Formentel", <div className="h-3 w-4/5 rounded-lg bg-default-200" />
date: "2021-09-01", </Skeleton>
hours: [ <Skeleton className="w-2/5 rounded-lg">
{ <div className="h-3 w-2/5 rounded-lg bg-default-300" />
start: { hours: 9, minutes: 0 }, </Skeleton>
end: { hours: 12, minutes: 0 }, </div>
}, <Divider />
{ <Skeleton className="rounded-lg">
start: { hours: 13, minutes: 0 }, <div className="h-12 rounded-lg bg-default-300" />
end: { hours: 15, minutes: 0 }, </Skeleton>
}, <div className="flex items-center gap-2">
], <Skeleton className="rounded-lg w-1/2">
}, <div className="h-10 rounded-lg bg-default-300" />
{ </Skeleton>
id: "2", <span>-</span>
title: "Apprendre a dev pour les nuls", <Skeleton className="rounded-lg w-1/2">
author: "Fabien Taupin", <div className="h-10 rounded-lg bg-default-300" />
date: "2021-09-01", </Skeleton>
hours: [ </div>
{ </Card>
start: { hours: 18, minutes: 0 }, }
end: { hours: 23, minutes: 0 },
},
],
},
];
const HomePage = () => { const HomePage = () => {
const [roomsLoading, setRoomsLoading] = useState(true);
const [rooms, setRooms] = useState<{
future: Room[];
actual: Room[];
past: Room[];
}>({
future: [],
actual: [],
past: []
});
useEffect(() => {
axiosInstance.get<{ id: string, name: string, createdAt: string }[]>("/@me/class")
.then((classResponse) => {
classResponse.data.length > 0 &&
axiosInstance.get<Room[]>(`/@me/class/${classResponse.data[0].id}/rooms`)
.then(classes => {
// Filter rooms by date, get future, actual and past rooms
const future = classes.data.filter(room => moment(room.date).isAfter(moment(), "day"));
const actual = classes.data.filter(room => moment(room.date).isSame(moment(), "day"));
const past = classes.data.filter(room => moment(room.date).isBefore(moment()));
setRooms({ future, actual, past });
setRoomsLoading(false);
});
})
}, []);
return ( return (
<> <>
<Header /> <Header />
<main className="flex flex-col gap-8 p-4"> <main className="flex flex-col gap-8 p-4">
<section className="flex flex-col gap-2"> {roomsLoading
<h2 className="font-semibold text-lg">Cours a venir</h2> ? <>
<ConferenceList conferences={RandomConferenceData} /> <section className="flex flex-col gap-2">
</section> <h2 className="font-semibold text-lg">Cours a venir</h2>
<section className="flex flex-col gap-2"> <div className="flex gap-4">
<h2 className="font-semibold text-lg">Cours passés</h2> <SkeletonCard />
<ConferenceList conferences={RandomConferenceData} /> <SkeletonCard />
</section> <SkeletonCard />
<section className="flex flex-col gap-2"> </div>
<h2 className="font-semibold text-lg">Autres cours</h2> </section>
<ConferenceList conferences={RandomConferenceData} /> <section className="flex flex-col gap-2">
</section> <h2 className="font-semibold text-lg">Cours actuels</h2>
<div className="flex gap-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</section>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours passés</h2>
<div className="flex gap-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
</div>
</section>
</>
: <>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours a venir</h2>
<ConferenceList rooms={rooms.future} />
</section>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours actuels</h2>
<ConferenceList rooms={rooms.actual} />
</section>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours passés</h2>
<ConferenceList rooms={rooms.past} />
</section>
</>
}
</main> </main>
</> </>
); );

View File

@ -14,16 +14,6 @@ export default {
background: "var(--background)", background: "var(--background)",
foreground: "var(--foreground)", foreground: "var(--foreground)",
}, },
animation: {
"gradient-x": "gradient-x 5s ease infinite",
},
keyframes: {
"gradient-x": {
"0%": { "background-position": "0% 50%" },
"50%": { "background-position": "100% 50%" },
"100%": { "background-position": "0% 50%" },
},
},
}, },
}, },
darkMode: "class", darkMode: "class",

View File

@ -4148,6 +4148,11 @@ next-auth@^4.24.10:
preact-render-to-string "^5.1.19" preact-render-to-string "^5.1.19"
uuid "^8.3.2" uuid "^8.3.2"
next-themes@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.4.4.tgz#ce6f68a4af543821bbc4755b59c0d3ced55c2d13"
integrity sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==
next@15.0.3: next@15.0.3:
version "15.0.3" version "15.0.3"
resolved "https://registry.yarnpkg.com/next/-/next-15.0.3.tgz#804f5b772e7570ef1f088542a59860914d3288e9" resolved "https://registry.yarnpkg.com/next/-/next-15.0.3.tgz#804f5b772e7570ef1f088542a59860914d3288e9"
@ -4730,8 +4735,16 @@ streamsearch@^1.1.0:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: "string-width-cjs@npm:string-width@^4.2.0":
name string-width-cjs version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -4812,7 +4825,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1" define-properties "^1.2.1"
es-object-atoms "^1.0.0" es-object-atoms "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: "strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==