-
+
|
-
+
|
-
+
|
-
+
|
diff --git a/src/components/admin/Users.tsx b/src/components/admin/Users.tsx
index 66e3e3e..64652bb 100644
--- a/src/components/admin/Users.tsx
+++ b/src/components/admin/Users.tsx
@@ -30,6 +30,7 @@ export default function Users() {
| Class |
Number |
Name |
+ Display Name |
RFID |
Admin? |
Roster Only? |
@@ -45,6 +46,7 @@ export default function Users() {
{user.class} |
{user.number} |
{user.name} |
+ {user.dname} |
{user.rfid} |
toggleAdmin.mutateAsync(user.username).then(() => users.refetch())}
@@ -97,6 +99,11 @@ export default function Users() {
{errors.name && {errors.name.message}}
+
+
+
+ {errors.dname && {errors.dname.message}}
+
diff --git a/src/env.js b/src/env.js
index 1107bd0..0307a33 100644
--- a/src/env.js
+++ b/src/env.js
@@ -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,
},
/**
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 3c11075..fff7f4c 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -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 ;
+ return ;
};
export default api.withTRPC(MyApp);
diff --git a/src/pages/api/rfid.ts b/src/pages/api/rfid.ts
index 6a49f9b..5dde305 100644
--- a/src/pages/api/rfid.ts
+++ b/src/pages/api/rfid.ts
@@ -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
) {
- 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' });
}
}
diff --git a/src/pages/dash/admin/rfid.tsx b/src/pages/dash/admin/rfid.tsx
index 8dacfde..0e20301 100644
--- a/src/pages/dash/admin/rfid.tsx
+++ b/src/pages/dash/admin/rfid.tsx
@@ -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) {
diff --git a/src/pages/dash/admin/roster.tsx b/src/pages/dash/admin/roster.tsx
index 20e7a93..b7e57ef 100644
--- a/src/pages/dash/admin/roster.tsx
+++ b/src/pages/dash/admin/roster.tsx
@@ -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() {
+
+
+
+
+ {
+ (roster.isLoading) &&
+ }
+
+
+
+ 機器人研究社 Build Season 入校名單
+
+ 日期: {date.toLocaleDateString()}
+ 列印時間: {genDate.toLocaleString()}
+
+
+
+
+ 年級 |
+ 班級 |
+ 座號 |
+ 姓名 |
+ {
+ timePeriods.data?.map((timePeriods) => {
+ return ({timePeriods.name} ({timePeriods.start} ~ {timePeriods.end}) | )
+ })
+ }
+
+
+
+ {
+ roster.data?.map((user) => {
+ return (
+ {user.grade} |
+ {user.class} |
+ {user.number} |
+ {user.name} |
+ {
+ timePeriods.data?.map((timePeriod) => {
+ if (user.periods.find((period) => period.timePeriod.id === timePeriod.id)) {
+ return (✔ | )
+ } else {
+ return ( | )
+ }
+ })
+ }
+ )
+ })
+ }
+
+
+
+
>)
}
\ No newline at end of file
diff --git a/src/pages/dash/index.tsx b/src/pages/dash/index.tsx
index 80db379..b4003ff 100644
--- a/src/pages/dash/index.tsx
+++ b/src/pages/dash/index.tsx
@@ -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() {
- = 30 ? "bg-emerald-500" : "bg-red-300"
- ].join(" ")}>
- Hours
-
- {attendTime.data}
+
+ = 30 ? "bg-emerald-500" : "bg-red-300"
+ ].join(" ")}>
+ 已選取時數
+
+
+ = 30 ? "bg-emerald-500" : "bg-red-300"
+ ].join(" ")}>
+ 實際出席時數
+
+ {actualAttendTime.data?.toPrecision(2)}
+
- Click cells below to toggle
+ 請點選下方切換狀態 (上方綠燈 > 100)
@@ -56,33 +79,73 @@ export default function Dash() {
{
Object.keys(periods.data || {}).map((date) => {
periodCnt = 0;
+ entered = false;
return (
{date} |
{
- 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 toggleAttendance.mutateAsync({
- periodId: thisPeriodId || -1,
- attendance: false
- }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
- Will Attend
- |
+ // 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 toggleAttendance.mutateAsync({
+ periodId: thisPeriodId || -1,
+ attendance: false
+ }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}>
+ Will Attend
+ |
+ } else {
+ return toggleAttendance.mutateAsync({
+ periodId: thisPeriodId || -1,
+ attendance: true
+ }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}> |
+ }
} else {
- return toggleAttendance.mutateAsync({
- periodId: thisPeriodId || -1,
- attendance: true
- }).then(() => mySelectedPeriods.refetch()).then(() => attendTime.refetch())}> |
+ return N/A |
}
- } else {
- return N/A |
- }
- })
+ })
+ ) : (
+ 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 |
+ }
+ if (entered && periodCnt === timePeriods.data!.length) {
+ return {data} |
+ }
+ if (data === "") {
+ return Absent |
+ }
+ return {data} |
+ })
+ )
}
)
diff --git a/src/pages/kaiu.ttf b/src/pages/kaiu.ttf
new file mode 100644
index 0000000..d8a755f
Binary files /dev/null and b/src/pages/kaiu.ttf differ
diff --git a/src/server/api/routers/admin.ts b/src/server/api/routers/admin.ts
index 6ac0d8b..d32f777 100644
--- a/src/server/api/routers/admin.ts
+++ b/src/server/api/routers/admin.ts
@@ -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",
+ },
+ ]
+ })
+ }),
});
diff --git a/src/server/api/routers/time-sel.ts b/src/server/api/routers/time-sel.ts
index 97e8896..f0616b9 100644
--- a/src/server/api/routers/time-sel.ts
+++ b/src/server/api/routers/time-sel.ts
@@ -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;
+ }),
});
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 527fa68..7d4aee3 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -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(),
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 328fe7f..d34a398 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -7,6 +7,7 @@ export default {
extend: {
fontFamily: {
sans: ["Lato", "var(--font-sans)", ...fontFamily.sans],
+ kai: ["var(--font-kai)", "Lato", ...fontFamily.serif],
},
},
},
diff --git a/yarn.lock b/yarn.lock
index ec3fea0..83028b6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
|