feat: add Room component and refactor Conference components, implement custom scrollbar styles and smooth

This commit is contained in:
Rémi 2025-01-03 02:19:20 +01:00
parent 37a34dc004
commit f9aef69cfa
9 changed files with 322 additions and 246 deletions

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true
}

View File

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

View File

@ -1,3 +1,4 @@
'use client'
import { import {
Avatar, Avatar,
Button, Button,
@ -8,85 +9,79 @@ import {
Navbar, Navbar,
NavbarBrand, NavbarBrand,
NavbarContent, NavbarContent,
NavbarItem, NavbarItem
} 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"; import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher'
import { axiosInstance } from "@/app/lib/axios"; import { axiosInstance } from '@/app/lib/axios'
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import { useRouter } from "next/navigation"; import { useRouter } from 'next/navigation'
const getInitials = (name: string) => { const getInitials = (name: string) => {
if (!name) return ""; if (!name) return ''
const nameParts = name.split(" "); const nameParts = name.split(' ')
if (nameParts.length === 1) { if (nameParts.length === 1) {
return name; return name
} }
const firstInitial = nameParts[0]?.[0] || ""; const firstInitial = nameParts[0]?.[0] || ''
const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || ""; const secondInitial = nameParts[1]?.[0] || nameParts[0]?.[1] || ''
return firstInitial + secondInitial; return firstInitial + secondInitial
}; }
export const Header = () => { export const Header = () => {
const { data: session } = useSession(); const { data: session } = useSession()
const router = useRouter(); const router = useRouter()
const [userProfile, setUserProfile] = useState<{ const [userProfile, setUserProfile] = useState<{
id: string; id: string
username: string; username: string
role: "ADMIN" | "STUDENT"; role: 'ADMIN' | 'STUDENT'
}>(); }>()
const initials = session?.user?.name ? getInitials(session.user.name) : ""; const initials = session?.user?.name ? getInitials(session.user.name) : ''
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
return await axiosInstance<{ return await axiosInstance<{
id: string; id: string
username: string; username: string
role: "ADMIN" | "STUDENT"; role: 'ADMIN' | 'STUDENT'
}>("/@me"); }>('/@me')
} }
useEffect(() => { useEffect(() => {
fetchUserProfile().then(r => { fetchUserProfile().then(r => {
setUserProfile(r.data); setUserProfile(r.data)
}); })
}, []); }, [])
return ( return (
<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>
</NavbarBrand> </NavbarBrand>
<NavbarContent as="div" justify="end"> <NavbarContent as='div' justify='end'>
<Dropdown placement="bottom-end"> <Dropdown placement='bottom-end'>
<DropdownTrigger> <DropdownTrigger>
<Avatar <Avatar
isBordered isBordered
as="button" as='button'
className="transition-transform" className='transition-transform'
color="secondary" color='secondary'
name={initials} name={initials}
size="sm" size='sm'
/> />
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu aria-label="Profile Actions" variant="flat"> <DropdownMenu aria-label='Profile Actions' variant='flat'>
<DropdownItem key="profile" className="h-14 gap-2"> <DropdownItem key='profile' className='h-14 gap-2'>
<p>Signed in as</p> <p>Signed in as</p>
<p className="font-semibold"> <p className='font-semibold'>{session?.user?.name}</p>
{session?.user?.name}
</p>
</DropdownItem> </DropdownItem>
<DropdownItem key="settings">Settings</DropdownItem> <DropdownItem key='settings'>Settings</DropdownItem>
<DropdownItem <DropdownItem key='logout' color='danger' href='/auth/logout'>
key="logout"
color="danger"
href="/auth/logout"
>
Logout Logout
</DropdownItem> </DropdownItem>
</DropdownMenu> </DropdownMenu>
@ -94,18 +89,12 @@ export const Header = () => {
<NavbarItem> <NavbarItem>
<ThemeSwitcher /> <ThemeSwitcher />
</NavbarItem> </NavbarItem>
{ {userProfile?.role === 'ADMIN' ? (
userProfile?.role === "ADMIN" ? ( <NavbarItem>
<NavbarItem> <Button onPress={() => router.push('/admin')}>🔧</Button>
<Button </NavbarItem>
onPress={() => router.push("/admin")} ) : null}
>
🔧
</Button>
</NavbarItem>
) : null
}
</NavbarContent> </NavbarContent>
</Navbar> </Navbar>
); )
}; }

View File

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

View File

@ -0,0 +1,57 @@
'use client';
import { useEffect, useRef } from 'react';
import { RoomCard } from './Card';
import { Room } from './Room';
export const RoomList = ({ rooms }: { rooms: Room[] }) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const handleWheel = (event: WheelEvent) => {
if (event.deltaY === 0) return;
event.preventDefault();
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
const scrollAmount = 10;
const direction = event.deltaY > 0 ? 1 : -1;
let scrollCount = 0;
const interval = setInterval(() => {
scrollContainer.scrollLeft += scrollAmount * direction;
scrollCount += scrollAmount;
if (scrollCount >= 100) {
clearInterval(interval);
}
}, 10);
};
useEffect(() => {
const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return;
scrollContainer.addEventListener('wheel', handleWheel);
return () => {
scrollContainer.removeEventListener('wheel', handleWheel);
};
}, []);
return (
<div ref={scrollContainerRef} className='overflow-x-auto scrollbar-hide rounded-xl'>
<ul className='flex gap-2'>
{rooms.map(room => (
<li key={room.id}>
<RoomCard
id={room.id}
name={room.name}
date={room.date}
Presentator={room.Presentator}
Times={room.Times}
/>
</li>
))}
</ul>
</div>
);
};

View File

@ -1,17 +1,17 @@
export interface Room { export interface Room {
id: string; id: string;
name: string; name: string;
date: string; date: string;
Times: { Times: {
id: number; id: number;
startTime: string; startTime: string;
endTime: string; endTime: string;
roomId: string; roomId: string;
}[]; }[];
Presentator: { Presentator: {
id: string; id: string;
username: string; username: string;
role: string; role: string;
createdAt: string; createdAt: string;
} }
} }

View File

@ -1,13 +1,21 @@
"use client"; 'use client'
import { Button } from "@nextui-org/react"; import { Button } from '@nextui-org/react'
import { useTheme } from "next-themes"; import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
export const ThemeSwitcher = () => { export const ThemeSwitcher = () => {
const { theme, setTheme } = useTheme(); const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
return ( return (
<Button onPress={() => setTheme(theme === "light" ? "dark" : "light")}> <Button onPress={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
{theme === "light" ? "🌑" : "☀️"} {theme === 'light' ? '🌑' : '☀️'}
</Button> </Button>
); )
}; }

View File

@ -1,3 +1,19 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer utilities {
.scrollbar::-webkit-scrollbar {
height: 10px;
}
.scrollbar::-webkit-scrollbar-track {
border-radius: 100vh;
background: #5a5a5a54;
}
.scrollbar::-webkit-scrollbar-thumb {
background: #414141;
border-radius: 100vh;
}
}

View File

@ -1,37 +1,39 @@
"use client"; 'use client';
import { Card, Divider, Skeleton } from "@nextui-org/react"; import { Card, Divider, Skeleton } from '@nextui-org/react';
import moment from "moment"; import moment from 'moment';
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react';
import { Room } from "./components/Conference/Conference"; import { Room } from './components/Room/Room';
import { ConferenceList } from "./components/Conference/List"; import { RoomList } from './components/Room/List';
import { Header } from "./components/Header"; import { Header } from './components/Header';
import { axiosInstance } from "./lib/axios"; import { axiosInstance } from './lib/axios';
const SkeletonCard = () => { const SkeletonCard = () => {
return <Card className="w-[200px] space-y-5 p-4" radius="lg"> return (
<div className="flex flex-col gap-2"> <Card className='w-[200px] space-y-5 p-4' radius='lg'>
<Skeleton className="w-4/5 rounded-lg"> <div className='flex flex-col gap-2'>
<div className="h-3 w-4/5 rounded-lg bg-default-200" /> <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> </Skeleton>
<Skeleton className="w-2/5 rounded-lg"> <div className='flex items-center gap-2'>
<div className="h-3 w-2/5 rounded-lg bg-default-300" /> <Skeleton className='rounded-lg w-1/2'>
</Skeleton> <div className='h-10 rounded-lg bg-default-300' />
</div> </Skeleton>
<Divider /> <span>-</span>
<Skeleton className="rounded-lg"> <Skeleton className='rounded-lg w-1/2'>
<div className="h-12 rounded-lg bg-default-300" /> <div className='h-10 rounded-lg bg-default-300' />
</Skeleton> </Skeleton>
<div className="flex items-center gap-2"> </div>
<Skeleton className="rounded-lg w-1/2"> </Card>
<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 HomePage = () => {
const [roomsLoading, setRoomsLoading] = useState(true); const [roomsLoading, setRoomsLoading] = useState(true);
@ -46,70 +48,93 @@ const HomePage = () => {
}); });
useEffect(() => { useEffect(() => {
axiosInstance.get<{ id: string, name: string, createdAt: string }[]>("/@me/class") axiosInstance
.then((classResponse) => { .get<{ id: string; name: string; createdAt: string }[]>(
'/@me/class'
)
.then(classResponse => {
if (classResponse.data.length) if (classResponse.data.length)
axiosInstance.get<Room[]>(`/@me/class/${classResponse.data[0].id}/rooms`) axiosInstance
.get<Room[]>(
`/@me/class/${classResponse.data[0].id}/rooms`
)
.then(classes => { .then(classes => {
// Filter rooms by date, get future, actual and past rooms // Filter rooms by date, get future, actual and past rooms
const future = classes.data.filter(room => moment(room.date).isAfter(moment(), "day")); const future = classes.data.filter(room =>
const actual = classes.data.filter(room => moment(room.date).isSame(moment(), "day")); moment(room.date).isAfter(moment(), 'day')
const past = classes.data.filter(room => moment(room.date).isBefore(moment())); );
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 }); setRooms({ future, actual, past });
setRoomsLoading(false); setRoomsLoading(false);
}); });
}) });
}, []); }, []);
return ( return (
<> <>
<Header /> <Header />
<main className="flex flex-col gap-8 p-4"> <main className='flex flex-col gap-8 p-4'>
{roomsLoading {roomsLoading ? (
? <> <>
<section className="flex flex-col gap-2"> <section className='flex flex-col gap-2'>
<h2 className="font-semibold text-lg">Cours a venir</h2> <h2 className='font-semibold text-lg'>
<div className="flex gap-4"> Cours a venir
</h2>
<div className='flex gap-4'>
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
</div> </div>
</section> </section>
<section className="flex flex-col gap-2"> <section className='flex flex-col gap-2'>
<h2 className="font-semibold text-lg">Cours actuels</h2> <h2 className='font-semibold text-lg'>
<div className="flex gap-4"> Cours actuels
</h2>
<div className='flex gap-4'>
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
</div> </div>
</section> </section>
<section className="flex flex-col gap-2"> <section className='flex flex-col gap-2'>
<h2 className="font-semibold text-lg">Cours passés</h2> <h2 className='font-semibold text-lg'>
<div className="flex gap-4"> Cours passés
</h2>
<div className='flex gap-4'>
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
<SkeletonCard /> <SkeletonCard />
</div> </div>
</section> </section>
</> </>
: <> ) : (
<section className="flex flex-col gap-2"> <>
<h2 className="font-semibold text-lg">Cours a venir</h2> <section className='flex flex-col gap-2'>
<ConferenceList rooms={rooms.future} /> <h2 className='font-semibold text-lg'>
Cours a venir
</h2>
<RoomList rooms={rooms.future} />
</section> </section>
<section className="flex flex-col gap-2"> <section className='flex flex-col gap-2'>
<h2 className="font-semibold text-lg">Cours actuels</h2> <h2 className='font-semibold text-lg'>
<ConferenceList rooms={rooms.actual} /> Cours actuels
</h2>
<RoomList rooms={rooms.actual} />
</section> </section>
<section className="flex flex-col gap-2"> <section className='flex flex-col gap-2'>
<h2 className="font-semibold text-lg">Cours passés</h2> <h2 className='font-semibold text-lg'>
<ConferenceList rooms={rooms.past} /> Cours passés
</h2>
<RoomList rooms={rooms.past} />
</section> </section>
</> </>
)}
}
</main> </main>
</> </>
); );