feat: initial commit

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-02-18 11:54:42 +08:00
parent 23fb6f163d
commit 0a7d69e86b
167 changed files with 23952 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
import type { EventInfo } from '../events/types';
import { isNil } from 'lodash-es';
import { useState } from 'react';
import { useCheckinCode } from '@/hooks/data/useCheckinCode';
import { Dialog } from '../ui/dialog';
import { CheckinQrDialogError } from './checkin-qr.dialog.error';
import { CheckinQrDialogSkeleton } from './checkin-qr.dialog.skeleton';
import { CheckinQrDialogView } from './checkin-qr.dialog.view';
export function CheckinQrDialogContainer({ event, children }: { event: EventInfo; children: React.ReactNode }) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { data, isLoading, isError } = useCheckinCode(event.eventId, isDialogOpen);
return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
{children}
{isLoading && (
<CheckinQrDialogSkeleton />
)}
{isError && (
<CheckinQrDialogError />
)}
{!isLoading && !isError && !isNil(data) && (
<CheckinQrDialogView checkinCode={String(data.data!.checkin_code)} />
)}
</Dialog>
);
}

View File

@@ -0,0 +1,18 @@
import { TicketX } from 'lucide-react';
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/dialog';
export function CheckinQrDialogError() {
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
<div className="flex justify-center my-12">
<TicketX size={100} className="stroke-[1.5]" />
</div>
</DialogDescription>
</DialogHeader>
</DialogContent>
);
}

View File

@@ -0,0 +1,23 @@
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { QRCode } from '../ui/shadcn-io/qr-code';
export function CheckinQrDialogSkeleton() {
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
<QRCode data="welcome to join the conference" className="size-60 blur-sm" />
</div>
<DialogFooter>
<div className="flex flex-1 items-center ml-2 text-2xl text-primary/80 justify-center">
...
</div>
</DialogFooter>
</DialogContent>
);
}

View File

@@ -0,0 +1,23 @@
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
import { QRCode } from '../ui/shadcn-io/qr-code';
export function CheckinQrDialogView({ checkinCode }: { checkinCode: string }) {
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
<QRCode data={checkinCode} className="size-60" />
</div>
<DialogFooter>
<div className="flex flex-1 items-center ml-2 font-mono text-2xl tracking-widest text-primary/80 justify-center">
{checkinCode}
</div>
</DialogFooter>
</DialogContent>
);
}

View File

@@ -0,0 +1,19 @@
import { useCheckinSubmit } from '@/hooks/data/useCheckinSubmit';
import { useUserInfo } from '@/hooks/data/useUserInfo';
import { CheckinScannerNavView } from './checkin-scanner-nav.view';
export function CheckinScannerNavContainer() {
const { data } = useUserInfo();
const { mutate, isPending } = useCheckinSubmit();
if ((data.data?.permission_level ?? 0) <= 20) {
return null;
}
return (
<CheckinScannerNavView
onScan={code => mutate({ body: { checkin_code: code } })}
isPending={isPending}
/>
);
}

View File

@@ -0,0 +1,40 @@
import { IconScan } from '@tabler/icons-react';
import { useState } from 'react';
import { Dialog, DialogTrigger } from '@/components/ui/dialog';
import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { CheckinScannerDialogView } from './checkin-scanner.dialog.view';
interface CheckinScannerNavViewProps {
onScan: (code: string) => void;
isPending: boolean;
}
export function CheckinScannerNavView({ onScan, isPending }: CheckinScannerNavViewProps) {
const [open, setOpen] = useState(false);
const handleScan = (value: string) => {
if (isPending)
return;
if (!/^\d{6}$/.test(value)) {
return;
}
onScan(value);
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<SidebarMenuItem>
<DialogTrigger asChild>
<SidebarMenuButton tooltip="扫码签到">
<IconScan />
<span></span>
</SidebarMenuButton>
</DialogTrigger>
</SidebarMenuItem>
<CheckinScannerDialogView onScan={handleScan} />
</Dialog>
);
}

View File

@@ -0,0 +1,50 @@
import { Scanner } from '@yudiel/react-qr-scanner';
import { REGEXP_ONLY_DIGITS } from 'input-otp';
import { DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
import { InputOTP, InputOTPGroup, InputOTPSlot } from '../ui/input-otp';
export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) => void }) {
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-6 items-center">
<Scanner
onScan={(result) => {
if (result.length > 0) {
onScan(result[0].rawValue);
}
}}
onError={(error) => { throw error; }}
/>
<div className="relative w-full flex items-center justify-center">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
</span>
</div>
</div>
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS}
onComplete={(value: string) => onScan(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</div>
</DialogContent>
);
}