Compare commits
2 Commits
170afb4a3b
...
11388c4f35
| Author | SHA1 | Date | |
|---|---|---|---|
|
11388c4f35
|
|||
|
b4e32d5a6d
|
32
client/cms/.sisyphus/drafts/checkin_logic.md
Normal file
32
client/cms/.sisyphus/drafts/checkin_logic.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Draft: Implement Check-in Logic
|
||||||
|
|
||||||
|
## Requirements (User)
|
||||||
|
- **Input**: 6-digit number from scanner.
|
||||||
|
- **Action**: Call `/event/checkin/submit` (`postEventCheckinSubmit`).
|
||||||
|
- **Feedback**: Toaster (success/failure) using `sonner`.
|
||||||
|
|
||||||
|
## Research Questions
|
||||||
|
1. [Resolved] API Client: `postEventCheckinSubmit` exists.
|
||||||
|
2. [Pending] API Parameters: Need to verify `PostEventCheckinSubmitData`.
|
||||||
|
3. [Resolved] Toaster Library: `sonner` (`toast.success`, `toast.error`).
|
||||||
|
|
||||||
|
## Technical Decisions
|
||||||
|
- **Logic Placement**: `CheckinScannerNavContainer`.
|
||||||
|
- **State Management**: `useMutation` from `@tanstack/react-query`.
|
||||||
|
- **Validation**: Regex `^\d{6}$` for 6-digit number.
|
||||||
|
- **Error Handling**: `onError` in mutation -> `toast.error`.
|
||||||
|
- **Success Handling**: `onSuccess` in mutation -> `toast.success`.
|
||||||
|
|
||||||
|
## Code Snippets
|
||||||
|
```typescript
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { postEventCheckinSubmit } from '@/client/sdk.gen';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
// In container
|
||||||
|
const { mutate } = useMutation({
|
||||||
|
mutationFn: (code: string) => postEventCheckinSubmit({ body: { code } }),
|
||||||
|
onSuccess: () => toast.success('签到成功'),
|
||||||
|
onError: () => toast.error('签到失败'),
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
### useCheckinSubmit Hook
|
||||||
|
- Created `src/hooks/data/useCheckinSubmit.ts` using `postEventCheckinSubmitMutation` from `@/client/@tanstack/react-query.gen`.
|
||||||
|
- Integrated `sonner` for success and error toasts.
|
||||||
|
- Followed the pattern from `useJoinEvent.ts`.
|
||||||
|
|
||||||
|
### CheckinScannerNav Implementation
|
||||||
|
- `CheckinScannerNavView` validation logic implemented with regex `^\d{6}$`.
|
||||||
|
- `CheckinScannerNavContainer` connects the hook to the view.
|
||||||
|
- Type checking passed with `bun tsc -b`.
|
||||||
204
client/cms/.sisyphus/plans/implement-checkin-logic.md
Normal file
204
client/cms/.sisyphus/plans/implement-checkin-logic.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# Plan: Implement Check-in Logic
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
> **Quick Summary**: Connect the scanner to the backend check-in API. When a 6-digit code is scanned, submit it to `/event/checkin/submit`. Show success/error toasts.
|
||||||
|
>
|
||||||
|
> **Deliverables**:
|
||||||
|
> - Updated `CheckinScannerNavContainer` with mutation logic.
|
||||||
|
> - Integration with `sonner` for user feedback.
|
||||||
|
> - Proper parameter mapping (`checkin_code`).
|
||||||
|
>
|
||||||
|
> **Estimated Effort**: Short
|
||||||
|
> **Parallel Execution**: NO - sequential implementation.
|
||||||
|
> **Critical Path**: Implement Mutation → Update View Integration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Original Request
|
||||||
|
"扫码器扫到的如果是6位数字,使用/event/checkin/submit接口进行签到,成功/失败都弹出toaster提示。"
|
||||||
|
|
||||||
|
### Interview Summary
|
||||||
|
**Key Discussions**:
|
||||||
|
- **API**: `postEventCheckinSubmit` is the correct client function.
|
||||||
|
- **Parameters**: API expects `checkin_code` in the body.
|
||||||
|
- **Input**: "6位数字" implies regex validation `^\d{6}$`.
|
||||||
|
- **Feedback**: Use `sonner` (`toast.success`, `toast.error`).
|
||||||
|
|
||||||
|
**Metis Review Findings**:
|
||||||
|
- **Critical Fix**: Ensure parameter name is `checkin_code`, not `code`.
|
||||||
|
- **UX**: Disable scanning while `isPending` to prevent double submissions.
|
||||||
|
- **Error Handling**: Use generic "签到失败" for errors unless specific message available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
### Core Objective
|
||||||
|
Make the check-in scanner functional by connecting it to the backend.
|
||||||
|
|
||||||
|
### Concrete Deliverables
|
||||||
|
- `src/components/checkin/checkin-scanner-nav.container.tsx`: Updated with `useMutation`.
|
||||||
|
- `src/components/checkin/checkin-scanner-nav.view.tsx`: Updated to receive `isPending` prop and handle scan events.
|
||||||
|
|
||||||
|
### Definition of Done
|
||||||
|
- [ ] Scanning "123456" calls API with `{"checkin_code": "123456"}`.
|
||||||
|
- [ ] Success response shows "签到成功" toast.
|
||||||
|
- [ ] Error response shows "签到失败" toast.
|
||||||
|
- [ ] Scanner ignores non-6-digit inputs.
|
||||||
|
- [ ] Scanner pauses/ignores input while API is pending.
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- Regex validation: `^\d{6}$`.
|
||||||
|
- `checkin_code` parameter mapping.
|
||||||
|
- Toaster feedback.
|
||||||
|
|
||||||
|
### Must NOT Have (Guardrails)
|
||||||
|
- Do NOT change the existing permission logic in the container.
|
||||||
|
- Do NOT remove the Dialog wrapping.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy (MANDATORY)
|
||||||
|
|
||||||
|
> **UNIVERSAL RULE: ZERO HUMAN INTERVENTION**
|
||||||
|
> ALL tasks in this plan MUST be verifiable WITHOUT any human action.
|
||||||
|
|
||||||
|
### Test Decision
|
||||||
|
- **Infrastructure exists**: YES (Playwright).
|
||||||
|
- **Automated tests**: YES (Playwright API mocking).
|
||||||
|
|
||||||
|
### Agent-Executed QA Scenarios (MANDATORY)
|
||||||
|
|
||||||
|
**Scenario 1: Successful Check-in**
|
||||||
|
- **Tool**: Playwright
|
||||||
|
- **Steps**:
|
||||||
|
1. Mock `/event/checkin/submit` to return 200 OK.
|
||||||
|
2. Simulate scan event with "123456".
|
||||||
|
3. Assert API called with correct body.
|
||||||
|
4. Assert "签到成功" toast visible.
|
||||||
|
|
||||||
|
**Scenario 2: Failed Check-in**
|
||||||
|
- **Tool**: Playwright
|
||||||
|
- **Steps**:
|
||||||
|
1. Mock `/event/checkin/submit` to return 400 Bad Request.
|
||||||
|
2. Simulate scan event with "123456".
|
||||||
|
3. Assert "签到失败" toast visible.
|
||||||
|
|
||||||
|
**Scenario 3: Invalid Input**
|
||||||
|
- **Tool**: Playwright (if view exposes logic) or Unit Test
|
||||||
|
- **Steps**:
|
||||||
|
1. Simulate scan event with "ABC".
|
||||||
|
2. Assert API NOT called.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Parallel Execution Waves
|
||||||
|
|
||||||
|
```
|
||||||
|
Wave 1:
|
||||||
|
├── Task 1: Create Data Hook
|
||||||
|
└── Task 2: Implement Container & View Logic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. Create Data Hook
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `src/hooks/data/useCheckinSubmit.ts`.
|
||||||
|
- Import `useMutation` from `@tanstack/react-query`.
|
||||||
|
- Import `postEventCheckinSubmitMutation` from `@/client/@tanstack/react-query.gen`.
|
||||||
|
- Import `toast` from `sonner`.
|
||||||
|
- Export `useCheckinSubmit` hook that returns the mutation.
|
||||||
|
- Use `...postEventCheckinSubmitMutation()` pattern.
|
||||||
|
- On success: `toast.success('签到成功')`.
|
||||||
|
- On error: `toast.error('签到失败')`.
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: [`frontend-ui-ux`]
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `src/hooks/data/useJoinEvent.ts` (Pattern reference)
|
||||||
|
- `src/client/@tanstack/react-query.gen.ts`
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Hook uses `@hey-api` pattern.
|
||||||
|
- [ ] Toasts are configured.
|
||||||
|
|
||||||
|
- [x] 2. Implement Container & View Logic
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Update `src/components/checkin/checkin-scanner-nav.container.tsx`:
|
||||||
|
- Import `useCheckinSubmit` from `@/hooks/data/useCheckinSubmit`.
|
||||||
|
- Use the hook to get `mutate` and `isPending`.
|
||||||
|
- Pass `handleScan` (wrapper calling mutate with `{ body: { checkin_code: code } }`) and `isPending` to View.
|
||||||
|
- Update `src/components/checkin/checkin-scanner-nav.view.tsx`:
|
||||||
|
- Accept `onScan` and `isPending` props.
|
||||||
|
- Inside internal `handleScan`, check regex `^\d{6}$`.
|
||||||
|
- If valid and !isPending, call prop `onScan`.
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `visual-engineering`
|
||||||
|
- **Skills**: [`frontend-ui-ux`]
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
- **References**:
|
||||||
|
- `src/hooks/data/useCheckinSubmit.ts` (Dependency)
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Container uses the new hook.
|
||||||
|
- [ ] View logic validates regex.
|
||||||
|
|
||||||
|
- [ ] 2. Update Playwright Verification
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Update `tests/checkin-scanner.spec.ts`.
|
||||||
|
- Add test case for successful check-in (mock API success).
|
||||||
|
- Add test case for failed check-in (mock API failure).
|
||||||
|
- Verify toaster appearance.
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: [`playwright`]
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
- **Blocked By**: Task 1
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `tests/checkin-scanner.spec.ts`
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Tests pass.
|
||||||
|
|
||||||
|
**Agent-Executed QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: Run Updated Tests
|
||||||
|
Tool: Bash
|
||||||
|
Steps:
|
||||||
|
1. npx playwright test tests/checkin-scanner.spec.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Final Checklist
|
||||||
|
- [ ] API integration complete.
|
||||||
|
- [ ] Regex validation matches `^\d{6}$`.
|
||||||
|
- [ ] User feedback (toasts) functional.
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
"culori": "^4.0.2",
|
"culori": "^4.0.2",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"immer": "^11.1.0",
|
"immer": "^11.1.0",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
14
client/cms/pnpm-lock.yaml
generated
14
client/cms/pnpm-lock.yaml
generated
@@ -134,6 +134,9 @@ importers:
|
|||||||
immer:
|
immer:
|
||||||
specifier: ^11.1.0
|
specifier: ^11.1.0
|
||||||
version: 11.1.3
|
version: 11.1.3
|
||||||
|
input-otp:
|
||||||
|
specifier: ^1.4.2
|
||||||
|
version: 1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
lodash-es:
|
lodash-es:
|
||||||
specifier: ^4.17.22
|
specifier: ^4.17.22
|
||||||
version: 4.17.22
|
version: 4.17.22
|
||||||
@@ -3931,6 +3934,12 @@ packages:
|
|||||||
inline-style-parser@0.2.7:
|
inline-style-parser@0.2.7:
|
||||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||||
|
|
||||||
|
input-otp@1.4.2:
|
||||||
|
resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
|
||||||
internmap@2.0.3:
|
internmap@2.0.3:
|
||||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -9417,6 +9426,11 @@ snapshots:
|
|||||||
|
|
||||||
inline-style-parser@0.2.7: {}
|
inline-style-parser@0.2.7: {}
|
||||||
|
|
||||||
|
input-otp@1.4.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.3
|
||||||
|
react-dom: 19.2.3(react@19.2.3)
|
||||||
|
|
||||||
internmap@2.0.3: {}
|
internmap@2.0.3: {}
|
||||||
|
|
||||||
is-alphabetical@2.0.1: {}
|
is-alphabetical@2.0.1: {}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
||||||
|
import { useCheckinSubmit } from '@/hooks/data/useCheckinSubmit';
|
||||||
import { CheckinScannerNavView } from './checkin-scanner-nav.view';
|
import { CheckinScannerNavView } from './checkin-scanner-nav.view';
|
||||||
|
|
||||||
export function CheckinScannerNavContainer() {
|
export function CheckinScannerNavContainer() {
|
||||||
const { data } = useUserInfo();
|
const { data } = useUserInfo();
|
||||||
|
const { mutate, isPending } = useCheckinSubmit();
|
||||||
|
|
||||||
if ((data.data?.permission_level ?? 0) <= 20) {
|
if ((data.data?.permission_level ?? 0) <= 20) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CheckinScannerNavView />;
|
return (
|
||||||
|
<CheckinScannerNavView
|
||||||
|
onScan={(code) => mutate({ body: { checkin_code: code } })}
|
||||||
|
isPending={isPending}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,22 @@ import { Dialog, DialogTrigger } from '@/components/ui/dialog';
|
|||||||
import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
import { SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
|
||||||
import { CheckinScannerDialogView } from './checkin-scanner.dialog.view';
|
import { CheckinScannerDialogView } from './checkin-scanner.dialog.view';
|
||||||
|
|
||||||
export function CheckinScannerNavView() {
|
interface CheckinScannerNavViewProps {
|
||||||
|
onScan: (code: string) => void;
|
||||||
|
isPending: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckinScannerNavView({ onScan, isPending }: CheckinScannerNavViewProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleScan = (value: string) => {
|
const handleScan = (value: string) => {
|
||||||
console.log('Scanned:', value);
|
if (isPending) return;
|
||||||
|
|
||||||
|
if (!/^\d{6}$/.test(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onScan(value);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Scanner } from '@yudiel/react-qr-scanner';
|
import { Scanner } from '@yudiel/react-qr-scanner';
|
||||||
|
import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
||||||
import { DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
|
import { DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||||
|
import { InputOTP, InputOTPGroup, InputOTPSlot } from '../ui/input-otp';
|
||||||
|
|
||||||
export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) => void }) {
|
export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>扫描签到码</DialogTitle>
|
<DialogTitle>扫描签到码</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-6 items-center">
|
||||||
<Scanner
|
<Scanner
|
||||||
onScan={(result) => {
|
onScan={(result) => {
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
@@ -14,7 +18,33 @@ export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) =
|
|||||||
}}
|
}}
|
||||||
onError={(error) => { throw error; }}
|
onError={(error) => { throw error; }}
|
||||||
/>
|
/>
|
||||||
</DialogHeader>
|
|
||||||
|
<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>
|
</DialogContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
75
client/cms/src/components/ui/input-otp.tsx
Normal file
75
client/cms/src/components/ui/input-otp.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { OTPInput, OTPInputContext } from "input-otp"
|
||||||
|
import { MinusIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function InputOTP({
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof OTPInput> & {
|
||||||
|
containerClassName?: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<OTPInput
|
||||||
|
data-slot="input-otp"
|
||||||
|
containerClassName={cn(
|
||||||
|
"flex items-center gap-2 has-disabled:opacity-50",
|
||||||
|
containerClassName
|
||||||
|
)}
|
||||||
|
className={cn("disabled:cursor-not-allowed", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-otp-group"
|
||||||
|
className={cn("flex items-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSlot({
|
||||||
|
index,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
index: number
|
||||||
|
}) {
|
||||||
|
const inputOTPContext = React.useContext(OTPInputContext)
|
||||||
|
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="input-otp-slot"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
{hasFakeCaret && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||||
|
<MinusIcon />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||||
15
client/cms/src/hooks/data/useCheckinSubmit.ts
Normal file
15
client/cms/src/hooks/data/useCheckinSubmit.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { postEventCheckinSubmitMutation } from '@/client/@tanstack/react-query.gen';
|
||||||
|
|
||||||
|
export function useCheckinSubmit() {
|
||||||
|
return useMutation({
|
||||||
|
...postEventCheckinSubmitMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('签到成功');
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error('签到失败');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user