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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import {
NavbarContent,
} from "@nextui-org/react";
import { useSession } from "next-auth/react";
import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher";
export const Header = () => {
const { data: session } = useSession();
@ -18,6 +19,7 @@ export const Header = () => {
<Navbar className="mb-2">
<NavbarBrand>
<p className="font-bold text-inherit">Toogether</p>
<ThemeSwitcher />
</NavbarBrand>
<NavbarContent as="div" justify="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;
}) {
return (
<html lang="fr">
<html lang="fr" suppressHydrationWarning>
<body>
<AppWrapper>{children}</AppWrapper>
</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 { Header } from "./components/Header";
import { axiosInstance } from "./lib/axios";
const RandomConferenceData = [
{
id: "1",
title: "Conference sur les conférences",
author: "Rémi Formentel",
date: "2021-09-01",
hours: [
{
start: { hours: 9, minutes: 0 },
end: { hours: 12, minutes: 0 },
},
{
start: { hours: 13, minutes: 0 },
end: { hours: 15, minutes: 0 },
},
],
},
{
id: "2",
title: "Apprendre a dev pour les nuls",
author: "Fabien Taupin",
date: "2021-09-01",
hours: [
{
start: { hours: 18, minutes: 0 },
end: { hours: 23, minutes: 0 },
},
],
},
];
const SkeletonCard = () => {
return <Card className="w-[200px] space-y-5 p-4" radius="lg">
<div className="flex flex-col gap-2">
<Skeleton className="w-4/5 rounded-lg">
<div className="h-3 w-4/5 rounded-lg bg-default-200" />
</Skeleton>
<Skeleton className="w-2/5 rounded-lg">
<div className="h-3 w-2/5 rounded-lg bg-default-300" />
</Skeleton>
</div>
<Divider />
<Skeleton className="rounded-lg">
<div className="h-12 rounded-lg bg-default-300" />
</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>
<span>-</span>
<Skeleton className="rounded-lg w-1/2">
<div className="h-10 rounded-lg bg-default-300" />
</Skeleton>
</div>
</Card>
}
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 (
<>
<Header />
<main className="flex flex-col gap-8 p-4">
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours a venir</h2>
<ConferenceList conferences={RandomConferenceData} />
</section>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours passés</h2>
<ConferenceList conferences={RandomConferenceData} />
</section>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Autres cours</h2>
<ConferenceList conferences={RandomConferenceData} />
</section>
{roomsLoading
? <>
<section className="flex flex-col gap-2">
<h2 className="font-semibold text-lg">Cours a venir</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 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>
</>
);

View File

@ -14,16 +14,6 @@ export default {
background: "var(--background)",
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",

View File

@ -4148,6 +4148,11 @@ next-auth@^4.24.10:
preact-render-to-string "^5.1.19"
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:
version "15.0.3"
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"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
name string-width-cjs
"string-width-cjs@npm:string-width@^4.2.0":
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"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -4812,7 +4825,14 @@ string.prototype.trimstart@^1.0.8:
define-properties "^1.2.1"
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"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==