1
0
mirror of https://github.com/Fayorg/calendrier-avant.git synced 2026-05-27 17:18:38 +02:00

Compare commits

...

32 Commits

Author SHA1 Message Date
4e7dede837 add: idk 2024-08-15 21:08:38 -04:00
d21aaa1757 add: shadcn tableData component 2023-12-20 21:12:43 +01:00
ef45d7eac7 add: dashboard tabs 2023-12-20 19:36:52 +01:00
8b32c62754 add: new property on session & token 2023-12-20 19:36:35 +01:00
2e2a44cb5d add: shadcn Tab component 2023-12-20 19:35:53 +01:00
03f5321062 fix: removed unused featcher 2023-12-20 15:19:00 +01:00
01e87a75d6 add: canVote field in db 2023-12-20 15:16:23 +01:00
Elie Baier
25062abaf0 Merge pull request #10 from Fayorg/dev
Refactor + optimization
2023-12-20 15:12:20 +01:00
f326a0fd7b fix: removed nextjs telemetry 2023-12-20 15:01:38 +01:00
a9549c7180 add: sharp for image optimization in prod 2023-12-20 14:58:49 +01:00
7159c7f44a fix: added env for NextAuth in example 2023-12-20 14:56:09 +01:00
9ed99fff25 fix: removed comments 2023-12-20 14:55:17 +01:00
d31e709583 fix: unused db call 2023-12-20 14:54:54 +01:00
9b7daa2bcb fix: moved images to assets 2023-12-20 14:51:41 +01:00
771d9a8637 fix: refactored server functions names 2023-12-20 14:44:36 +01:00
7c89ca8338 fix: auto-refresh 2023-12-20 10:52:09 +01:00
Elie Baier
ff93a4c2d3 Merge pull request #9 from Fayorg/dev
Results page
2023-12-20 10:22:44 +01:00
b47b59f4cd fix: removed unused database fields & fix prisma errors 2023-12-20 10:20:38 +01:00
timhaller
da3a9df402 changed some styles on the GradingForm.tsx 2023-12-20 10:19:27 +01:00
7b409d5114 fix: removed unused import 2023-12-18 16:21:20 +01:00
25ec9c2e50 add: auto-refresh on the results pages 2023-12-18 16:17:29 +01:00
8ca5133527 fix: removed comments 2023-12-18 16:00:55 +01:00
18afac998b add: support for ginger 2023-12-18 16:00:05 +01:00
Elie Baier
0a6313ed00 Merge pull request #8 from Fayorg/dev
Using NextAuth for authentication and Server actions
2023-12-18 15:14:44 +01:00
Elie Baier
58afbe1ab5 add: workflow lint.yml on pr and push on master 2023-12-18 15:11:59 +01:00
Elie Baier
4ac6c9f488 Merge pull request #7 from Fayorg/authentication
Using NextAuth for authentication & Server actions
2023-12-18 15:06:48 +01:00
70877ea8dc fix: removed console.log in different authentication methods 2023-12-18 15:03:42 +01:00
ff6133d80b fix: removed unused api route 2023-12-18 15:02:07 +01:00
226100f25a add: full authentication with NextAuth 2023-12-18 14:59:34 +01:00
Elie Baier
57d169c0ce Merge pull request #6 from Fayorg/dev-2
Fixed typo in readme
2023-12-18 14:23:41 +01:00
c48f1e23a3 add: authentication WIP 2023-12-18 10:01:22 +01:00
2eaccd52e4 add: using nextauth WIP 2023-12-15 15:21:51 +01:00
68 changed files with 1541 additions and 472 deletions

View File

@@ -1 +1,3 @@
DATABASE_URL='mysql://root:root@mydb.com/calendrier'
NEXTAUTH_SECRET=supersecret
NEXTAUTH_URL=http://localhost:3000

View File

@@ -75,7 +75,7 @@ jobs:
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXT_TELEMETRY_DISABLED: 1
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

23
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Lint
on:
pull_request:
push:
branches:
- master
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm ci # or yarn install
- uses: sibiraj-s/action-eslint@v3
with:
eslint-args: '--ignore-path=.gitignore --quiet'
ignore-path: .eslintignore
extensions: 'js,jsx,ts,tsx'
annotations: true

28
actions/accounts.ts Normal file
View File

@@ -0,0 +1,28 @@
"use server";
import prisma from "@/lib/prisma";
import { Users } from "@prisma/client";
interface GetAccountsParameters {
take?: number;
skip?: number;
}
export type Account = Pick<Users, "id" | "firstName" | "lastName" | "isAdmin" | "isTeacher">;
export async function getAccounts(args?: GetAccountsParameters): Promise<Account[]> {
return await prisma.users.findMany({
take: args?.take,
skip: args?.skip,
orderBy: {
id: "asc",
},
select: {
id: true,
firstName: true,
lastName: true,
isAdmin: true,
isTeacher: true,
}
});
}

15
actions/grades.ts Normal file
View File

@@ -0,0 +1,15 @@
"use server";
import prisma from "@/lib/prisma";
export async function addGrade(testId: number, userId: number, grade: number) {
const newGrade = await prisma.grade.create({
data: {
grade: grade,
testId: testId,
userId: userId,
}
});
return newGrade;
}

View File

@@ -2,6 +2,7 @@
import prisma from "@/lib/prisma";
// Only a test function
export async function setTestActive(id: number, active: boolean) {
const users = await prisma.users.findFirst();
@@ -9,4 +10,61 @@ export async function setTestActive(id: number, active: boolean) {
return true;
}
export async function getFirstActiveTest(date: Date) {
return await prisma.test.findFirst({
select: {
id: true,
isActive: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
},
},
createdAt: true,
testOn: true,
},
where: {
isActive: true,
testOn: new Date(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDate().toString().length === 1 ? '0' + date.getDate() : date.getDate())),
},
});
}
export async function getFirstActiveTestWithGrade(date: Date, userId: number) {
return await prisma.test.findFirst({
select: {
id: true,
isActive: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
},
},
grades: {
select: {
id: true,
grade: true,
createdAt: true,
},
take: 1,
where: {
userId: userId
}
},
createdAt: true,
testOn: true,
},
where: {
isActive: true,
testOn: new Date(date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + (date.getDate().toString().length === 1 ? '0' + date.getDate() : date.getDate())),
},
});
}

33
actions/results.ts Normal file
View File

@@ -0,0 +1,33 @@
"use server";
import prisma from "@/lib/prisma";
export interface Results {
name: string;
total: number;
}
export async function getStudentResults(testId: number) {
const grades = await prisma.grade.findMany({ where: { testId: testId, user: { isTeacher: false } } });
const allGrades = ['1', '1.5', '2', '2.5', '3', '3.5', '4', '4.5', '5', '5.5', '6'];
let gradeOccurences = new Array(allGrades.length).fill(0);
const gradeList = grades.map((grade) => grade.grade);
for (let i = 0; i < gradeList.length; i++) {
gradeOccurences[allGrades.indexOf(gradeList[i].toString())]++;
}
let data: Results[] = [];
for (let i = 0; i < gradeOccurences.length; i++) {
data.push({
name: allGrades[i],
total: gradeOccurences[i],
});
}
return data;
}
export async function getTeacherResult(testId: number) {
return await prisma.grade.findFirst({ where: { testId: testId, user: { isTeacher: true } } });
}

View File

@@ -0,0 +1,6 @@
import NextAuth from "next-auth";
import { authOptions } from "@lib/authenticate";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -1,93 +0,0 @@
import {NextRequest, NextResponse} from "next/server";
import prisma from "@/lib/prisma";
interface IBody {
key: string
grade: number
testId: number
}
export async function POST(req: Request){
const body: IBody = await req.json();
const test = await prisma.test.findFirst({
select: {
id: true,
testOn: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true
}
},
grades: {
select: {
note: true,
grade: true,
createdAt: true,
},
where: {
user: {
key: body.key
}
}
}
},
where: {
id: body.testId
}
});
if(!test){
return NextResponse.json({error: "Test not found"}, {status: 404});
}
if(test.grades.length > 0){
return NextResponse.json({error: "You have already voted"}, {status: 403});
}
const grade = await prisma.grade.create({
data: {
note: "",
grade: body.grade,
user: {
connect: {
key: body.key
}
},
test: {
connect: {
id: test.id
}
}
}
});
return NextResponse.json({
id: test.id,
testOn: test.testOn,
testOf: {
firstName: test.testOf.firstName,
lastName: test.testOf.lastName,
isTeacher: test.testOf.isTeacher
},
vote: {
hasVoted: test.grades?.length > 0,
grade: grade.grade,
note: grade.note,
createdAt: grade.createdAt
}
});
}
export async function GET(req: NextRequest){
const key = req.nextUrl.searchParams.get("key");
if(!key) return NextResponse.json({error: "No key provided"}, {status: 400});
const grades = await prisma.grade.findMany({})
return NextResponse.json(grades, {status: 200});
}

View File

@@ -1,36 +0,0 @@
import {NextRequest, NextResponse} from "next/server";
import prisma from "@/lib/prisma";
export async function GET(req: NextRequest){
const key = req.nextUrl.searchParams.get("key");
if(!key) return NextResponse.json({error: "No key provided"}, {status: 400});
const user = await prisma.users.findUnique({
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
test: {
select: {
testOn: true
}
}
},
where: {
key
}
});
if(!user) return NextResponse.json({error: "Key not found"}, {status: 404});
return NextResponse.json({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
isTeacher: user.isTeacher,
testOn: user.test?.testOn
});
}

View File

@@ -1,59 +0,0 @@
import {NextRequest, NextResponse} from "next/server";
import prisma from "@/lib/prisma";
export async function GET(req: NextRequest){
const date = req.nextUrl.searchParams.get("date");
const key = req.nextUrl.searchParams.get("key");
const usableDate = new Date(date || new Date());
const test = await prisma.test.findFirst({
select: {
id: true,
testOn: true,
testOf: {
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true
}
},
grades: {
select: {
note: true,
grade: true,
createdAt: true,
},
where: {
user: {
key: key || ""
}
}
}
},
where: {
testOn: new Date(usableDate.getFullYear() + "-" + (usableDate.getMonth() + 1) + "-" + (usableDate.getDate().toString().length === 1 ? "0" + usableDate.getDate() : usableDate.getDate()))
}
});
if(!test){
return NextResponse.json({error: "Test not found"}, {status: 404});
}
return NextResponse.json({
id: test.id,
testOn: test.testOn,
testOf: {
firstName: test.testOf.firstName,
lastName: test.testOf.lastName,
isTeacher: test.testOf.isTeacher
},
vote: {
hasVoted: test.grades?.length > 0,
grade: test.grades[0]?.grade,
note: test.grades[0]?.note,
createdAt: test.grades[0]?.createdAt
}
});
}

View File

@@ -0,0 +1,14 @@
import { getAccounts } from '@/actions/accounts';
import prisma from '@/lib/prisma';
export async function Accounts() {
const users = await getAccounts({ take: 2 });
console.log(users);
return (
<div>
<h1 className="text-white">Accounts</h1>
</div>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { Account } from '@/actions/accounts';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"
export const columns: ColumnDef<Account>[] = [
{
accessorKey: 'lastName',
header: 'Last Name',
},
{
accessorKey: 'firstName',
header: 'First Name',
},
];
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})

View File

@@ -1,22 +0,0 @@
'use client';
import { setTestActive } from '@/actions/mangeTest';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
export default function ActiveCard() {
return (
<AlertDialog>
<AlertDialogTrigger>Open</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Etes-vous sur de vouloir terminer ce test?</AlertDialogTitle>
<AlertDialogDescription>Les votations ne seront plus ouverte pour ce test. Vous pouvez cependant le réactiver dans le dashboard.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => setTestActive(1, true)}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -0,0 +1,7 @@
export function Simulation() {
return (
<div>
<h1 className="text-white">Simulation</h1>
</div>
);
}

22
app/dashboard/Tabs.tsx Normal file
View File

@@ -0,0 +1,22 @@
import { TabsContent, TabsList, TabsTrigger, Tabs as TabsShad } from '@/components/ui/tabs';
import { Simulation } from './Simulation';
import { Accounts } from './Accounts';
export function Tabs() {
return (
<TabsShad defaultValue="simulation" className="w-[400px]">
<TabsList>
<TabsTrigger value="simulation">Simulation</TabsTrigger>
<TabsTrigger value="accounts">Comptes</TabsTrigger>
{/* <TabsTrigger value="exercices">Exercices</TabsTrigger> */}
<TabsTrigger value="statistics">Statistiques</TabsTrigger>
</TabsList>
<TabsContent value="simulation">
<Simulation />
</TabsContent>
<TabsContent value="accounts">
<Accounts />
</TabsContent>
</TabsShad>
);
}

View File

@@ -1,25 +1,21 @@
import prisma from '@/lib/prisma';
import ActiveCard from './ActiveCard';
import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
import { Tabs } from './Tabs';
export default async function Dashboard() {
const tests = await prisma.test.findMany({ select: { isActive: true, isPassed: true, id: true, testOf: { select: { id: true, firstName: true, lastName: true, isTeacher: true } } } });
const session = await getAuthServerSession();
console.log(session?.user);
if (!session || !(session.user.isAdmin || session.user.isTeacher)) return redirect('/');
const tests = await prisma.test.findMany({ select: { isActive: true, id: true, testOf: { select: { id: true, firstName: true, lastName: true, isTeacher: true } } } });
const activeTests = tests.filter((test) => test.isActive);
const passedTests = tests.filter((test) => test.isPassed);
return (
<div>
<h1>Dashboard</h1>
<div className="border-2 border-white rounded-2xl bg-red-500 p-2">
<h2>Test(s) Actif(s) :</h2>
<ul>
{activeTests.map((test) => (
<li key={test.id}>
{test.testOf.firstName + ' ' + test.testOf.lastName} <ActiveCard />
</li>
))}
</ul>
</div>
<div className="container flex flex-col gap-4 my-4">
<h1 className="text-4xl text-white font-bold">Dashboard</h1>
<Tabs />
</div>
);
}

View File

@@ -1,42 +1,20 @@
'use client';
import logo from '@/assets/images/logo.svg';
import Image from 'next/image';
import { LoginForm } from '@/components/forms/LoginForm';
import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
import { Poppins } from 'next/font/google'
import { Check } from 'lucide-react'
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default async function Home() {
const session = await getAuthServerSession();
import logo from '../images/logo.svg';
import Image from "next/image";
import {Input} from "@components/ui/input";
import {white} from "next/dist/lib/picocolors";
export default function Home() {
const [password, setPassword] = useState('');
const router = useRouter();
useEffect(() => {
if (localStorage.getItem('@password')) {
router.push('/play');
}
}, [router]);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
localStorage.setItem('@password', password);
router.push('/play');
if (session != null) {
redirect('/play');
}
return (
<div className={"w-full h-screen text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28"}>
<Image src={logo} alt={"Logo"} width={100} height={200} className={"mx-auto w-full md:w-[400px]"}/>
<form onSubmit={handleSubmit} className={"flex flex-row gap-4 w-full md:w-[400px]"}>
<Input type="password" placeholder="Mot de passe" id="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)}></Input>
<input type="submit" value="Submit" id="submit" className={"hidden"} />
<label htmlFor="submit" className={"w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center"}>
<Check width={24} height={24} color={"white"}/>
</label>
</form>
<div className={'w-full h-screen text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}>
<Image src={logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} />
<LoginForm />
</div>
);
}

47
app/play/TodayTest.tsx Normal file
View File

@@ -0,0 +1,47 @@
'use client';
import { getFirstActiveTestWithGrade } from '@/actions/mangeTest';
import { TestCard } from '@/components/custom';
import { Session } from 'next-auth';
import { signOut } from 'next-auth/react';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import LogOut from '@images/logout.svg';
import { GradingForm } from '@/components/forms/GradingForm';
import YourGrade from '@images/your-grade.svg';
export function TodayTest({ session }: { session: Session }) {
const [activeTest, setActiveTest] = useState<{ data: any | null; error: Error | null; isLoading: boolean }>({ isLoading: true, data: null, error: null });
useEffect(() => {
getFirstActiveTestWithGrade(new Date(), session.user.id)
.catch((err) => setActiveTest({ data: null, error: err, isLoading: false }))
.then((data) => setActiveTest({ data, error: null, isLoading: false }));
}, [session.user.id]);
return (
<>
<TestCard data={activeTest.data} error={activeTest.error} isLoading={activeTest.isLoading} />
{activeTest.data &&
(activeTest.data.grades[0] ? (
<div className={'w-full md:w-[400px]'}>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={'Your Grade'} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{activeTest.data.grades[0].grade}</span>
</div>
</div>
) : (
<GradingForm session={session} testId={activeTest.data.id} />
))}
<div>
<button
onClick={async () => {
await signOut();
}}
>
<Image src={LogOut} alt={'Logo'} width={50} height={50} className={'mx-auto w-full md:w-[400px]'} />
</button>
</div>
</>
);
}

12
app/play/layout.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
export default async function Layout({ children }: { children: React.ReactNode }) {
const session = await getAuthServerSession();
if (session == null) {
redirect('/');
}
return <>{children}</>;
}

View File

@@ -1,43 +1,21 @@
'use client';
import Image from 'next/image';
import Logo from '@images/logo.svg';
import { getAuthServerSession } from '@/lib/authenticate';
import { redirect } from 'next/navigation';
import { getFirstActiveTest } from '@/actions/mangeTest';
import { TodayTest } from './TodayTest';
import { GradingForm, TestCard } from '@/components/custom';
import { useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';
import useSWR from 'swr';
import { fetcher } from '@/lib/fetcher';
import Image from "next/image";
import Logo from "/images/logo.svg";
import LogOut from "@images/logout.svg";
export default async function Play() {
const session = await getAuthServerSession();
export default function Play() {
const router = useRouter();
const [password, setPassword] = useState<string | null>('');
const { data, error, isLoading } = useSWR('/api/test?key=' + password, fetcher);
useEffect(() => {
const pass = localStorage.getItem('@password');
setPassword(pass);
if (!pass) {
router.push('/');
}
}, [router]);
if (session == null) {
redirect('/');
}
return (
<div className={"w-full h-[100vh] text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28"}>
<Image src={Logo} alt={"Logo"} width={100} height={200} className={"mx-auto w-full md:w-[400px]"}/>
<TestCard data={data} error={error} isLoading={isLoading} />
{data && data.status == 200 && password && <GradingForm password={password} data={data} />}
<div>
<button
onClick={() => {
localStorage.clear();
router.push('/');
}}
>
<Image src={LogOut} alt={"Logo"} width={50} height={50} className={"mx-auto w-full md:w-[400px]"}/>
</button>
</div>
<div className={'w-full h-[100vh] text-[#F0F0F0] bg-black p-12 flex flex-col items-center justify-center gap-y-28'}>
<Image src={Logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} />
<TodayTest session={session} />
</div>
);
}

View File

@@ -1,43 +1,43 @@
'use server';
'use client';
import { Chart } from '@components/custom/chart';
import logo from '@images/logo.svg';
import Image from 'next/image';
import Prisma from '@lib/prisma';
import ginger from '@images/ginger.png';
import { getStudentResults, getTeacherResult, Results } from '@/actions/results';
import { useEffect, useState } from 'react';
interface data {
name: string;
total: number;
}
export default function Page({ params }: { params: { id: string } }) {
const testId = parseInt(params.id);
export default async function Page({ params }: { params: { id: string } }) {
const teacherGrade = await Prisma.grade.findFirst({ where: { testId: parseInt(params.id), user: { isTeacher: true } } });
const [teacherGrade, setTeacherGrade] = useState<number>(0);
const [data, setData] = useState<Results[]>([]);
const grades = await Prisma.grade.findMany({where: {testId: parseInt(params.id), user: {isTeacher: false}}});
function featchResults(testId: number) {
getTeacherResult(testId)
.then((res) => setTeacherGrade(res?.grade || 0))
.catch((err) => console.error(err));
const allGrades = ['1', '1.5', '2', '2.5', '3', '3.5', '4', '4.5', '5', '5.5', '6'];
let gradeOccurences = new Array(allGrades.length).fill(0);
const gradeList = grades.map((grade) => grade.grade);
for (let i = 0; i < gradeList.length; i++) {
gradeOccurences[allGrades.indexOf(gradeList[i].toString())]++;
getStudentResults(testId)
.then((res) => setData(res))
.catch((err) => console.error(err));
}
let data: data[] = [];
for (let i = 0; i < gradeOccurences.length; i++) {
data.push({
name: allGrades[i],
total: gradeOccurences[i],
});
}
useEffect(() => {
featchResults(testId);
setInterval(() => {
featchResults(testId);
}, 5000);
}, [testId]);
return (
<div className={'p-4 md:p-12 w-full h-screen bg-black flex flex-col justify-between gap-y-4'}>
<Image src={logo} alt={'Logo'} width={100} height={200} className={'mx-auto w-full md:w-[400px]'} />
{teacherGrade && (
<div className={'flex flex-col gap-y-2'}>
<p className="text-white">Mme Tixhon :</p>
<h2 className={'text-4xl font-bold text-white'}>{teacherGrade.grade}</h2>
<div className={'flex justify-center relative w-full'}>
<div className={'flex items-center text-center'}>
<Image src={ginger} alt={'Logo'} width={120} height={120} className={'inline-flex'} />
<h2 className={'text-4xl font-bold text-white absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]'}>{teacherGrade}</h2>
</div>
</div>
)}
<Chart data={data} />

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/images/ginger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 74 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1,32 +1,32 @@
"use client"
'use client';
import {Santa} from "@components/custom/santa";
import { Santa } from '@components/custom/santa';
interface ChartProps {
data: {
name: string
total: number
}[]
data: {
name: string;
total: number;
}[];
}
export function Chart({...props}: ChartProps) {
return (
<div className={"text-[#F0F0F0] flex-1 flex items-end"}>
<div className={"w-full"}>
<div className={"flex flex-row justify-between w-full items-end"}>
{props.data.map((item, index) => (
<Santa height={item.total} key={index}/>
))}
</div>
<div className={"flex flex-row justify-between w-full items-end mt-2"}>
{props.data.map((item, index) => (
<div className={"w-[100px] h-fit flex flex-col items-center"} key={index}>
<div className={"h-[14px] w-[3pt] bg-white"}/>
<span className={""}>{item.name}</span>
</div>
))}
</div>
</div>
</div>
)
export function Chart({ ...props }: ChartProps) {
return (
<div className={'text-[#F0F0F0] flex-1 flex items-end'}>
<div className={'w-full'}>
<div className={'flex flex-row justify-between w-full items-end'}>
{props.data.map((item, index) => (
<Santa height={item.total} key={index} />
))}
</div>
<div className={'flex flex-row justify-between w-full items-end mt-2'}>
{props.data.map((item, index) => (
<div className={'w-[100px] h-fit flex flex-col items-center'} key={index}>
<div className={'h-[14px] w-[3pt] bg-white'} />
<span className={''}>{item.name}</span>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,64 +0,0 @@
'use client';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import Image from 'next/image';
import YourGrade from '@images/your-grade.svg';
import {Check} from "lucide-react";
export function GradingForm({ password, data }: { password: string; data: any }) {
const [grade, setGrade] = useState<number>(0);
const [hasVoted, setHasVoted] = useState<boolean>(false);
const router = useRouter();
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const response = await fetch('/api/grade', {
method: 'POST',
body: JSON.stringify({ grade, key: password, testId: data.id }),
});
const json = await response.json();
if (response.status == 200) {
setHasVoted(true);
} else {
alert(json.message);
}
}
if (data.vote.hasVoted || hasVoted) {
return (
<div className={"w-full md:w-[400px]"}>
<div className={"flex flex-row justify-evenly"}>
<Image src={YourGrade} alt={YourGrade} width={100}/>
<span className={"w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center"}>{data.vote.hasVoted ? data.vote.grade : grade}</span>
</div>
</div>
);
}
return (
<div className={"w-full md:w-[400px]"}>
<form onSubmit={handleSubmit}>
<div>
<input
type="range"
min="2"
max="12"
id="gradeSelector"
className={"w-full h-4 bg-white range range:bg-white text-white"}
onChange={(e) => setGrade(Number(e.target.value) / 2)}
/>
</div>
<div className={"flex flex-row justify-evenly"}>
<Image src={YourGrade} alt={YourGrade} width={100}/>
<span className={"w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center"}>{grade}</span>
<input type="submit" value={'Envoyer'} id="submit" className={"hidden"}></input>
<label htmlFor="submit" className={"w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center"}>
<Check width={24} height={24} color={"white"}/>
</label>
</div>
</form>
</div>
);
}

View File

@@ -1,4 +1,3 @@
export { DayCard as DayCard } from './dayCard';
export { TestCard as TestCard } from './testCard';
export { GradingForm } from "./gradingForm";
export { Santa } from "./santa";

View File

@@ -1,17 +1,17 @@
import Image from "next/image";
import Image from 'next/image';
import { santas } from "@santa/index";
import { santas } from '@/assets/images/santa/index';
interface SantaProps {
height: number
height: number;
}
export function Santa({ ...props} : SantaProps) {
if (props.height <= 0 || props.height > 24) {
return <div className={"w-[100px] bg-blue-500"}/>
}
return (
<div className="flex flex-col items-center">
<Image src={santas[props.height - 1]} alt={"Santa"} width={100} height={200}/>
</div>
)
export function Santa({ ...props }: SantaProps) {
if (props.height <= 0 || props.height > 24) {
return <div className={'w-[100px] bg-blue-500'} />;
}
return (
<div className="flex flex-col items-center">
<Image src={santas[props.height - 1]} alt={'Santa'} width={100} height={200} />
</div>
);
}

View File

@@ -1,49 +1,48 @@
'use client';
import Calendar from "../../images/date.svg";
import Image from "next/image";
import Calendar from '@images/date.svg';
import Image from 'next/image';
import { Ban, Snowflake, CircleOff } from "lucide-react";
import { Ban, Snowflake, CircleOff } from 'lucide-react';
export function TestCard({ data, error, isLoading }: { data: any; error: any; isLoading: boolean }) {
console.log(data);
if (isLoading)
return (
<div className={"w-fit h-fit relative"}>
<Image src={Calendar} alt={"Calendrier absolute"} />
<div className={"w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center"}>
<Snowflake width={36} height={36} color={"#000000"} className={"animate-spin place-self-center"} />
<span className={"text-black text-xs"}>Chargement</span>
<div className={'w-fit h-fit relative'}>
<Image src={Calendar} alt={'Calendrier absolute'} />
<div className={'w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center'}>
<Snowflake width={36} height={36} color={'#000000'} className={'animate-spin place-self-center'} />
<span className={'text-black text-xs'}>Chargement</span>
</div>
</div>
);
if (error)
return (
<div className={"w-fit h-fit relative"}>
<Image src={Calendar} alt={"Calendrier absolute"} />
<div className={"w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center"}>
<Ban width={36} height={36} color={"#000000"} className={"place-self-center"}/>
<span className={"text-black text-xs"}>Erreur</span>
<div className={'w-fit h-fit relative'}>
<Image src={Calendar} alt={'Calendrier absolute'} />
<div className={'w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center'}>
<Ban width={36} height={36} color={'#000000'} className={'place-self-center'} />
<span className={'text-black text-xs'}>Erreur</span>
</div>
</div>
);
if (data.status == 404)
if (!data)
return (
<div className={"w-fit h-fit relative"}>
<Image src={Calendar} alt={"Calendrier absolute"} />
<div className={"w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center"}>
<CircleOff width={36} height={36} color={"#000000"} className={"place-self-center"}/>
<span className={"text-black text-xs"}>Pas de test</span>
<div className={'w-fit h-fit relative'}>
<Image src={Calendar} alt={'Calendrier absolute'} />
<div className={'w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center'}>
<CircleOff width={36} height={36} color={'#000000'} className={'place-self-center'} />
<span className={'text-black text-xs'}>Pas de test</span>
</div>
</div>
);
return (
<div className={"w-fit h-fit relative"}>
<Image src={Calendar} alt={"Calendrier absolute"} />
<div className={"w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center"}>
<span className={"text-black text-[36px] font-medium"}>{new Date().getDate()}</span>
<span className={"text-black text-xs"}>{data.testOf.firstName}</span>
<div className={'w-fit h-fit relative'}>
<Image src={Calendar} alt={'Calendrier absolute'} />
<div className={'w-full h-full absolute top-0 left-0 p-4 flex flex-col place-content-end text-center'}>
<span className={'text-black text-[36px] font-medium'}>{new Date().getDate()}</span>
<span className={'text-black text-xs'}>{data.testOf.firstName}</span>
</div>
</div>
);

View File

@@ -0,0 +1,55 @@
'use client';
import { useState } from 'react';
import Image from 'next/image';
import YourGrade from '@images/your-grade.svg';
import { Check } from 'lucide-react';
import { Session } from 'next-auth';
import { addGrade } from '@/actions/grades';
export function GradingForm({ session, testId }: { session: Session; testId: number }) {
const [grade, setGrade] = useState<number>(4);
const [hasVoted, setHasVoted] = useState<boolean>(false);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const result = await addGrade(testId, session.user.id, grade);
if (result.id) {
console.log('ok');
setHasVoted(true);
setGrade(result.grade);
} else {
console.log('error');
}
}
if (hasVoted && grade != 0) {
return (
<div className={'w-full md:w-[400px]'}>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={YourGrade} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{grade}</span>
</div>
</div>
);
}
return (
<div className={'w-full md:w-[400px]'}>
<form onSubmit={handleSubmit}>
<div>
<input type="range" value={grade} step="0.5" min="1" max="6" id="gradeSelector" className={'w-full appearance-none bg-transparent [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-primary/75 accent-accent'} onChange={(e) => setGrade(Number(e.target.value))} />
</div>
<div className={'flex flex-row justify-evenly'}>
<Image src={YourGrade} alt={YourGrade} width={100} />
<span className={'w-[54px] h-[54px] bg-accent rounded-[20px] contents-none grid place-content-center'}>{grade}</span>
<input type="submit" value={'Envoyer'} id="submit" className={'hidden'}></input>
<label htmlFor="submit" className={'w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center'}>
<Check width={24} height={24} color={'white'} />
</label>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,29 @@
'use client';
import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { Input } from '../ui/input';
import { Check } from 'lucide-react';
export function LoginForm() {
const [password, setPassword] = useState('');
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
await signIn('credentials', {
key: password,
callbackUrl: '/play',
});
}
return (
<form onSubmit={handleSubmit} className={'flex flex-row gap-4 w-full md:w-[400px]'}>
<Input type="password" placeholder="Mot de passe" id="password" name="password" value={password} onChange={(e) => setPassword(e.target.value)}></Input>
<input type="submit" value="Submit" id="submit" className={'hidden'} />
<label htmlFor="submit" className={'w-[54px] h-[54px] bg-secondary rounded-[20px] contents-none grid place-content-center'}>
<Check width={24} height={24} color={'white'} />
</label>
</form>
);
}

117
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

55
components/ui/tabs.tsx Normal file
View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

78
lib/authenticate.ts Normal file
View File

@@ -0,0 +1,78 @@
import Credentials from "next-auth/providers/credentials";
import prisma from "./prisma";
import { getServerSession, RequestInternal, type NextAuthOptions, User } from "next-auth";
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next"
export async function authenticate(key: string) {
const user = await prisma.users.findUnique({
select: {
id: true,
firstName: true,
lastName: true,
isTeacher: true,
isAdmin: true
},
where: {
key: key
}
});
if(!user) return null;
return user;
}
export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.userId = parseInt(user.id as string);
token.firstName = user.firstName;
token.lastName = user.lastName;
token.isTeacher = user.isTeacher;
token.isAdmin = user.isAdmin;
}
return token;
},
async session({ session, token, user }) {
if(token) {
session.user.id = token.userId;
session.user.firstName = token.firstName;
session.user.lastName = token.lastName;
session.user.isTeacher = token.isTeacher;
session.user.isAdmin = token.isAdmin;
}
return session;
},
},
pages: {
signIn: '/',
},
providers: [
Credentials({
name: "Credentials",
credentials: {
key: { label: "Key", type: "password" }
},
async authorize(credentials: Record<"key", string> | undefined, req: Pick<RequestInternal, "body" | "query" | "headers" | "method">) {
const { key } = credentials as {
key: string
};
const user = await authenticate(key) as User | null
return user;
}
})
],
};
// Use it in server contexts
export function auth(...args: [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]] | [NextApiRequest, NextApiResponse] | []) {
return getServerSession(...args, authOptions)
}
export const getAuthServerSession = () => getServerSession(authOptions);

View File

@@ -1,3 +0,0 @@
import { stat } from "fs";
export const fetcher = (input: URL | RequestInfo, init?: RequestInit | undefined) => fetch(input, init).then(res => res.json().then(data => ({...data, status: res.status})));

745
package-lock.json generated
View File

@@ -13,17 +13,21 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-table": "^8.11.2",
"chart.js": "^4.4.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"next": "14.0.3",
"next-auth": "^4.24.5",
"prisma": "^5.6.0",
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18",
"react-timer-hook": "^3.0.7",
"sharp": "^0.33.1",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"swr": "^2.2.4",
@@ -74,6 +78,15 @@
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz",
"integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -197,6 +210,437 @@
"integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz",
"integrity": "sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz",
"integrity": "sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz",
"integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=11",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz",
"integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"macos": ">=10.13",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz",
"integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz",
"integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz",
"integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz",
"integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz",
"integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz",
"integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz",
"integrity": "sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz",
"integrity": "sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz",
"integrity": "sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.28",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz",
"integrity": "sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"glibc": ">=2.26",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz",
"integrity": "sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz",
"integrity": "sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"musl": ">=1.2.2",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz",
"integrity": "sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^0.44.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz",
"integrity": "sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz",
"integrity": "sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0",
"npm": ">=9.6.5",
"pnpm": ">=7.1.0",
"yarn": ">=3.2.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -426,6 +870,14 @@
"node": ">= 8"
}
},
"node_modules/@panva/hkdf": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz",
"integrity": "sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@prisma/client": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.6.0.tgz",
@@ -826,6 +1278,37 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz",
"integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-collection": "1.0.3",
"@radix-ui/react-compose-refs": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-use-callback-ref": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz",
@@ -920,6 +1403,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
"integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.1",
"@radix-ui/react-context": "1.0.1",
"@radix-ui/react-direction": "1.0.1",
"@radix-ui/react-id": "1.0.1",
"@radix-ui/react-presence": "1.0.1",
"@radix-ui/react-primitive": "1.0.3",
"@radix-ui/react-roving-focus": "1.0.4",
"@radix-ui/react-use-controllable-state": "1.0.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
@@ -1127,6 +1640,37 @@
"tslib": "^2.4.0"
}
},
"node_modules/@tanstack/react-table": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.11.2.tgz",
"integrity": "sha512-ztLg2OpM3HZIWzkQYjQER1inZuhbt79fBwZxc9bPXzsvqY+7RYI3dCZLw3CynYd9s4YltdrTbmSyh4xQSHexDQ==",
"dependencies": {
"@tanstack/table-core": "8.11.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.11.2.tgz",
"integrity": "sha512-rR0VEQOtr0ARLvaNLaSQnt2BVwOp0OavOUA0LcZ3N45tLYXc4sXruNv8kJ7R7+5W1CrzGha217tzjBG83CpoMQ==",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
@@ -1849,11 +2393,22 @@
"node": ">=6"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -1864,8 +2419,16 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/commander": {
"version": "4.1.1",
@@ -1999,6 +2562,14 @@
"node": ">=6"
}
},
"node_modules/detect-libc": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
"integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@@ -3159,6 +3730,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-async-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
@@ -3499,6 +4075,14 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/jose": {
"version": "4.15.4",
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz",
"integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3650,7 +4234,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -3816,6 +4399,41 @@
}
}
},
"node_modules/next-auth": {
"version": "4.24.5",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.5.tgz",
"integrity": "sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"@panva/hkdf": "^1.0.2",
"cookie": "^0.5.0",
"jose": "^4.11.4",
"oauth": "^0.9.15",
"openid-client": "^5.4.0",
"preact": "^10.6.3",
"preact-render-to-string": "^5.1.19",
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "^12.2.5 || ^13 || ^14",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
}
},
"node_modules/next-auth/node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -3866,6 +4484,11 @@
"node": ">=0.10.0"
}
},
"node_modules/oauth": {
"version": "0.9.15",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -3991,6 +4614,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/oidc-token-hash": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
"engines": {
"node": "^10.13.0 || >=12.0.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -3999,6 +4630,28 @@
"wrappy": "1"
}
},
"node_modules/openid-client": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.1.tgz",
"integrity": "sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==",
"dependencies": {
"jose": "^4.15.1",
"lru-cache": "^6.0.0",
"object-hash": "^2.2.0",
"oidc-token-hash": "^5.0.3"
},
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/openid-client/node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
"engines": {
"node": ">= 6"
}
},
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -4268,6 +4921,26 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/preact": {
"version": "10.19.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.3.tgz",
"integrity": "sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "5.2.6",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
"dependencies": {
"pretty-format": "^3.8.0"
},
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -4277,6 +4950,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/pretty-format": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
},
"node_modules/prisma": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.6.0.tgz",
@@ -4629,7 +5307,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -4669,6 +5346,45 @@
"node": ">= 0.4"
}
},
"node_modules/sharp": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz",
"integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"semver": "^7.5.4"
},
"engines": {
"libvips": ">=8.15.0",
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.1",
"@img/sharp-darwin-x64": "0.33.1",
"@img/sharp-libvips-darwin-arm64": "1.0.0",
"@img/sharp-libvips-darwin-x64": "1.0.0",
"@img/sharp-libvips-linux-arm": "1.0.0",
"@img/sharp-libvips-linux-arm64": "1.0.0",
"@img/sharp-libvips-linux-s390x": "1.0.0",
"@img/sharp-libvips-linux-x64": "1.0.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.0",
"@img/sharp-libvips-linuxmusl-x64": "1.0.0",
"@img/sharp-linux-arm": "0.33.1",
"@img/sharp-linux-arm64": "0.33.1",
"@img/sharp-linux-s390x": "0.33.1",
"@img/sharp-linux-x64": "0.33.1",
"@img/sharp-linuxmusl-arm64": "0.33.1",
"@img/sharp-linuxmusl-x64": "0.33.1",
"@img/sharp-wasm32": "0.33.1",
"@img/sharp-win32-ia32": "0.33.1",
"@img/sharp-win32-x64": "0.33.1"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4704,6 +5420,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -5325,6 +6049,14 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -5472,8 +6204,7 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yaml": {
"version": "2.3.4",

View File

@@ -14,17 +14,21 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@tanstack/react-table": "^8.11.2",
"chart.js": "^4.4.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.294.0",
"next": "14.0.3",
"next-auth": "^4.24.5",
"prisma": "^5.6.0",
"react": "^18",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18",
"react-timer-hook": "^3.0.7",
"sharp": "^0.33.1",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"swr": "^2.2.4",

View File

@@ -14,27 +14,26 @@ model Users {
firstName String
lastName String
isTeacher Boolean @default(false)
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
grades Grade[]
test Test?
}
model Test {
id Int @id @default(autoincrement())
testOfId Int @unique
testOn DateTime @db.Date
createdAt DateTime @default(now())
testOf Users @relation(fields: [testOfId], references: [id])
grades Grade[]
isActive Boolean @default(false)
isPassed Boolean @default(false)
teacherGrade Float?
id Int @id @default(autoincrement())
testOfId Int @unique
testOn DateTime @db.Date
canVote Boolean @default(false)
createdAt DateTime @default(now())
testOf Users @relation(fields: [testOfId], references: [id])
grades Grade[]
isActive Boolean @default(false)
}
model Grade {
id Int @id @default(autoincrement())
userId Int
note String?
createdAt DateTime @default(now())
grade Float
testId Int

View File

@@ -23,8 +23,8 @@
"@components/*": ["./components/*"],
"@lib/*": ["./lib/*"],
"@styles/*": ["/styles/*"],
"@santa/*": ["./images/santa/*"],
"@images/*": ["./images/*"],
"@santa/*": ["./assets/images/santa/*"],
"@images/*": ["./assets/images/*"],
"@assets/*": ["./assets/*"],
}
},

31
types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,31 @@
import NextAuth, { DefaultSession, DefaultJWT, DefaultUser } from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session {
user: {
id: number;
firstName: string;
lastName: string
isTeacher: boolean;
isAdmin: boolean;
} & DefaultSession["user"];
}
interface User {
id: number;
firstName: string;
lastName: string
isTeacher: boolean;
isAdmin: boolean;
}
}
declare module "next-auth/jwt" {
interface JWT {
userId: number;
firstName: string;
lastName: string
isTeacher: boolean;
isAdmin: boolean;
}
}