feat: add ProvidersList component for authentication and update LoginPage

This commit is contained in:
Rémi 2025-01-04 18:17:06 +01:00
parent 096a10b9c1
commit 38fa50e0c8
8 changed files with 147 additions and 135 deletions

View File

@ -1,10 +1,11 @@
"use client"; import { Metadata } from "next";
import { authOptions } from "@/authOptions"; import { ProvidersList } from "./providersList";
import { signIn } from "next-auth/react";
const LoginPage = () => { export const metadata: Metadata = {
const provider = authOptions.providers[0]; title: "Toogether | Connexion",
};
export default function LoginPage() {
return ( return (
<div className="flex flex-col h-screen items-center justify-center bg-gradient-to-br from-green-700 to-green-950 bg-[length:200%_200%] animate-gradient-x text-white"> <div className="flex flex-col h-screen items-center justify-center bg-gradient-to-br from-green-700 to-green-950 bg-[length:200%_200%] animate-gradient-x text-white">
<div className="flex flex-col justify-center items-center gap-6 bg-black bg-opacity-40 rounded-md p-5 mx-5 w-2/3"> <div className="flex flex-col justify-center items-center gap-6 bg-black bg-opacity-40 rounded-md p-5 mx-5 w-2/3">
@ -25,19 +26,8 @@ const LoginPage = () => {
<div className="w-full border-t border-white"></div> <div className="w-full border-t border-white"></div>
<h3 className="font-bold text-xl">Via</h3> <h3 className="font-bold text-xl">Via</h3>
<ul> <ProvidersList />
<li key={provider.id}>
<button
className="bg-white text-black p-2 rounded-md"
onClick={() => signIn(provider.id)}
>
{provider.name}
</button>
</li>
</ul>
</div> </div>
</div> </div>
); );
}; }
export default LoginPage;

View File

@ -0,0 +1,22 @@
"use client";
import { authOptions } from "@/authOptions";
import { signIn } from "next-auth/react";
export const ProvidersList = () => {
const providers = authOptions.providers;
return (
<div className="flex gap-2">
{providers.map((provider) => (
<button
key={provider.id}
className="bg-white text-black p-2 rounded-md"
onClick={() => signIn(provider.id)}
>
{provider.name}
</button>
))}
</div>
);
};

View File

@ -1,4 +1,4 @@
'use client'; "use client";
import { import {
Avatar, Avatar,
Button, Button,
@ -9,24 +9,24 @@ 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;
}; };
@ -38,39 +38,39 @@ export const Header = () => {
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">
{userProfile?.role === 'ADMIN' ? ( {userProfile?.role === "ADMIN" ? (
<NavbarItem> <NavbarItem>
<Button <Button
size='sm' size="sm"
variant='flat' variant="flat"
className='min-w-0' className="min-w-0"
onPress={() => router.push('/admin')} onPress={() => router.push("/admin")}
> >
🔧 🔧
</Button> </Button>
@ -80,29 +80,29 @@ export const Header = () => {
<ThemeSwitcher /> <ThemeSwitcher />
</NavbarItem> </NavbarItem>
<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} {session?.user?.name}
</p> </p>
</DropdownItem> </DropdownItem>
<DropdownItem key='settings'>Settings</DropdownItem> <DropdownItem key="settings">Settings</DropdownItem>
<DropdownItem <DropdownItem
key='logout' key="logout"
color='danger' color="danger"
href='/auth/logout' href="/auth/logout"
> >
Logout Logout
</DropdownItem> </DropdownItem>

View File

@ -1,6 +1,6 @@
'use client'; "use client";
import { parseDate, Time } from '@internationalized/date'; import { parseDate, Time } from "@internationalized/date";
import { import {
Button, Button,
@ -9,56 +9,56 @@ import {
CardHeader, CardHeader,
DateInput, DateInput,
Divider, Divider,
TimeInput TimeInput,
} from '@nextui-org/react'; } from "@nextui-org/react";
import { Room } from './Room'; import { Room } from "./Room";
import moment from 'moment'; import moment from "moment";
import { useRouter } from 'next/navigation'; import { useRouter } from "next/navigation";
export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => { export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => {
const router = useRouter(); const router = useRouter();
return ( return (
<Card className='w-[300px]'> <Card className="w-[300px]">
<CardHeader> <CardHeader>
<div className='flex flex-col min-h-20'> <div className="flex flex-col min-h-20">
<p className='text-md'>{name}</p> <p className="text-md">{name}</p>
<p className='text-small text-default-500'> <p className="text-small text-default-500">
{Presentator.username} {Presentator.username}
</p> </p>
</div> </div>
</CardHeader> </CardHeader>
<Divider /> <Divider />
<CardBody> <CardBody>
<div className='flex flex-col gap-2' key={`times.${id}`}> <div className="flex flex-col gap-2" key={`times.${id}`}>
<DateInput <DateInput
isReadOnly isReadOnly
label='Date' label="Date"
value={parseDate(moment(date).format('YYYY-MM-DD'))} value={parseDate(moment(date).format("YYYY-MM-DD"))}
/> />
{Times.map(time => ( {Times.map((time) => (
<div key={`time.${time.id}`}> <div key={`time.${time.id}`}>
<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( new Time(
moment(time.startTime).hours(), moment(time.startTime).hours(),
moment(time.startTime).minutes() moment(time.startTime).minutes(),
) )
} }
/> />
<span>-</span> <span>-</span>
<TimeInput <TimeInput
isReadOnly isReadOnly
label='End' label="End"
hourCycle={24} hourCycle={24}
value={ value={
new Time( new Time(
moment(time.endTime).hours(), moment(time.endTime).hours(),
moment(time.endTime).minutes() moment(time.endTime).minutes(),
) )
} }
/> />
@ -68,13 +68,13 @@ export const RoomCard = ({ id, name, date, Times, Presentator }: Room) => {
</div> </div>
</CardBody> </CardBody>
{moment(date).dayOfYear() === moment().dayOfYear() && ( {moment(date).dayOfYear() === moment().dayOfYear() && (
<div className='flex p-2'> <div className="flex p-2">
<Button <Button
className={''} className={""}
color='primary' color="primary"
radius='full' radius="full"
size='sm' size="sm"
variant={'flat'} variant={"flat"}
onPress={() => { onPress={() => {
router.push(`/room/${id}`); router.push(`/room/${id}`);
}} }}

View File

@ -1,8 +1,8 @@
'use client'; "use client";
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from "react";
import { RoomCard } from './Card'; import { RoomCard } from "./Card";
import { Room } from './Room'; import { Room } from "./Room";
import { SkeletonRoomCard } from './SkeletonRoomCard'; import { SkeletonRoomCard } from "./SkeletonRoomCard";
export const RoomList = ({ rooms }: { rooms: Room[] }) => { export const RoomList = ({ rooms }: { rooms: Room[] }) => {
const scrollContainerRef = useRef<HTMLUListElement>(null); const scrollContainerRef = useRef<HTMLUListElement>(null);
@ -40,20 +40,20 @@ export const RoomList = ({ rooms }: { rooms: Room[] }) => {
const scrollContainer = scrollContainerRef.current; const scrollContainer = scrollContainerRef.current;
if (!scrollContainer) return; if (!scrollContainer) return;
scrollContainer.addEventListener('wheel', handleWheel); scrollContainer.addEventListener("wheel", handleWheel);
return () => { return () => {
scrollContainer.removeEventListener('wheel', handleWheel); scrollContainer.removeEventListener("wheel", handleWheel);
}; };
}, []); }, []);
return ( return (
<ul <ul
ref={scrollContainerRef} ref={scrollContainerRef}
className='flex gap-2 overflow-x-auto scrollbar-hide rounded-xl bg-default-100 p-1' className="flex gap-2 overflow-x-auto scrollbar-hide rounded-xl bg-default-100 p-1"
> >
{rooms.length > 0 ? ( {rooms.length > 0 ? (
rooms.map(room => ( rooms.map((room) => (
<li key={room.id}> <li key={room.id}>
<RoomCard <RoomCard
id={room.id} id={room.id}

View File

@ -1,9 +1,9 @@
'use client'; "use client";
import { axiosInstance } from '@/app/lib/axios'; import { axiosInstance } from "@/app/lib/axios";
import moment from 'moment'; import moment from "moment";
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { RoomList } from './List'; import { RoomList } from "./List";
import { Room } from './Room'; import { Room } from "./Room";
export const RoomTable = () => { export const RoomTable = () => {
const [rooms, setRooms] = useState<{ const [rooms, setRooms] = useState<{
@ -13,30 +13,30 @@ export const RoomTable = () => {
}>({ }>({
future: [], future: [],
actual: [], actual: [],
past: [] past: [],
}); });
useEffect(() => { useEffect(() => {
axiosInstance axiosInstance
.get<{ id: string; name: string; createdAt: string }[]>( .get<
'/@me/class' { id: string; name: string; createdAt: string }[]
) >("/@me/class")
.then(classResponse => { .then((classResponse) => {
if (classResponse.data.length) if (classResponse.data.length)
axiosInstance axiosInstance
.get<Room[]>( .get<
`/@me/class/${classResponse.data[0].id}/rooms` 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 => const future = classes.data.filter((room) =>
moment(room.date).isAfter(moment(), 'day') moment(room.date).isAfter(moment(), "day"),
); );
const actual = classes.data.filter(room => const actual = classes.data.filter((room) =>
moment(room.date).isSame(moment(), 'day') moment(room.date).isSame(moment(), "day"),
); );
const past = classes.data.filter(room => const past = classes.data.filter((room) =>
moment(room.date).isBefore(moment()) moment(room.date).isBefore(moment()),
); );
setRooms({ future, actual, past }); setRooms({ future, actual, past });
}); });
@ -44,17 +44,17 @@ export const RoomTable = () => {
}, []); }, []);
return ( return (
<div className='flex flex-col gap-4'> <div className="flex flex-col gap-4">
<section className='flex flex-col gap-2'> <section className="flex flex-col gap-2">
<h2 className='font-semibold text-lg'>Upcoming</h2> <h2 className="font-semibold text-lg">Upcoming</h2>
<RoomList rooms={rooms.future} /> <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'>Today</h2> <h2 className="font-semibold text-lg">Today</h2>
<RoomList rooms={rooms.actual} /> <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'>Past</h2> <h2 className="font-semibold text-lg">Past</h2>
<RoomList rooms={rooms.past} /> <RoomList rooms={rooms.past} />
</section> </section>
</div> </div>

View File

@ -1,7 +1,7 @@
'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'; import { useEffect, useState } from "react";
export const ThemeSwitcher = () => { export const ThemeSwitcher = () => {
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
@ -15,12 +15,12 @@ export const ThemeSwitcher = () => {
return ( return (
<Button <Button
size='sm' size="sm"
variant='flat' variant="flat"
className='min-w-0' className="min-w-0"
onPress={() => setTheme(theme === 'light' ? 'dark' : 'light')} onPress={() => setTheme(theme === "light" ? "dark" : "light")}
> >
{theme === 'light' ? '🌑' : '☀️'} {theme === "light" ? "🌑" : "☀️"}
</Button> </Button>
); );
}; };

View File

@ -1,18 +1,18 @@
import { Metadata } from 'next'; import { Metadata } from "next";
import { Header } from './components/Header'; import { Header } from "./components/Header";
import { RoomTable } from './components/Room/Table'; import { RoomTable } from "./components/Room/Table";
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Toogether | Home', title: "Toogether | Home",
description: description:
'Toogether is a platform that allows you to create and join rooms to study together.' "Toogether is a platform that allows you to create and join rooms to study together.",
}; };
export default function HomePage() { export default function HomePage() {
return ( return (
<> <>
<Header /> <Header />
<main className='flex flex-col gap-8 p-4'> <main className="flex flex-col gap-8 p-4">
<RoomTable /> <RoomTable />
</main> </main>
</> </>