feat: add theme switcher and refactor conference components to use new Room interface
This commit is contained in:
parent
bc8216fbe2
commit
b4d054de36
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
27
src/app/components/Conference/Conference.d.ts
vendored
27
src/app/components/Conference/Conference.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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">
|
||||
|
23
src/app/components/ThemeSwitcher/ThemeSwitcher.tsx
Normal file
23
src/app/components/ThemeSwitcher/ThemeSwitcher.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -7,7 +7,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</body>
|
||||
|
145
src/app/page.tsx
145
src/app/page.tsx
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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",
|
||||
|
26
yarn.lock
26
yarn.lock
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user