wrote multiple parts; attendance todo

This commit is contained in:
Aaron Lee 2023-12-31 02:28:09 +08:00
parent b5f7f9ef3c
commit 7b895a20c1
17 changed files with 591 additions and 61 deletions

View file

@ -29,6 +29,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.2",
"react-to-pdf": "^1.0.1",
"superjson": "^2.2.1",
"zod": "^3.22.4"
},

View file

@ -15,6 +15,7 @@ model User {
grade Int?
class String?
name String
dname String
username String @unique
password String
rfid String? @unique
@ -22,6 +23,7 @@ model User {
rosterOnly Boolean @default(false)
periods Period[]
attendance Attendance[]
number Int?
createdAt DateTime @default(now())
@ -48,8 +50,9 @@ model TimePeriod {
model Period {
id Int @id @default(autoincrement())
date DateTime
timePeriod TimePeriod @relation(fields: [timePeriodId], references: [id])
timePeriod TimePeriod @relation(fields: [timePeriodId], references: [id], onDelete: Cascade)
timePeriodId Int
collecting Boolean @default(true)
users User[]
@ -57,4 +60,16 @@ model Period {
updatedAt DateTime @updatedAt
@@index([date])
}
model Attendance {
id Int @id @default(autoincrement())
datetime DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([datetime])
}

View file

@ -40,7 +40,7 @@ export default function DashboardHeader({ url }: { url: string }) {
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-sky-200 focus:ring-opacity-70",
url == "/dash" ? "bg-sky-600 text-white" : "bg-sky-300 text-black"
].join(" ")}>
Time Selection
Attendance
</button>
</Link>
<Link href="/dash/chgPassword">
@ -61,7 +61,7 @@ export default function DashboardHeader({ url }: { url: string }) {
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
url == "/dash/admin/roster" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
].join(" ")}>
View Roster
Roster
</button>
</Link>
{
@ -71,7 +71,7 @@ export default function DashboardHeader({ url }: { url: string }) {
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
url == "/dash/admin" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
].join(" ")}>
Admin
Settings
</button>
</Link>
<Link href="/dash/admin/rfid">
@ -79,7 +79,7 @@ export default function DashboardHeader({ url }: { url: string }) {
"text-sm md:text-xl px-3 py-2 rounded-lg rounded-b-none focus:ring focus:ring-emerald-200 focus:ring-opacity-70",
url == "/dash/admin/rfid" ? "bg-emerald-600 text-white" : "bg-emerald-200 text-black"
].join(" ")}>
RFID Setup
RFID
</button>
</Link>
</>)

View file

@ -21,9 +21,9 @@ export default function Config() {
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<p className="text-xl font-bold mb-1">Current time periods:</p>
<table className="table-auto border-collapse border w-fit">
<table className="table-auto border-collapse w-fit border-2 border-black">
<thead>
<tr className="*:p-1 *:border">
<tr className="*:p-1 *:border border-b-2 border-b-black">
<th>ID</th>
<th>Name</th>
<th>Start</th>
@ -50,16 +50,16 @@ export default function Config() {
}
<tr className="*:p-1 *:border">
<td>
<input type="number" className="mt-1 block w-16 rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="ID" {...register("id")} />
<input type="number" className="mt-1 block w-16 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" placeholder="ID" {...register("id")} />
</td>
<td>
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="Name" {...register("name")} />
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" placeholder="Name" {...register("name")} />
</td>
<td>
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="Start" {...register("startTime")} />
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" placeholder="Start" {...register("startTime")} />
</td>
<td>
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-70" placeholder="End" {...register("endTime")} />
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" placeholder="End" {...register("endTime")} />
</td>
<td className="text-center">
<button className="p-1 px-2 bg-emerald-600 text-white rounded-lg" type="submit">Add</button>

View file

@ -30,6 +30,7 @@ export default function Users() {
<th>Class</th>
<th>Number</th>
<th>Name</th>
<th>Display Name</th>
<th>RFID</th>
<th>Admin?</th>
<th>Roster Only?</th>
@ -45,6 +46,7 @@ export default function Users() {
<td>{user.class}</td>
<td>{user.number}</td>
<td>{user.name}</td>
<td>{user.dname}</td>
<td>{user.rfid}</td>
<td className="text-sky-600 underline text-center hover:cursor-pointer"
onClick={() => toggleAdmin.mutateAsync(user.username).then(() => users.refetch())}
@ -97,6 +99,11 @@ export default function Users() {
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("name")} />
{errors.name && <span className="text-red-500">{errors.name.message}</span>}
</div>
<div className="mb-5">
<label className="block text-md font-medium text-gray-700 mt-2">Display Name</label>
<input type="text" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-70" {...register("dname")} />
{errors.dname && <span className="text-red-500">{errors.dname.message}</span>}
</div>
<button className="bg-emerald-600 px-3 py-2 rounded text-white focus:ring focus:ring-emerald-200 focus:ring-opacity-70 disabled:bg-emerald-400 mb-5" disabled={addUser.isLoading}>
Add User
</button>

View file

@ -17,6 +17,9 @@ export const env = createEnv({
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
JWT_SECRET: z.string().min(32).default("changeme"),
TZ: z.string().default("UTC"),
RFID_PKEY: z.string().default("changeme"),
},
/**
@ -35,6 +38,9 @@ export const env = createEnv({
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
JWT_SECRET: process.env.JWT_SECRET,
TZ: process.env.TZ,
RFID_PKEY: process.env.RFID_PKEY,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
},
/**

View file

@ -1,15 +1,17 @@
import { type AppType } from "next/app";
import { Noto_Sans } from "next/font/google"
import localFont from 'next/font/local'
import { api } from "~/utils/api";
import "~/styles/globals.css";
const noto = Noto_Sans({ subsets: ["latin"] })
const kai = localFont({ src: "./kaiu.ttf", variable: "--font-kai"})
const MyApp: AppType = ({ Component, pageProps }) => {
return <main className={noto.className}><Component {...pageProps} /></main>;
return <main className={`${kai.variable} ${noto.className}`}><Component {...pageProps} /></main>;
};
export default api.withTRPC(MyApp);

View file

@ -1,29 +1,45 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { z } from 'zod'
import { appRouter } from '~/server/api/root'
import { setLastRfid } from '~/utils/rfid'
import { rfidAttendance, setLastRfid } from '~/utils/rfid'
import { db } from '~/server/db'
import { env } from 'process'
type ResponseData = {
message: string
status: string,
name?: string
}
export default function handler(
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if ("uid" in req.body) {
if (!req.body || !("key" in req.body) || !(req.body.key === process.env.RFID_PKEY)) {
res.status(403).json({ status: 'ERR_BADKEY' });
return;
}
else if ("uid" in req.body) {
try {
z.string().regex(/^[0-9a-f]{8}$/i).parse(req.body.uid);
} catch (e) {
res.status(200).json({ message: 'Invalid UID!' });
res.status(200).json({ status: 'ERR_BADUID' });
return;
}
setLastRfid(req.body.uid);
const caller = appRouter.createCaller({ session: undefined, db, req, res });
res.status(200).json({ message: 'Received!' });
if (rfidAttendance) {
const caller = appRouter.createCaller({ session: undefined, db, req, res });
try {
const resp = await caller.admin.rfidAttendance({ rfid: req.body.uid, key: req.body.key });
res.status(200).json(resp);
} catch (e) {
res.status(500).json({ status: "ERR_INTERNAL" });
return;
}
} else {
res.status(200).json({ status: 'RFID_ATTENDANCE_NOT_ENABLED' });
}
}
else {
res.status(200).json({ message: 'No UID!' });
res.status(400).json({ status: 'ERR_NOUID' });
}
}

View file

@ -10,8 +10,8 @@ export default function Dash() {
const users = api.admin.getAllUsers.useQuery();
const lastRfid = api.admin.getLastRfid.useQuery();
const setUserRfid = api.admin.setUserRfid.useMutation();
const rfidAttendance = api.admin.rfidAttendance.useQuery();
const toggleRfidAttendance = api.admin.toggleRfidAttendance.useMutation();
const rfidAttendance = api.admin.getRfidAttendanceForDash.useQuery();
const toggleRfidAttendance = api.admin.toggleRfidAttendanceForDash.useMutation();
useEffect(() => {
if (isLoggedIn.failureCount > 0) {

View file

@ -1,18 +1,28 @@
import Head from "next/head";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { usePDF } from "react-to-pdf";
import DashboardHeader from "~/components/DashboardHeader";
import { api } from "~/utils/api";
export default function Dash() {
const { push } = useRouter();
const [date, setDate] = useState(new Date());
const [genDate, setGenDate] = useState(new Date(0));
const { toPDF, targetRef: pdfRef } = usePDF({ filename: `入校名單_${date.toLocaleDateString().replace("/", "-")}.pdf` });
const isLoggedIn = api.admin.isLoggedIn.useQuery();
const periods = api.admin.getPeriods.useQuery();
const timePeriods = api.admin.getTimePeriods.useQuery();
const roster = api.admin.getRoster.useQuery({
date: date
});
useEffect(() => {
if (isLoggedIn.failureCount > 0) {
push("/");
}
}
}, [isLoggedIn.failureCount])
if (isLoggedIn.isLoading) return <></>
@ -25,6 +35,86 @@ export default function Dash() {
</Head>
<main className="">
<DashboardHeader url="/dash/admin/roster" />
<div className="m-5">
<div className="flex gap-5 items-end">
<label className="block max-w-72 mb-5">
<span className="text-gray-700">Date</span>
<select className="block w-full mt-1 rounded-md border-gray-300 shadow-sm focus:border-emerald-300 focus:ring focus:ring-emerald-200 focus:ring-opacity-50"
onChange={(evt) => {
setDate(new Date(evt.target.value));
roster.refetch().then(() => {
setGenDate(new Date());
});
}}>
<option value="">Select a date</option>
{
Object.keys(periods.data || {}).map((date) => {
return (<option key={date}>{date}</option>)
})
}
</select>
</label>
<button className="h-fit bg-emerald-600 px-3 py-2 rounded text-white focus:ring focus:ring-emerald-200 focus:ring-opacity-70 disabled:bg-emerald-400 mb-5" disabled={roster.isLoading}
onClick={() => {
toPDF();
}}>
Download PDF
</button>
{
(roster.isLoading) && <div role="status">
<svg aria-hidden="true" className="w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-emerald-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
</svg>
<span className="sr-only">Loading...</span>
</div>
}
</div>
<hr />
<div className="pdf my-5 w-fit p-5" ref={pdfRef}>
<h1 className="text-3xl font-extrabold font-kai mb-2"> Build Season </h1>
<div className="flex mb-2 justify-between font-extrabold font-kai items-end">
<h2 className="text-2xl font-kai">: {date.toLocaleDateString()}</h2>
<h3 className="m-0">: {genDate.toLocaleString()}</h3>
</div>
<table className="table-auto border-collapse border-2 border-black w-fit mt-3 font-kai text-lg">
<thead>
<tr className="*:p-1 *:border border-b-2 border-b-black">
<th></th>
<th></th>
<th></th>
<th></th>
{
timePeriods.data?.map((timePeriods) => {
return (<th key={timePeriods.id}>{timePeriods.name} ({timePeriods.start} ~ {timePeriods.end})</th>)
})
}
</tr>
</thead>
<tbody>
{
roster.data?.map((user) => {
return (<tr key={user.id} className="*:p-1 *:border">
<td>{user.grade}</td>
<td>{user.class}</td>
<td>{user.number}</td>
<td>{user.name}</td>
{
timePeriods.data?.map((timePeriod) => {
if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) {
return (<td key={timePeriod.id}></td>)
} else {
return (<td key={timePeriod.id}></td>)
}
})
}
</tr>)
})
}
</tbody>
</table>
</div>
</div>
</main>
</>)
}

View file

@ -10,9 +10,21 @@ export default function Dash() {
const periods = api.admin.getPeriods.useQuery();
const timePeriods = api.admin.getTimePeriods.useQuery();
const mySelectedPeriods = api.timeSel.getMySelectedPeriods.useQuery();
const myAttendance = api.timeSel.getMyAttendance.useQuery();
const toggleAttendance = api.timeSel.toggleAttendance.useMutation();
const attendTime = api.timeSel.attendTime.useQuery();
const actualAttendTime = api.timeSel.actualAttendTime.useQuery();
let periodCnt = 0;
let attCnt = 0;
let entered = false;
const fuseDateAndTime = (date: string, time: Date) => {
const year = new Date(date).getFullYear();
const month = new Date(date).getMonth();
const day = new Date(date).getDate();
return new Date(year, month, day, time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds());
}
useEffect(() => {
if (isLoggedIn.failureCount > 0) {
@ -31,16 +43,27 @@ export default function Dash() {
<main className="">
<DashboardHeader url="/dash" />
<div className="p-5">
<div className={[
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
(attendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
].join(" ")}>
<div className="text-center text-3xl font-bold">Hours</div>
<div className="flex-grow flex items-center">
<div className="text-center text-6xl font-bold">{attendTime.data}</div>
<div className="flex gap-2">
<div className={[
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
(attendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
].join(" ")}>
<div className="text-center text-2xl font-bold"></div>
<div className="flex-grow flex items-center">
<div className="text-center text-6xl font-bold">{attendTime.data}</div>
</div>
</div>
<div className={[
"border rounded-xl h-32 w-40 p-2 flex items-center justify-center flex-col mb-5",
(actualAttendTime.data || 0) >= 30 ? "bg-emerald-500" : "bg-red-300"
].join(" ")}>
<div className="text-center text-xl font-bold"></div>
<div className="flex-grow flex items-center">
<div className="text-center text-6xl font-bold">{actualAttendTime.data?.toPrecision(2)}</div>
</div>
</div>
</div>
<div className="text-2xl font-bold mb-2">Click cells below to toggle</div>
<div className="text-2xl font-bold mb-2"> ( &gt; 100)</div>
<table className="table-auto border-collapse border-2 border-black w-fit mb-5">
<thead>
<tr className="*:p-1 *:border border-b-2 border-b-black">
@ -56,33 +79,73 @@ export default function Dash() {
{
Object.keys(periods.data || {}).map((date) => {
periodCnt = 0;
entered = false;
return (
<tr className="*:p-1 *:border" key={date}>
<td>{date}</td>
{
timePeriods.data?.map((timePeriod) => {
const thisPeriodId = periods.data![date]![periodCnt]?.id!;
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
periodCnt++;
if (mySelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) {
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: false
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
Will Attend
</td>
// TODO: fix this very ugly code
new Date(new Date(date).setDate(new Date(date).getDate() - 1)).setHours(23, 59, 59, 999) > new Date().getTime() ? (
timePeriods.data?.map((timePeriod) => {
const thisPeriodId = periods.data![date]![periodCnt]?.id!;
if (periods.data![date]![periodCnt]?.timePeriodId == timePeriod.id) {
periodCnt++;
if (mySelectedPeriods.data?.findIndex((period) => period == thisPeriodId) != -1) {
return <td key={timePeriod.id * periodCnt} className="bg-emerald-200 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: false
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
Will Attend
</td>
} else {
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: true
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}></td>
}
} else {
return <td key={timePeriod.id * periodCnt} className="bg-sky-100 hover:cursor-pointer"
onClick={() => toggleAttendance.mutateAsync({
periodId: thisPeriodId || -1,
attendance: true
}).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}></td>
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
}
} else {
return <td key={timePeriod.id * periodCnt} className="bg-gray-400">N/A</td>
}
})
})
) : (
timePeriods.data?.map((timePeriod) => {
periodCnt++;
let data = "";
const thisPeriodStart = new Date(date);
thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0)
const thisPeriodEnd = new Date(date);
thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0)
for (let i = attCnt; i < (myAttendance.data || []).length; i++) {
const thisAtt = myAttendance.data![i];
if (thisAtt?.datetime! < thisPeriodStart) continue;
if (thisAtt?.datetime! > thisPeriodEnd) {
attCnt = i;
break;
}
if (!entered) {
data += `${data !== "" ? " / " : ""}${thisAtt?.datetime!.toLocaleTimeString()} ~ `;
} else {
data += `${thisAtt?.datetime!.toLocaleTimeString()}`;
}
entered = !entered;
}
if (entered && data === "" && periodCnt !== timePeriods.data!.length) {
return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white"></td>
}
if (entered && periodCnt === timePeriods.data!.length) {
return <td key={timePeriod.id * periodCnt} className="bg-yellow-700 text-white">{data}</td>
}
if (data === "") {
return <td key={timePeriod.id * periodCnt} className="bg-gray-500 text-white">Absent</td>
}
return <td key={timePeriod.id * periodCnt} className="bg-green-700 text-white">{data}</td>
})
)
}
</tr>
)

BIN
src/pages/kaiu.ttf Normal file

Binary file not shown.

View file

@ -121,6 +121,7 @@ export const adminRouter = createTRPCRouter({
select: {
username: true,
name: true,
dname: true,
grade: true,
class: true,
number: true,
@ -185,6 +186,7 @@ export const adminRouter = createTRPCRouter({
data: {
username: input.username,
name: input.name,
dname: input.dname,
grade: input.grade,
class: input.class,
number: input.number,
@ -203,11 +205,11 @@ export const adminRouter = createTRPCRouter({
}).then((periods) => {
const groupedPeriods: { [key: string]: Period[] } = {};
periods.forEach((period) => {
const utcDate = period.date.toLocaleDateString();
if (groupedPeriods[utcDate] === undefined) {
groupedPeriods[utcDate] = [];
const dateStr = period.date.toLocaleDateString();
if (groupedPeriods[dateStr] === undefined) {
groupedPeriods[dateStr] = [];
}
groupedPeriods[utcDate]?.push(period);
groupedPeriods[dateStr]?.push(period);
});
return groupedPeriods;
}).catch((err) => {
@ -309,14 +311,62 @@ export const adminRouter = createTRPCRouter({
},
})
}),
rfidAttendance: adminProcedure
getRfidAttendanceForDash: adminProcedure
.query(() => {
return {
status: "success",
rfidAttendance: rfidAttendance,
}
}),
toggleRfidAttendance: adminProcedure
rfidAttendance: publicProcedure
.input(z.object({ rfid: z.string().regex(/^[0-9a-f]{8}$/i), key: z.string() }))
.query(async ({ input, ctx }) => {
if (input.key !== process.env.RFID_PKEY) {
return {
status: "ERR_BADKEY",
}
}
return await ctx.db.attendance.create({
data: {
datetime: new Date(),
user: {
connect: {
rfid: input.rfid,
},
},
}
}).then(async () => {
const records = await ctx.db.attendance.findMany({
where: {
datetime: {
gte: new Date(new Date().setHours(0, 0, 0, 0)),
lt: new Date(new Date().setHours(23, 59, 59, 999)),
},
user: {
rfid: input.rfid,
},
},
select: {
user: {
select: {
dname: true,
},
},
},
orderBy: {
datetime: "asc",
},
})
return {
status: records.length % 2 ? "WELCOME" : "GOODBYE",
name: records[records.length - 1]!.user.dname,
}
}).catch((err) => {
return {
status: "ERR_INTERNAL",
}
})
}),
toggleRfidAttendanceForDash: adminProcedure
.mutation(() => {
setRfidAttendance(!rfidAttendance);
return {
@ -324,4 +374,47 @@ export const adminRouter = createTRPCRouter({
message: "RFID attendance toggled.",
};
}),
getRoster: adminProcedure
.input(z.object({ date: z.date()}))
.query(async ({ input, ctx }) => {
return await ctx.db.user.findMany({
where: {
periods: {
some: {
date: input.date,
},
}
},
select: {
id: true,
name: true,
grade: true,
class: true,
number: true,
periods: {
where: {
date: input.date,
},
select: {
timePeriod: {
select: {
id: true,
},
},
},
},
},
orderBy: [
{
grade: "asc",
},
{
class: "asc",
},
{
number: "asc",
},
]
})
}),
});

View file

@ -26,6 +26,7 @@ export const timeSelRouter = createTRPCRouter({
}))
.mutation(async ({ ctx, input }) => {
try {
if (input.attendance) {
await ctx.db.period.update({
where: { id: input.periodId },
@ -86,4 +87,93 @@ export const timeSelRouter = createTRPCRouter({
})
return total;
}),
getMyAttendance: loggedInProcedure
.query(async ({ ctx }) => {
return await ctx.db.attendance.findMany({
where: {
user: {
username: ctx.session?.username,
}
},
select: {
datetime: true,
},
orderBy: {
datetime: "asc",
},
});
}),
actualAttendTime: loggedInProcedure
.query(async ({ ctx }) => {
const attendance = await ctx.db.attendance.findMany({
where: {
user: {
username: ctx.session?.username,
}
},
select: {
datetime: true,
},
orderBy: {
datetime: "asc",
},
});
const dates = await ctx.db.period.findMany({
select: {
date: true,
},
orderBy: {
date: "asc",
}
}).then((periods) => {
const dates: string[] = [];
periods.forEach(period => {
const dateStr = period.date.toLocaleDateString();
if (!dates.includes(dateStr)) dates.push(dateStr);
})
return dates;
})
const timePeriods = await ctx.db.timePeriod.findMany({
select: {
start: true,
end: true,
},
orderBy: [
{ start: "asc" },
{ end: "asc" },
]
});
let attCnt = 0;
let calculator = 0.0;
dates.forEach((date) => {
let periodCnt = 0;
let entered = false;
timePeriods.forEach((timePeriod) => {
periodCnt++;
const thisPeriodStart = new Date(date);
thisPeriodStart.setHours(parseInt(timePeriod.start.split(":")[0]!), parseInt(timePeriod.start.split(":")[1]!), 0, 0)
const thisPeriodEnd = new Date(date);
thisPeriodEnd.setHours(parseInt(timePeriod.end.split(":")[0]!), parseInt(timePeriod.end.split(":")[1]!), 0, 0)
for (let i = attCnt; i < attendance.length; i++) {
const thisAtt = attendance[i]!;
if (thisAtt.datetime < thisPeriodStart) continue;
if (thisAtt.datetime > thisPeriodEnd) {
attCnt = i;
break;
}
if (!entered) {
calculator -= thisAtt.datetime.getTime();
} else {
calculator += thisAtt.datetime.getTime();
}
entered = !entered;
}
if (entered && periodCnt === timePeriods.length) {
calculator += thisPeriodEnd.getTime();
}
})
})
return calculator / 1000 / 60 / 60;
}),
});

View file

@ -29,6 +29,7 @@ export const AddTimePeriodSchema = z.object({
export const AddUserSchema = z.object({
username: z.string().min(1, { message: "Username cannot be empty." }),
name: z.string().min(1, { message: "Name cannot be empty." }),
dname: z.string().min(1, { message: "Display name cannot be empty." }).regex(/^[a-z0-9_\-]+$/i, { message: "Display name must only contain letters, numbers, underscores, and dashes." }),
grade: z.coerce.number().int().nullable(),
class: z.string().nullable(),
number: z.coerce.number().int().nullable(),

View file

@ -7,6 +7,7 @@ export default {
extend: {
fontFamily: {
sans: ["Lato", "var(--font-sans)", ...fontFamily.sans],
kai: ["var(--font-kai)", "Lato", ...fontFamily.serif],
},
},
},

145
yarn.lock
View file

@ -12,6 +12,13 @@
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0":
version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d"
integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==
dependencies:
regenerator-runtime "^0.14.0"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -346,6 +353,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
"@types/raf@^3.4.0":
version "3.4.3"
resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
"@types/react-dom@^18.2.15":
version "18.2.18"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.18.tgz#16946e6cd43971256d874bc3d0a72074bb8571dd"
@ -547,6 +559,11 @@ array-union@^2.1.0:
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
atob@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
autoprefixer@^10.4.14:
version "10.4.16"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8"
@ -564,6 +581,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
bcrypt@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2"
@ -602,6 +624,11 @@ browserslist@^4.21.10:
node-releases "^2.0.14"
update-browserslist-db "^1.0.13"
btoa@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
@ -629,6 +656,20 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.300015
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz#b4e5c1fa786f733ab78fc70f592df6b3f23244ca"
integrity sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==
canvg@^3.0.6:
version "3.0.10"
resolved "https://registry.yarnpkg.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==
dependencies:
"@babel/runtime" "^7.12.5"
"@types/raf" "^3.4.0"
core-js "^3.8.3"
raf "^3.4.1"
regenerator-runtime "^0.13.7"
rgbcolor "^1.0.1"
stackblur-canvas "^2.0.0"
svg-pathdata "^6.0.3"
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -701,6 +742,11 @@ copy-anything@^3.0.2:
dependencies:
is-what "^4.1.8"
core-js@^3.6.0, core-js@^3.8.3:
version "3.35.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.35.0.tgz#58e651688484f83c34196ca13f099574ee53d6b4"
integrity sha512-ntakECeqg81KqMueeGJ79Q5ZgQNR+6eaE8sxGCx62zMbAIj65q+uYvatToew3m6eAGdU4gNZwpZ34NMe4GYswg==
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -710,6 +756,13 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
@ -766,6 +819,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dompurify@^2.2.0:
version "2.4.7"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc"
integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
@ -916,6 +974,11 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
fflate@^0.4.8:
version "0.4.8"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.4.8.tgz#f90b82aefbd8ac174213abb338bd7ef848f0f5ae"
integrity sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -1095,6 +1158,14 @@ hasown@^2.0.0:
dependencies:
function-bind "^1.1.2"
html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
https-proxy-agent@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
@ -1233,6 +1304,21 @@ jsonwebtoken@^9.0.2:
ms "^2.1.1"
semver "^7.5.4"
jspdf@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/jspdf/-/jspdf-2.5.1.tgz#00c85250abf5447a05f3b32ab9935ab4a56592cc"
integrity sha512-hXObxz7ZqoyhxET78+XR34Xu2qFGrJJ2I2bE5w4SM8eFaFEkW2xcGRVUss360fYelwRSid/jT078kbNvmoW0QA==
dependencies:
"@babel/runtime" "^7.14.0"
atob "^2.1.2"
btoa "^1.2.1"
fflate "^0.4.8"
optionalDependencies:
canvg "^3.0.6"
core-js "^3.6.0"
dompurify "^2.2.0"
html2canvas "^1.0.0-rc.5"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@ -1570,6 +1656,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
@ -1684,6 +1775,13 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
dependencies:
performance-now "^2.1.0"
react-dom@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@ -1702,6 +1800,14 @@ react-ssr-prepass@^1.5.0:
resolved "https://registry.yarnpkg.com/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz#bc4ca7fcb52365e6aea11cc254a3d1bdcbd030c5"
integrity sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==
react-to-pdf@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-to-pdf/-/react-to-pdf-1.0.1.tgz#a9a54ba9af9df4213078bf34eb2ffabb752b7263"
integrity sha512-ZsIkY6Z5gg3oBhMbWfl+tYwQ12vpPuuAzvCv+MnXchO8l08tElzRkBNAXxfbQNG/EDOHgE5EvWBlvE7ypt/y9A==
dependencies:
html2canvas "^1.4.1"
jspdf "^2.5.1"
react@18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@ -1732,6 +1838,16 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
regenerator-runtime@^0.13.7:
version "0.13.11"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -1751,6 +1867,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rgbcolor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -1821,6 +1942,11 @@ source-map-js@^1.0.2:
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
stackblur-canvas@^2.0.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/stackblur-canvas/-/stackblur-canvas-2.6.0.tgz#7876bab4ea99bfc97b69ce662614d7a1afb2d71b"
integrity sha512-8S1aIA+UoF6erJYnglGPug6MaHYGo1Ot7h5fuXx4fUPvcvQfcdw2o/ppCse63+eZf8PPidSu4v1JnmEVtEDnpg==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
@ -1893,6 +2019,11 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svg-pathdata@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
tailwindcss@^3.3.5:
version "3.4.0"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.0.tgz#045a9c474e6885ebd0436354e611a76af1c76839"
@ -1933,6 +2064,13 @@ tar@^6.1.11:
mkdirp "^1.0.3"
yallist "^4.0.0"
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -2026,6 +2164,13 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
watchpack@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"