Fix deploy/compose file, remove client codes (multirepo)
All checks were successful
Server Check Build (NixCN CMS) TeamCity build finished
All checks were successful
Server Check Build (NixCN CMS) TeamCity build finished
Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
29
client/cms/.gitignore
vendored
29
client/cms/.gitignore
vendored
@@ -1,29 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
.direnv
|
|
||||||
|
|
||||||
*storybook.log
|
|
||||||
storybook-static
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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('签到失败'),
|
|
||||||
});
|
|
||||||
```
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
### 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`.
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
"stories": [
|
|
||||||
"../src/**/*.mdx",
|
|
||||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
|
||||||
],
|
|
||||||
"addons": [
|
|
||||||
"@chromatic-com/storybook",
|
|
||||||
"@storybook/addon-vitest",
|
|
||||||
"@storybook/addon-a11y",
|
|
||||||
"@storybook/addon-docs",
|
|
||||||
"@storybook/addon-onboarding"
|
|
||||||
],
|
|
||||||
"framework": "@storybook/react-vite"
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type { Decorator, Preview } from '@storybook/react-vite';
|
|
||||||
import { ThemeProvider } from '../src/components/theme-provider';
|
|
||||||
import '../src/index.css';
|
|
||||||
import { createRootRoute, createRouter, RouterProvider } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
const RouterDecorator: Decorator = (Story) => {
|
|
||||||
const rootRoute = createRootRoute({ component: () => <Story /> });
|
|
||||||
const routeTree = rootRoute;
|
|
||||||
const router = createRouter({ routeTree });
|
|
||||||
return <RouterProvider router={router} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ThemeDecorator: Decorator = (Story) => {
|
|
||||||
return <ThemeProvider defaultTheme="dark"><Story /></ThemeProvider>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
decorators: [RouterDecorator, ThemeDecorator],
|
|
||||||
parameters: {
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/i,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
a11y: {
|
|
||||||
// 'todo' - show a11y violations in the test UI only
|
|
||||||
// 'error' - fail CI on a11y violations
|
|
||||||
// 'off' - skip a11y checks entirely
|
|
||||||
test: 'todo'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
|
|
||||||
import { setProjectAnnotations } from '@storybook/react-vite';
|
|
||||||
import * as projectAnnotations from './preview';
|
|
||||||
|
|
||||||
// This is an important step to apply the right configuration when testing your stories.
|
|
||||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
|
||||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"file_scan_exclusions": [
|
|
||||||
"src/components/ui",
|
|
||||||
".tanstack",
|
|
||||||
"node_modules",
|
|
||||||
"dist",
|
|
||||||
|
|
||||||
// default values below
|
|
||||||
"**/.git",
|
|
||||||
"**/.svn",
|
|
||||||
"**/.hg",
|
|
||||||
"**/CVS",
|
|
||||||
"**/.DS_Store",
|
|
||||||
"**/Thumbs.db",
|
|
||||||
"**/.classpath",
|
|
||||||
"**/.settings"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://ui.shadcn.com/schema.json",
|
|
||||||
"style": "new-york",
|
|
||||||
"rsc": false,
|
|
||||||
"tsx": true,
|
|
||||||
"tailwind": {
|
|
||||||
"config": "",
|
|
||||||
"css": "src/index.css",
|
|
||||||
"baseColor": "neutral",
|
|
||||||
"cssVariables": true,
|
|
||||||
"prefix": ""
|
|
||||||
},
|
|
||||||
"iconLibrary": "lucide",
|
|
||||||
"aliases": {
|
|
||||||
"components": "@/components",
|
|
||||||
"utils": "@/lib/utils",
|
|
||||||
"ui": "@/components/ui",
|
|
||||||
"lib": "@/lib",
|
|
||||||
"hooks": "@/hooks"
|
|
||||||
},
|
|
||||||
"registries": {}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
|
||||||
import antfu from '@antfu/eslint-config';
|
|
||||||
import pluginQuery from '@tanstack/eslint-plugin-query';
|
|
||||||
|
|
||||||
export default antfu({
|
|
||||||
gitignore: true,
|
|
||||||
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*', 'src/client/**/*', 'openapi-ts.config.ts', 'vitest.shims.d.ts', '.storybook/**/*'],
|
|
||||||
react: true,
|
|
||||||
stylistic: {
|
|
||||||
semi: true,
|
|
||||||
quotes: 'single',
|
|
||||||
indent: 2,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
tsconfigPath: 'tsconfig.json',
|
|
||||||
},
|
|
||||||
}, ...pluginQuery.configs['flat/recommended']);
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>client</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { defineConfig } from '@hey-api/openapi-ts';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
input: 'http://10.0.0.10:8000/swagger/doc.json', // sign up at app.heyapi.dev
|
|
||||||
output: 'src/client',
|
|
||||||
plugins: [
|
|
||||||
'@hey-api/typescript',
|
|
||||||
{
|
|
||||||
name: '@tanstack/react-query',
|
|
||||||
infiniteQueryOptions: true,
|
|
||||||
infiniteQueryKeys: true,
|
|
||||||
},
|
|
||||||
'zod',
|
|
||||||
{
|
|
||||||
name: '@hey-api/transformers',
|
|
||||||
dates: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '@hey-api/sdk',
|
|
||||||
transformer: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "client",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc -b && vite build",
|
|
||||||
"lint": "eslint .",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"gen": "openapi-ts",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"build-storybook": "storybook build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@base-ui/react": "^1.1.0",
|
|
||||||
"@dicebear/collection": "^9.3.1",
|
|
||||||
"@dicebear/core": "^9.3.1",
|
|
||||||
"@dicebear/identicon": "^9.3.1",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@hookform/resolvers": "^5.2.2",
|
|
||||||
"@marsidev/react-turnstile": "^1.4.0",
|
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
|
||||||
"@tabler/icons-react": "^3.36.0",
|
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
|
||||||
"@tanstack/react-form": "^1.27.7",
|
|
||||||
"@tanstack/react-query": "^5.90.12",
|
|
||||||
"@tanstack/react-router": "^1.141.6",
|
|
||||||
"@tanstack/react-router-devtools": "^1.141.6",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"@tanstack/zod-adapter": "^1.143.4",
|
|
||||||
"@tanstack/zod-form-adapter": "^0.42.1",
|
|
||||||
"@uiw/react-md-editor": "^4.0.11",
|
|
||||||
"@yudiel/react-qr-scanner": "^2.5.1",
|
|
||||||
"axios": "^1.13.2",
|
|
||||||
"base-64": "^1.0.0",
|
|
||||||
"buffer": "^6.0.3",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"culori": "^4.0.2",
|
|
||||||
"dayjs": "^1.11.19",
|
|
||||||
"immer": "^11.1.0",
|
|
||||||
"input-otp": "^1.4.2",
|
|
||||||
"lodash-es": "^4.17.22",
|
|
||||||
"lucide-react": "^0.562.0",
|
|
||||||
"next-themes": "^0.4.6",
|
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"radix-ui": "^1.4.3",
|
|
||||||
"react": "^19.2.0",
|
|
||||||
"react-dom": "^19.2.0",
|
|
||||||
"react-error-boundary": "^6.1.0",
|
|
||||||
"react-hook-form": "^7.69.0",
|
|
||||||
"react-markdown": "^10.1.0",
|
|
||||||
"react-spinners": "^0.17.0",
|
|
||||||
"recharts": "2.15.4",
|
|
||||||
"sonner": "^2.0.7",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
|
||||||
"tailwindcss": "^4.1.18",
|
|
||||||
"utf8": "^3.0.0",
|
|
||||||
"vaul": "^1.1.2",
|
|
||||||
"zod": "^3.25.76",
|
|
||||||
"zustand": "^5.0.9"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@antfu/eslint-config": "^6.7.1",
|
|
||||||
"@chromatic-com/storybook": "^5.0.0",
|
|
||||||
"@eslint-react/eslint-plugin": "^2.3.13",
|
|
||||||
"@eslint/js": "^9.39.1",
|
|
||||||
"@hey-api/openapi-ts": "0.91.0",
|
|
||||||
"@playwright/test": "^1.58.2",
|
|
||||||
"@redux-devtools/extension": "^3.3.0",
|
|
||||||
"@storybook/addon-a11y": "^10.2.3",
|
|
||||||
"@storybook/addon-docs": "^10.2.3",
|
|
||||||
"@storybook/addon-onboarding": "^10.2.3",
|
|
||||||
"@storybook/addon-themes": "^10.2.3",
|
|
||||||
"@storybook/addon-vitest": "^10.2.3",
|
|
||||||
"@storybook/react-vite": "^10.2.3",
|
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
|
||||||
"@tanstack/eslint-plugin-query": "^5.91.2",
|
|
||||||
"@tanstack/router-plugin": "^1.141.7",
|
|
||||||
"@types/base-64": "^1.0.2",
|
|
||||||
"@types/culori": "^4.0.1",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@types/node": "^25.0.3",
|
|
||||||
"@types/qrcode": "^1.5.6",
|
|
||||||
"@types/react": "^19.2.5",
|
|
||||||
"@types/react-dom": "^19.2.3",
|
|
||||||
"@types/utf8": "^3.0.3",
|
|
||||||
"@vitejs/plugin-react": "^5.1.1",
|
|
||||||
"@vitest/browser-playwright": "^4.0.18",
|
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
|
||||||
"eslint": "^9.39.1",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.26",
|
|
||||||
"eslint-plugin-storybook": "^10.2.3",
|
|
||||||
"globals": "^16.5.0",
|
|
||||||
"lint-staged": "^16.2.7",
|
|
||||||
"playwright": "^1.58.2",
|
|
||||||
"simple-git-hooks": "^2.13.1",
|
|
||||||
"storybook": "^10.2.3",
|
|
||||||
"tw-animate-css": "^1.4.0",
|
|
||||||
"type-fest": "^5.4.1",
|
|
||||||
"typescript": "~5.9.3",
|
|
||||||
"typescript-eslint": "^8.46.4",
|
|
||||||
"vite": "^7.2.4",
|
|
||||||
"vite-plugin-svgr": "^4.5.0",
|
|
||||||
"vitest": "^4.0.18"
|
|
||||||
},
|
|
||||||
"simple-git-hooks": {
|
|
||||||
"pre-commit": "bun run lint-staged"
|
|
||||||
},
|
|
||||||
"lint-staged": {
|
|
||||||
"*": "eslint --fix"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './tests',
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: 'html',
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:5173',
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
11366
client/cms/pnpm-lock.yaml
generated
11366
client/cms/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
import { ThemeProvider } from '@/components/theme-provider';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
|
||||||
<p>Hello world</p>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { App };
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,28 +0,0 @@
|
|||||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M120.587 309.626L148.36 261.518L269.887 472.005H214.34L186.567 423.897L158.794 472.005H131.021L117.126 447.943L158.794 375.772L120.604 309.626H120.587Z" fill="url(#paint0_linear_2199_41)"/>
|
|
||||||
<path d="M141.421 165.285H196.968L75.4412 375.772L47.6681 327.664L75.4412 279.556H19.8949L6 255.494L19.8949 231.432H103.231L141.421 165.285Z" fill="url(#paint1_linear_2199_41)"/>
|
|
||||||
<path d="M276.826 111.17L304.599 159.278H61.5632L89.3364 111.17H144.883L117.11 63.0623L131.004 39H158.778L200.446 111.17H276.826Z" fill="url(#paint2_linear_2199_41)"/>
|
|
||||||
<path d="M391.413 201.379L363.64 249.487L242.114 39H297.66L325.433 87.108L353.206 39H380.979L394.874 63.0623L353.206 135.233L391.396 201.379H391.413Z" fill="url(#paint3_linear_2199_41)"/>
|
|
||||||
<path d="M370.579 345.703H315.032L436.559 135.216L464.332 183.324L436.559 231.432H492.105L506 255.494L492.105 279.556H408.769L370.579 345.703Z" fill="url(#paint4_linear_2199_41)"/>
|
|
||||||
<path d="M235.175 399.835L207.401 351.727H450.454L422.681 399.835H367.134L394.908 447.943L381.013 472.005H353.24L311.572 399.835H235.191H235.175Z" fill="url(#paint5_linear_2199_41)"/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="paint0_linear_2199_41" x1="-244.299" y1="-121.183" x2="-163.239" y2="19.218" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_2199_41" x1="-194.029" y1="-258.424" x2="-275.089" y2="-118.023" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear_2199_41" x1="-50.0423" y1="-283.509" x2="-212.164" y2="-283.509" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint3_linear_2199_41" x1="43.6727" y1="-171.37" x2="-37.3876" y2="-311.771" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint4_linear_2199_41" x1="-6.58154" y1="-34.1292" x2="74.4793" y2="-174.531" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint5_linear_2199_41" x1="-150.568" y1="-9.02725" x2="11.5536" y2="-9.02733" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="1" stop-color="white"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,547 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { client } from '../client.gen';
|
|
||||||
import { getAuthRedirect, getEventAttendance, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventJoined, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAgendaSubmit, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from '../sdk.gen';
|
|
||||||
import type { GetAuthRedirectData, GetAuthRedirectError, GetEventAttendanceData, GetEventAttendanceError, GetEventAttendanceResponse, GetEventCheckinData, GetEventCheckinError, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryResponse, GetEventCheckinResponse, GetEventInfoData, GetEventInfoError, GetEventInfoResponse, GetEventJoinedData, GetEventJoinedError, GetEventJoinedResponse, GetEventListData, GetEventListError, GetEventListResponse, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, GetUserInfoData, GetUserInfoError, GetUserInfoResponse, GetUserListData, GetUserListError, GetUserListResponse, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateResponse, PostAgendaSubmitData, PostAgendaSubmitError, PostAgendaSubmitResponse, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeResponse, PostAuthMagicData, PostAuthMagicError, PostAuthMagicResponse, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshResponse, PostAuthTokenData, PostAuthTokenError, PostAuthTokenResponse, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitResponse, PostEventJoinData, PostEventJoinError, PostEventJoinResponse, PostKycQueryData, PostKycQueryError, PostKycQueryResponse, PostKycSessionData, PostKycSessionError, PostKycSessionResponse } from '../types.gen';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit Agenda
|
|
||||||
*
|
|
||||||
* Creates a new agenda item for a specific attendance record.
|
|
||||||
*/
|
|
||||||
export const postAgendaSubmitMutation = (options?: Partial<Options<PostAgendaSubmitData>>): UseMutationOptions<PostAgendaSubmitResponse, PostAgendaSubmitError, Options<PostAgendaSubmitData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostAgendaSubmitResponse, PostAgendaSubmitError, Options<PostAgendaSubmitData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postAgendaSubmit({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Auth Code
|
|
||||||
*
|
|
||||||
* Exchanges client credentials and user session for a specific redirect authorization code.
|
|
||||||
*/
|
|
||||||
export const postAuthExchangeMutation = (options?: Partial<Options<PostAuthExchangeData>>): UseMutationOptions<PostAuthExchangeResponse, PostAuthExchangeError, Options<PostAuthExchangeData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostAuthExchangeResponse, PostAuthExchangeError, Options<PostAuthExchangeData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postAuthExchange({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request Magic Link
|
|
||||||
*
|
|
||||||
* Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
|
|
||||||
*/
|
|
||||||
export const postAuthMagicMutation = (options?: Partial<Options<PostAuthMagicData>>): UseMutationOptions<PostAuthMagicResponse, PostAuthMagicError, Options<PostAuthMagicData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostAuthMagicResponse, PostAuthMagicError, Options<PostAuthMagicData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postAuthMagic({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QueryKey<TOptions extends Options> = [
|
|
||||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
|
||||||
_id: string;
|
|
||||||
_infinite?: boolean;
|
|
||||||
tags?: ReadonlyArray<string>;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean, tags?: ReadonlyArray<string>): [
|
|
||||||
QueryKey<TOptions>[0]
|
|
||||||
] => {
|
|
||||||
const params: QueryKey<TOptions>[0] = { _id: id, baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl } as QueryKey<TOptions>[0];
|
|
||||||
if (infinite) {
|
|
||||||
params._infinite = infinite;
|
|
||||||
}
|
|
||||||
if (tags) {
|
|
||||||
params.tags = tags;
|
|
||||||
}
|
|
||||||
if (options?.body) {
|
|
||||||
params.body = options.body;
|
|
||||||
}
|
|
||||||
if (options?.headers) {
|
|
||||||
params.headers = options.headers;
|
|
||||||
}
|
|
||||||
if (options?.path) {
|
|
||||||
params.path = options.path;
|
|
||||||
}
|
|
||||||
if (options?.query) {
|
|
||||||
params.query = options.query;
|
|
||||||
}
|
|
||||||
return [params];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAuthRedirectQueryKey = (options: Options<GetAuthRedirectData>) => createQueryKey('getAuthRedirect', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle Auth Callback and Redirect
|
|
||||||
*
|
|
||||||
* Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
|
|
||||||
*/
|
|
||||||
export const getAuthRedirectOptions = (options: Options<GetAuthRedirectData>) => queryOptions<unknown, GetAuthRedirectError, unknown, ReturnType<typeof getAuthRedirectQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getAuthRedirect({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getAuthRedirectQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh Access Token
|
|
||||||
*
|
|
||||||
* Accepts a valid refresh token to issue a new access token and a rotated refresh token.
|
|
||||||
*/
|
|
||||||
export const postAuthRefreshMutation = (options?: Partial<Options<PostAuthRefreshData>>): UseMutationOptions<PostAuthRefreshResponse, PostAuthRefreshError, Options<PostAuthRefreshData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostAuthRefreshResponse, PostAuthRefreshError, Options<PostAuthRefreshData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postAuthRefresh({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Code for Token
|
|
||||||
*
|
|
||||||
* Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
|
|
||||||
*/
|
|
||||||
export const postAuthTokenMutation = (options?: Partial<Options<PostAuthTokenData>>): UseMutationOptions<PostAuthTokenResponse, PostAuthTokenError, Options<PostAuthTokenData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostAuthTokenResponse, PostAuthTokenError, Options<PostAuthTokenData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postAuthToken({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEventAttendanceQueryKey = (options: Options<GetEventAttendanceData>) => createQueryKey('getEventAttendance', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Attendance List
|
|
||||||
*
|
|
||||||
* Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
|
|
||||||
*/
|
|
||||||
export const getEventAttendanceOptions = (options: Options<GetEventAttendanceData>) => queryOptions<GetEventAttendanceResponse, GetEventAttendanceError, GetEventAttendanceResponse, ReturnType<typeof getEventAttendanceQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventAttendance({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventAttendanceQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getEventCheckinQueryKey = (options: Options<GetEventCheckinData>) => createQueryKey('getEventCheckin', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Check-in Code
|
|
||||||
*
|
|
||||||
* Creates a temporary check-in code for the authenticated user and event.
|
|
||||||
*/
|
|
||||||
export const getEventCheckinOptions = (options: Options<GetEventCheckinData>) => queryOptions<GetEventCheckinResponse, GetEventCheckinError, GetEventCheckinResponse, ReturnType<typeof getEventCheckinQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventCheckin({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventCheckinQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getEventCheckinQueryQueryKey = (options: Options<GetEventCheckinQueryData>) => createQueryKey('getEventCheckinQuery', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query Check-in Status
|
|
||||||
*
|
|
||||||
* Returns the timestamp of when the user checked in, or null if not yet checked in.
|
|
||||||
*/
|
|
||||||
export const getEventCheckinQueryOptions = (options: Options<GetEventCheckinQueryData>) => queryOptions<GetEventCheckinQueryResponse, GetEventCheckinQueryError, GetEventCheckinQueryResponse, ReturnType<typeof getEventCheckinQueryQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventCheckinQuery({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventCheckinQueryQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit Check-in Code
|
|
||||||
*
|
|
||||||
* Submits the generated code to mark the user as attended.
|
|
||||||
*/
|
|
||||||
export const postEventCheckinSubmitMutation = (options?: Partial<Options<PostEventCheckinSubmitData>>): UseMutationOptions<PostEventCheckinSubmitResponse, PostEventCheckinSubmitError, Options<PostEventCheckinSubmitData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostEventCheckinSubmitResponse, PostEventCheckinSubmitError, Options<PostEventCheckinSubmitData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postEventCheckinSubmit({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEventInfoQueryKey = (options: Options<GetEventInfoData>) => createQueryKey('getEventInfo', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Event Information
|
|
||||||
*
|
|
||||||
* Fetches the name, start time, and end time of an event using its UUID.
|
|
||||||
*/
|
|
||||||
export const getEventInfoOptions = (options: Options<GetEventInfoData>) => queryOptions<GetEventInfoResponse, GetEventInfoError, GetEventInfoResponse, ReturnType<typeof getEventInfoQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventInfo({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventInfoQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join an Event
|
|
||||||
*
|
|
||||||
* Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.
|
|
||||||
*/
|
|
||||||
export const postEventJoinMutation = (options?: Partial<Options<PostEventJoinData>>): UseMutationOptions<PostEventJoinResponse, PostEventJoinError, Options<PostEventJoinData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostEventJoinResponse, PostEventJoinError, Options<PostEventJoinData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postEventJoin({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEventJoinedQueryKey = (options?: Options<GetEventJoinedData>) => createQueryKey('getEventJoined', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Joined Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events where the authenticated user is a participant. Supports pagination.
|
|
||||||
*/
|
|
||||||
export const getEventJoinedOptions = (options?: Options<GetEventJoinedData>) => queryOptions<GetEventJoinedResponse, GetEventJoinedError, GetEventJoinedResponse, ReturnType<typeof getEventJoinedQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventJoined({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventJoinedQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
const createInfiniteParams = <K extends Pick<QueryKey<Options>[0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey<Options>, page: K) => {
|
|
||||||
const params = { ...queryKey[0] };
|
|
||||||
if (page.body) {
|
|
||||||
params.body = {
|
|
||||||
...queryKey[0].body as any,
|
|
||||||
...page.body as any
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (page.headers) {
|
|
||||||
params.headers = {
|
|
||||||
...queryKey[0].headers,
|
|
||||||
...page.headers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (page.path) {
|
|
||||||
params.path = {
|
|
||||||
...queryKey[0].path as any,
|
|
||||||
...page.path as any
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (page.query) {
|
|
||||||
params.query = {
|
|
||||||
...queryKey[0].query as any,
|
|
||||||
...page.query as any
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return params as unknown as typeof page;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getEventJoinedInfiniteQueryKey = (options?: Options<GetEventJoinedData>): QueryKey<Options<GetEventJoinedData>> => createQueryKey('getEventJoined', options, true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Joined Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events where the authenticated user is a participant. Supports pagination.
|
|
||||||
*/
|
|
||||||
export const getEventJoinedInfiniteOptions = (options?: Options<GetEventJoinedData>) => infiniteQueryOptions<GetEventJoinedResponse, GetEventJoinedError, InfiniteData<GetEventJoinedResponse>, QueryKey<Options<GetEventJoinedData>>, number | Pick<QueryKey<Options<GetEventJoinedData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
|
|
||||||
// @ts-ignore
|
|
||||||
{
|
|
||||||
queryFn: async ({ pageParam, queryKey, signal }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const page: Pick<QueryKey<Options<GetEventJoinedData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
|
|
||||||
query: {
|
|
||||||
offset: pageParam
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const params = createInfiniteParams(queryKey, page);
|
|
||||||
const { data } = await getEventJoined({
|
|
||||||
...options,
|
|
||||||
...params,
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventJoinedInfiniteQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getEventListQueryKey = (options?: Options<GetEventListData>) => createQueryKey('getEventList', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
|
||||||
*/
|
|
||||||
export const getEventListOptions = (options?: Options<GetEventListData>) => queryOptions<GetEventListResponse, GetEventListError, GetEventListResponse, ReturnType<typeof getEventListQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getEventList({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventListQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getEventListInfiniteQueryKey = (options?: Options<GetEventListData>): QueryKey<Options<GetEventListData>> => createQueryKey('getEventList', options, true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
|
||||||
*/
|
|
||||||
export const getEventListInfiniteOptions = (options?: Options<GetEventListData>) => infiniteQueryOptions<GetEventListResponse, GetEventListError, InfiniteData<GetEventListResponse>, QueryKey<Options<GetEventListData>>, number | Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
|
|
||||||
// @ts-ignore
|
|
||||||
{
|
|
||||||
queryFn: async ({ pageParam, queryKey, signal }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const page: Pick<QueryKey<Options<GetEventListData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
|
|
||||||
query: {
|
|
||||||
offset: pageParam
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const params = createInfiniteParams(queryKey, page);
|
|
||||||
const { data } = await getEventList({
|
|
||||||
...options,
|
|
||||||
...params,
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getEventListInfiniteQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query KYC Status
|
|
||||||
*
|
|
||||||
* Checks the current state of a KYC session and updates local database if approved.
|
|
||||||
*/
|
|
||||||
export const postKycQueryMutation = (options?: Partial<Options<PostKycQueryData>>): UseMutationOptions<PostKycQueryResponse, PostKycQueryError, Options<PostKycQueryData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostKycQueryResponse, PostKycQueryError, Options<PostKycQueryData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postKycQuery({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create KYC Session
|
|
||||||
*
|
|
||||||
* Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.
|
|
||||||
*/
|
|
||||||
export const postKycSessionMutation = (options?: Partial<Options<PostKycSessionData>>): UseMutationOptions<PostKycSessionResponse, PostKycSessionError, Options<PostKycSessionData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PostKycSessionResponse, PostKycSessionError, Options<PostKycSessionData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await postKycSession({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserInfoQueryKey = (options?: Options<GetUserInfoData>) => createQueryKey('getUserInfo', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get My User Information
|
|
||||||
*
|
|
||||||
* Fetches the complete profile data for the user associated with the provided session/token.
|
|
||||||
*/
|
|
||||||
export const getUserInfoOptions = (options?: Options<GetUserInfoData>) => queryOptions<GetUserInfoResponse, GetUserInfoError, GetUserInfoResponse, ReturnType<typeof getUserInfoQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getUserInfo({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getUserInfoQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getUserInfoByUserIdQueryKey = (options: Options<GetUserInfoByUserIdData>) => createQueryKey('getUserInfoByUserId', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Other User Information
|
|
||||||
*
|
|
||||||
* Fetches the complete profile data for the user associated with the provided session/token.
|
|
||||||
*/
|
|
||||||
export const getUserInfoByUserIdOptions = (options: Options<GetUserInfoByUserIdData>) => queryOptions<GetUserInfoByUserIdResponse, GetUserInfoByUserIdError, GetUserInfoByUserIdResponse, ReturnType<typeof getUserInfoByUserIdQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getUserInfoByUserId({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getUserInfoByUserIdQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getUserListQueryKey = (options: Options<GetUserListData>) => createQueryKey('getUserList', options);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Users
|
|
||||||
*
|
|
||||||
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
|
|
||||||
*/
|
|
||||||
export const getUserListOptions = (options: Options<GetUserListData>) => queryOptions<GetUserListResponse, GetUserListError, GetUserListResponse, ReturnType<typeof getUserListQueryKey>>({
|
|
||||||
queryFn: async ({ queryKey, signal }) => {
|
|
||||||
const { data } = await getUserList({
|
|
||||||
...options,
|
|
||||||
...queryKey[0],
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getUserListQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getUserListInfiniteQueryKey = (options: Options<GetUserListData>): QueryKey<Options<GetUserListData>> => createQueryKey('getUserList', options, true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Users
|
|
||||||
*
|
|
||||||
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
|
|
||||||
*/
|
|
||||||
export const getUserListInfiniteOptions = (options: Options<GetUserListData>) => infiniteQueryOptions<GetUserListResponse, GetUserListError, InfiniteData<GetUserListResponse>, QueryKey<Options<GetUserListData>>, string | Pick<QueryKey<Options<GetUserListData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
|
|
||||||
// @ts-ignore
|
|
||||||
{
|
|
||||||
queryFn: async ({ pageParam, queryKey, signal }) => {
|
|
||||||
// @ts-ignore
|
|
||||||
const page: Pick<QueryKey<Options<GetUserListData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : {
|
|
||||||
query: {
|
|
||||||
offset: pageParam
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const params = createInfiniteParams(queryKey, page);
|
|
||||||
const { data } = await getUserList({
|
|
||||||
...options,
|
|
||||||
...params,
|
|
||||||
signal,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
queryKey: getUserListInfiniteQueryKey(options)
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update User Information
|
|
||||||
*
|
|
||||||
* Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
|
|
||||||
* Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
|
|
||||||
*/
|
|
||||||
export const patchUserUpdateMutation = (options?: Partial<Options<PatchUserUpdateData>>): UseMutationOptions<PatchUserUpdateResponse, PatchUserUpdateError, Options<PatchUserUpdateData>> => {
|
|
||||||
const mutationOptions: UseMutationOptions<PatchUserUpdateResponse, PatchUserUpdateError, Options<PatchUserUpdateData>> = {
|
|
||||||
mutationFn: async (fnOptions) => {
|
|
||||||
const { data } = await patchUserUpdate({
|
|
||||||
...options,
|
|
||||||
...fnOptions,
|
|
||||||
throwOnError: true
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return mutationOptions;
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { type ClientOptions, type Config, createClient, createConfig } from './client';
|
|
||||||
import type { ClientOptions as ClientOptions2 } from './types.gen';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
|
||||||
* and the returned object will become the client's initial configuration.
|
|
||||||
*
|
|
||||||
* You may want to initialize your client this way instead of calling
|
|
||||||
* `setConfig()`. This is useful for example if you're using Next.js
|
|
||||||
* to ensure your client always has the correct values.
|
|
||||||
*/
|
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:8000/app/api/v1' }));
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { createSseClient } from '../core/serverSentEvents.gen';
|
|
||||||
import type { HttpMethod } from '../core/types.gen';
|
|
||||||
import { getValidRequestBody } from '../core/utils.gen';
|
|
||||||
import type {
|
|
||||||
Client,
|
|
||||||
Config,
|
|
||||||
RequestOptions,
|
|
||||||
ResolvedRequestOptions,
|
|
||||||
} from './types.gen';
|
|
||||||
import {
|
|
||||||
buildUrl,
|
|
||||||
createConfig,
|
|
||||||
createInterceptors,
|
|
||||||
getParseAs,
|
|
||||||
mergeConfigs,
|
|
||||||
mergeHeaders,
|
|
||||||
setAuthParams,
|
|
||||||
} from './utils.gen';
|
|
||||||
|
|
||||||
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
|
|
||||||
body?: any;
|
|
||||||
headers: ReturnType<typeof mergeHeaders>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createClient = (config: Config = {}): Client => {
|
|
||||||
let _config = mergeConfigs(createConfig(), config);
|
|
||||||
|
|
||||||
const getConfig = (): Config => ({ ..._config });
|
|
||||||
|
|
||||||
const setConfig = (config: Config): Config => {
|
|
||||||
_config = mergeConfigs(_config, config);
|
|
||||||
return getConfig();
|
|
||||||
};
|
|
||||||
|
|
||||||
const interceptors = createInterceptors<
|
|
||||||
Request,
|
|
||||||
Response,
|
|
||||||
unknown,
|
|
||||||
ResolvedRequestOptions
|
|
||||||
>();
|
|
||||||
|
|
||||||
const beforeRequest = async (options: RequestOptions) => {
|
|
||||||
const opts = {
|
|
||||||
..._config,
|
|
||||||
...options,
|
|
||||||
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
|
||||||
headers: mergeHeaders(_config.headers, options.headers),
|
|
||||||
serializedBody: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (opts.security) {
|
|
||||||
await setAuthParams({
|
|
||||||
...opts,
|
|
||||||
security: opts.security,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.requestValidator) {
|
|
||||||
await opts.requestValidator(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.body !== undefined && opts.bodySerializer) {
|
|
||||||
opts.serializedBody = opts.bodySerializer(opts.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove Content-Type header if body is empty to avoid sending invalid requests
|
|
||||||
if (opts.body === undefined || opts.serializedBody === '') {
|
|
||||||
opts.headers.delete('Content-Type');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = buildUrl(opts);
|
|
||||||
|
|
||||||
return { opts, url };
|
|
||||||
};
|
|
||||||
|
|
||||||
const request: Client['request'] = async (options) => {
|
|
||||||
// @ts-expect-error
|
|
||||||
const { opts, url } = await beforeRequest(options);
|
|
||||||
const requestInit: ReqInit = {
|
|
||||||
redirect: 'follow',
|
|
||||||
...opts,
|
|
||||||
body: getValidRequestBody(opts),
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = new Request(url, requestInit);
|
|
||||||
|
|
||||||
for (const fn of interceptors.request.fns) {
|
|
||||||
if (fn) {
|
|
||||||
request = await fn(request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
|
||||||
const _fetch = opts.fetch!;
|
|
||||||
let response: Response;
|
|
||||||
|
|
||||||
try {
|
|
||||||
response = await _fetch(request);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle fetch exceptions (AbortError, network errors, etc.)
|
|
||||||
let finalError = error;
|
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
|
||||||
if (fn) {
|
|
||||||
finalError = (await fn(
|
|
||||||
error,
|
|
||||||
undefined as any,
|
|
||||||
request,
|
|
||||||
opts,
|
|
||||||
)) as unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalError = finalError || ({} as unknown);
|
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
|
||||||
throw finalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return error response
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
error: finalError,
|
|
||||||
request,
|
|
||||||
response: undefined as any,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const fn of interceptors.response.fns) {
|
|
||||||
if (fn) {
|
|
||||||
response = await fn(response, request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const parseAs =
|
|
||||||
(opts.parseAs === 'auto'
|
|
||||||
? getParseAs(response.headers.get('Content-Type'))
|
|
||||||
: opts.parseAs) ?? 'json';
|
|
||||||
|
|
||||||
if (
|
|
||||||
response.status === 204 ||
|
|
||||||
response.headers.get('Content-Length') === '0'
|
|
||||||
) {
|
|
||||||
let emptyData: any;
|
|
||||||
switch (parseAs) {
|
|
||||||
case 'arrayBuffer':
|
|
||||||
case 'blob':
|
|
||||||
case 'text':
|
|
||||||
emptyData = await response[parseAs]();
|
|
||||||
break;
|
|
||||||
case 'formData':
|
|
||||||
emptyData = new FormData();
|
|
||||||
break;
|
|
||||||
case 'stream':
|
|
||||||
emptyData = response.body;
|
|
||||||
break;
|
|
||||||
case 'json':
|
|
||||||
default:
|
|
||||||
emptyData = {};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? emptyData
|
|
||||||
: {
|
|
||||||
data: emptyData,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: any;
|
|
||||||
switch (parseAs) {
|
|
||||||
case 'arrayBuffer':
|
|
||||||
case 'blob':
|
|
||||||
case 'formData':
|
|
||||||
case 'text':
|
|
||||||
data = await response[parseAs]();
|
|
||||||
break;
|
|
||||||
case 'json': {
|
|
||||||
// Some servers return 200 with no Content-Length and empty body.
|
|
||||||
// response.json() would throw; read as text and parse if non-empty.
|
|
||||||
const text = await response.text();
|
|
||||||
data = text ? JSON.parse(text) : {};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'stream':
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? response.body
|
|
||||||
: {
|
|
||||||
data: response.body,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parseAs === 'json') {
|
|
||||||
if (opts.responseValidator) {
|
|
||||||
await opts.responseValidator(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.responseTransformer) {
|
|
||||||
data = await opts.responseTransformer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? data
|
|
||||||
: {
|
|
||||||
data,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const textError = await response.text();
|
|
||||||
let jsonError: unknown;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonError = JSON.parse(textError);
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = jsonError ?? textError;
|
|
||||||
let finalError = error;
|
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
|
||||||
if (fn) {
|
|
||||||
finalError = (await fn(error, response, request, opts)) as string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalError = finalError || ({} as string);
|
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
|
||||||
throw finalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we probably want to return error and improve types
|
|
||||||
return opts.responseStyle === 'data'
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
error: finalError,
|
|
||||||
...result,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMethodFn =
|
|
||||||
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
|
||||||
request({ ...options, method });
|
|
||||||
|
|
||||||
const makeSseFn =
|
|
||||||
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
|
||||||
const { opts, url } = await beforeRequest(options);
|
|
||||||
return createSseClient({
|
|
||||||
...opts,
|
|
||||||
body: opts.body as BodyInit | null | undefined,
|
|
||||||
headers: opts.headers as unknown as Record<string, string>,
|
|
||||||
method,
|
|
||||||
onRequest: async (url, init) => {
|
|
||||||
let request = new Request(url, init);
|
|
||||||
for (const fn of interceptors.request.fns) {
|
|
||||||
if (fn) {
|
|
||||||
request = await fn(request, opts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return request;
|
|
||||||
},
|
|
||||||
serializedBody: getValidRequestBody(opts) as
|
|
||||||
| BodyInit
|
|
||||||
| null
|
|
||||||
| undefined,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
buildUrl,
|
|
||||||
connect: makeMethodFn('CONNECT'),
|
|
||||||
delete: makeMethodFn('DELETE'),
|
|
||||||
get: makeMethodFn('GET'),
|
|
||||||
getConfig,
|
|
||||||
head: makeMethodFn('HEAD'),
|
|
||||||
interceptors,
|
|
||||||
options: makeMethodFn('OPTIONS'),
|
|
||||||
patch: makeMethodFn('PATCH'),
|
|
||||||
post: makeMethodFn('POST'),
|
|
||||||
put: makeMethodFn('PUT'),
|
|
||||||
request,
|
|
||||||
setConfig,
|
|
||||||
sse: {
|
|
||||||
connect: makeSseFn('CONNECT'),
|
|
||||||
delete: makeSseFn('DELETE'),
|
|
||||||
get: makeSseFn('GET'),
|
|
||||||
head: makeSseFn('HEAD'),
|
|
||||||
options: makeSseFn('OPTIONS'),
|
|
||||||
patch: makeSseFn('PATCH'),
|
|
||||||
post: makeSseFn('POST'),
|
|
||||||
put: makeSseFn('PUT'),
|
|
||||||
trace: makeSseFn('TRACE'),
|
|
||||||
},
|
|
||||||
trace: makeMethodFn('TRACE'),
|
|
||||||
} as Client;
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type { Auth } from '../core/auth.gen';
|
|
||||||
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
|
||||||
export {
|
|
||||||
formDataBodySerializer,
|
|
||||||
jsonBodySerializer,
|
|
||||||
urlSearchParamsBodySerializer,
|
|
||||||
} from '../core/bodySerializer.gen';
|
|
||||||
export { buildClientParams } from '../core/params.gen';
|
|
||||||
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
|
|
||||||
export { createClient } from './client.gen';
|
|
||||||
export type {
|
|
||||||
Client,
|
|
||||||
ClientOptions,
|
|
||||||
Config,
|
|
||||||
CreateClientConfig,
|
|
||||||
Options,
|
|
||||||
RequestOptions,
|
|
||||||
RequestResult,
|
|
||||||
ResolvedRequestOptions,
|
|
||||||
ResponseStyle,
|
|
||||||
TDataShape,
|
|
||||||
} from './types.gen';
|
|
||||||
export { createConfig, mergeHeaders } from './utils.gen';
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Auth } from '../core/auth.gen';
|
|
||||||
import type {
|
|
||||||
ServerSentEventsOptions,
|
|
||||||
ServerSentEventsResult,
|
|
||||||
} from '../core/serverSentEvents.gen';
|
|
||||||
import type {
|
|
||||||
Client as CoreClient,
|
|
||||||
Config as CoreConfig,
|
|
||||||
} from '../core/types.gen';
|
|
||||||
import type { Middleware } from './utils.gen';
|
|
||||||
|
|
||||||
export type ResponseStyle = 'data' | 'fields';
|
|
||||||
|
|
||||||
export interface Config<T extends ClientOptions = ClientOptions>
|
|
||||||
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
|
|
||||||
CoreConfig {
|
|
||||||
/**
|
|
||||||
* Base URL for all requests made by this client.
|
|
||||||
*/
|
|
||||||
baseUrl?: T['baseUrl'];
|
|
||||||
/**
|
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
|
||||||
* fetch instance.
|
|
||||||
*
|
|
||||||
* @default globalThis.fetch
|
|
||||||
*/
|
|
||||||
fetch?: typeof fetch;
|
|
||||||
/**
|
|
||||||
* Please don't use the Fetch client for Next.js applications. The `next`
|
|
||||||
* options won't have any effect.
|
|
||||||
*
|
|
||||||
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
|
||||||
*/
|
|
||||||
next?: never;
|
|
||||||
/**
|
|
||||||
* Return the response data parsed in a specified format. By default, `auto`
|
|
||||||
* will infer the appropriate method from the `Content-Type` response header.
|
|
||||||
* You can override this behavior with any of the {@link Body} methods.
|
|
||||||
* Select `stream` if you don't want to parse response data at all.
|
|
||||||
*
|
|
||||||
* @default 'auto'
|
|
||||||
*/
|
|
||||||
parseAs?:
|
|
||||||
| 'arrayBuffer'
|
|
||||||
| 'auto'
|
|
||||||
| 'blob'
|
|
||||||
| 'formData'
|
|
||||||
| 'json'
|
|
||||||
| 'stream'
|
|
||||||
| 'text';
|
|
||||||
/**
|
|
||||||
* Should we return only data or multiple fields (data, error, response, etc.)?
|
|
||||||
*
|
|
||||||
* @default 'fields'
|
|
||||||
*/
|
|
||||||
responseStyle?: ResponseStyle;
|
|
||||||
/**
|
|
||||||
* Throw an error instead of returning it in the response?
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
throwOnError?: T['throwOnError'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RequestOptions<
|
|
||||||
TData = unknown,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
Url extends string = string,
|
|
||||||
> extends Config<{
|
|
||||||
responseStyle: TResponseStyle;
|
|
||||||
throwOnError: ThrowOnError;
|
|
||||||
}>,
|
|
||||||
Pick<
|
|
||||||
ServerSentEventsOptions<TData>,
|
|
||||||
| 'onSseError'
|
|
||||||
| 'onSseEvent'
|
|
||||||
| 'sseDefaultRetryDelay'
|
|
||||||
| 'sseMaxRetryAttempts'
|
|
||||||
| 'sseMaxRetryDelay'
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Any body that you want to add to your request.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
|
||||||
*/
|
|
||||||
body?: unknown;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
/**
|
|
||||||
* Security mechanism(s) to use for the request.
|
|
||||||
*/
|
|
||||||
security?: ReadonlyArray<Auth>;
|
|
||||||
url: Url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResolvedRequestOptions<
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
Url extends string = string,
|
|
||||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
|
||||||
serializedBody?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RequestResult<
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
> = ThrowOnError extends true
|
|
||||||
? Promise<
|
|
||||||
TResponseStyle extends 'data'
|
|
||||||
? TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData
|
|
||||||
: {
|
|
||||||
data: TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData;
|
|
||||||
request: Request;
|
|
||||||
response: Response;
|
|
||||||
}
|
|
||||||
>
|
|
||||||
: Promise<
|
|
||||||
TResponseStyle extends 'data'
|
|
||||||
?
|
|
||||||
| (TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData)
|
|
||||||
| undefined
|
|
||||||
: (
|
|
||||||
| {
|
|
||||||
data: TData extends Record<string, unknown>
|
|
||||||
? TData[keyof TData]
|
|
||||||
: TData;
|
|
||||||
error: undefined;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
data: undefined;
|
|
||||||
error: TError extends Record<string, unknown>
|
|
||||||
? TError[keyof TError]
|
|
||||||
: TError;
|
|
||||||
}
|
|
||||||
) & {
|
|
||||||
request: Request;
|
|
||||||
response: Response;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ClientOptions {
|
|
||||||
baseUrl?: string;
|
|
||||||
responseStyle?: ResponseStyle;
|
|
||||||
throwOnError?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MethodFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
|
||||||
|
|
||||||
type SseFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
|
||||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
|
||||||
|
|
||||||
type RequestFn = <
|
|
||||||
TData = unknown,
|
|
||||||
TError = unknown,
|
|
||||||
ThrowOnError extends boolean = false,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
>(
|
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
|
|
||||||
Pick<
|
|
||||||
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
|
|
||||||
'method'
|
|
||||||
>,
|
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
|
||||||
|
|
||||||
type BuildUrlFn = <
|
|
||||||
TData extends {
|
|
||||||
body?: unknown;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
url: string;
|
|
||||||
},
|
|
||||||
>(
|
|
||||||
options: TData & Options<TData>,
|
|
||||||
) => string;
|
|
||||||
|
|
||||||
export type Client = CoreClient<
|
|
||||||
RequestFn,
|
|
||||||
Config,
|
|
||||||
MethodFn,
|
|
||||||
BuildUrlFn,
|
|
||||||
SseFn
|
|
||||||
> & {
|
|
||||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
|
||||||
* and the returned object will become the client's initial configuration.
|
|
||||||
*
|
|
||||||
* You may want to initialize your client this way instead of calling
|
|
||||||
* `setConfig()`. This is useful for example if you're using Next.js
|
|
||||||
* to ensure your client always has the correct values.
|
|
||||||
*/
|
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
|
||||||
override?: Config<ClientOptions & T>,
|
|
||||||
) => Config<Required<ClientOptions> & T>;
|
|
||||||
|
|
||||||
export interface TDataShape {
|
|
||||||
body?: unknown;
|
|
||||||
headers?: unknown;
|
|
||||||
path?: unknown;
|
|
||||||
query?: unknown;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
|
||||||
|
|
||||||
export type Options<
|
|
||||||
TData extends TDataShape = TDataShape,
|
|
||||||
ThrowOnError extends boolean = boolean,
|
|
||||||
TResponse = unknown,
|
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
|
||||||
> = OmitKeys<
|
|
||||||
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
|
||||||
'body' | 'path' | 'query' | 'url'
|
|
||||||
> &
|
|
||||||
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { getAuthToken } from '../core/auth.gen';
|
|
||||||
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
|
||||||
import { jsonBodySerializer } from '../core/bodySerializer.gen';
|
|
||||||
import {
|
|
||||||
serializeArrayParam,
|
|
||||||
serializeObjectParam,
|
|
||||||
serializePrimitiveParam,
|
|
||||||
} from '../core/pathSerializer.gen';
|
|
||||||
import { getUrl } from '../core/utils.gen';
|
|
||||||
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
|
||||||
|
|
||||||
export const createQuerySerializer = <T = unknown>({
|
|
||||||
parameters = {},
|
|
||||||
...args
|
|
||||||
}: QuerySerializerOptions = {}) => {
|
|
||||||
const querySerializer = (queryParams: T) => {
|
|
||||||
const search: string[] = [];
|
|
||||||
if (queryParams && typeof queryParams === 'object') {
|
|
||||||
for (const name in queryParams) {
|
|
||||||
const value = queryParams[name];
|
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = parameters[name] || args;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
const serializedArray = serializeArrayParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
explode: true,
|
|
||||||
name,
|
|
||||||
style: 'form',
|
|
||||||
value,
|
|
||||||
...options.array,
|
|
||||||
});
|
|
||||||
if (serializedArray) search.push(serializedArray);
|
|
||||||
} else if (typeof value === 'object') {
|
|
||||||
const serializedObject = serializeObjectParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
explode: true,
|
|
||||||
name,
|
|
||||||
style: 'deepObject',
|
|
||||||
value: value as Record<string, unknown>,
|
|
||||||
...options.object,
|
|
||||||
});
|
|
||||||
if (serializedObject) search.push(serializedObject);
|
|
||||||
} else {
|
|
||||||
const serializedPrimitive = serializePrimitiveParam({
|
|
||||||
allowReserved: options.allowReserved,
|
|
||||||
name,
|
|
||||||
value: value as string,
|
|
||||||
});
|
|
||||||
if (serializedPrimitive) search.push(serializedPrimitive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return search.join('&');
|
|
||||||
};
|
|
||||||
return querySerializer;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infers parseAs value from provided Content-Type header.
|
|
||||||
*/
|
|
||||||
export const getParseAs = (
|
|
||||||
contentType: string | null,
|
|
||||||
): Exclude<Config['parseAs'], 'auto'> => {
|
|
||||||
if (!contentType) {
|
|
||||||
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
|
||||||
// which is effectively the same as the 'stream' option.
|
|
||||||
return 'stream';
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleanContent = contentType.split(';')[0]?.trim();
|
|
||||||
|
|
||||||
if (!cleanContent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
cleanContent.startsWith('application/json') ||
|
|
||||||
cleanContent.endsWith('+json')
|
|
||||||
) {
|
|
||||||
return 'json';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanContent === 'multipart/form-data') {
|
|
||||||
return 'formData';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
['application/', 'audio/', 'image/', 'video/'].some((type) =>
|
|
||||||
cleanContent.startsWith(type),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return 'blob';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cleanContent.startsWith('text/')) {
|
|
||||||
return 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkForExistence = (
|
|
||||||
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
|
||||||
headers: Headers;
|
|
||||||
},
|
|
||||||
name?: string,
|
|
||||||
): boolean => {
|
|
||||||
if (!name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
options.headers.has(name) ||
|
|
||||||
options.query?.[name] ||
|
|
||||||
options.headers.get('Cookie')?.includes(`${name}=`)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setAuthParams = async ({
|
|
||||||
security,
|
|
||||||
...options
|
|
||||||
}: Pick<Required<RequestOptions>, 'security'> &
|
|
||||||
Pick<RequestOptions, 'auth' | 'query'> & {
|
|
||||||
headers: Headers;
|
|
||||||
}) => {
|
|
||||||
for (const auth of security) {
|
|
||||||
if (checkForExistence(options, auth.name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await getAuthToken(auth, options.auth);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = auth.name ?? 'Authorization';
|
|
||||||
|
|
||||||
switch (auth.in) {
|
|
||||||
case 'query':
|
|
||||||
if (!options.query) {
|
|
||||||
options.query = {};
|
|
||||||
}
|
|
||||||
options.query[name] = token;
|
|
||||||
break;
|
|
||||||
case 'cookie':
|
|
||||||
options.headers.append('Cookie', `${name}=${token}`);
|
|
||||||
break;
|
|
||||||
case 'header':
|
|
||||||
default:
|
|
||||||
options.headers.set(name, token);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildUrl: Client['buildUrl'] = (options) =>
|
|
||||||
getUrl({
|
|
||||||
baseUrl: options.baseUrl as string,
|
|
||||||
path: options.path,
|
|
||||||
query: options.query,
|
|
||||||
querySerializer:
|
|
||||||
typeof options.querySerializer === 'function'
|
|
||||||
? options.querySerializer
|
|
||||||
: createQuerySerializer(options.querySerializer),
|
|
||||||
url: options.url,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
|
||||||
const config = { ...a, ...b };
|
|
||||||
if (config.baseUrl?.endsWith('/')) {
|
|
||||||
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
|
||||||
}
|
|
||||||
config.headers = mergeHeaders(a.headers, b.headers);
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
|
||||||
const entries: Array<[string, string]> = [];
|
|
||||||
headers.forEach((value, key) => {
|
|
||||||
entries.push([key, value]);
|
|
||||||
});
|
|
||||||
return entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mergeHeaders = (
|
|
||||||
...headers: Array<Required<Config>['headers'] | undefined>
|
|
||||||
): Headers => {
|
|
||||||
const mergedHeaders = new Headers();
|
|
||||||
for (const header of headers) {
|
|
||||||
if (!header) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const iterator =
|
|
||||||
header instanceof Headers
|
|
||||||
? headersEntries(header)
|
|
||||||
: Object.entries(header);
|
|
||||||
|
|
||||||
for (const [key, value] of iterator) {
|
|
||||||
if (value === null) {
|
|
||||||
mergedHeaders.delete(key);
|
|
||||||
} else if (Array.isArray(value)) {
|
|
||||||
for (const v of value) {
|
|
||||||
mergedHeaders.append(key, v as string);
|
|
||||||
}
|
|
||||||
} else if (value !== undefined) {
|
|
||||||
// assume object headers are meant to be JSON stringified, i.e. their
|
|
||||||
// content value in OpenAPI specification is 'application/json'
|
|
||||||
mergedHeaders.set(
|
|
||||||
key,
|
|
||||||
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mergedHeaders;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ErrInterceptor<Err, Res, Req, Options> = (
|
|
||||||
error: Err,
|
|
||||||
response: Res,
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Err | Promise<Err>;
|
|
||||||
|
|
||||||
type ReqInterceptor<Req, Options> = (
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Req | Promise<Req>;
|
|
||||||
|
|
||||||
type ResInterceptor<Res, Req, Options> = (
|
|
||||||
response: Res,
|
|
||||||
request: Req,
|
|
||||||
options: Options,
|
|
||||||
) => Res | Promise<Res>;
|
|
||||||
|
|
||||||
class Interceptors<Interceptor> {
|
|
||||||
fns: Array<Interceptor | null> = [];
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.fns = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
eject(id: number | Interceptor): void {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
if (this.fns[index]) {
|
|
||||||
this.fns[index] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exists(id: number | Interceptor): boolean {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
return Boolean(this.fns[index]);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInterceptorIndex(id: number | Interceptor): number {
|
|
||||||
if (typeof id === 'number') {
|
|
||||||
return this.fns[id] ? id : -1;
|
|
||||||
}
|
|
||||||
return this.fns.indexOf(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(
|
|
||||||
id: number | Interceptor,
|
|
||||||
fn: Interceptor,
|
|
||||||
): number | Interceptor | false {
|
|
||||||
const index = this.getInterceptorIndex(id);
|
|
||||||
if (this.fns[index]) {
|
|
||||||
this.fns[index] = fn;
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
use(fn: Interceptor): number {
|
|
||||||
this.fns.push(fn);
|
|
||||||
return this.fns.length - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Middleware<Req, Res, Err, Options> {
|
|
||||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
|
||||||
request: Interceptors<ReqInterceptor<Req, Options>>;
|
|
||||||
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
|
||||||
Req,
|
|
||||||
Res,
|
|
||||||
Err,
|
|
||||||
Options
|
|
||||||
> => ({
|
|
||||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
|
||||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
|
||||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultQuerySerializer = createQuerySerializer({
|
|
||||||
allowReserved: false,
|
|
||||||
array: {
|
|
||||||
explode: true,
|
|
||||||
style: 'form',
|
|
||||||
},
|
|
||||||
object: {
|
|
||||||
explode: true,
|
|
||||||
style: 'deepObject',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultHeaders = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
|
||||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
|
||||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
|
||||||
...jsonBodySerializer,
|
|
||||||
headers: defaultHeaders,
|
|
||||||
parseAs: 'auto',
|
|
||||||
querySerializer: defaultQuerySerializer,
|
|
||||||
...override,
|
|
||||||
});
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export type AuthToken = string | undefined;
|
|
||||||
|
|
||||||
export interface Auth {
|
|
||||||
/**
|
|
||||||
* Which part of the request do we use to send the auth?
|
|
||||||
*
|
|
||||||
* @default 'header'
|
|
||||||
*/
|
|
||||||
in?: 'header' | 'query' | 'cookie';
|
|
||||||
/**
|
|
||||||
* Header or query parameter name.
|
|
||||||
*
|
|
||||||
* @default 'Authorization'
|
|
||||||
*/
|
|
||||||
name?: string;
|
|
||||||
scheme?: 'basic' | 'bearer';
|
|
||||||
type: 'apiKey' | 'http';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAuthToken = async (
|
|
||||||
auth: Auth,
|
|
||||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
|
||||||
): Promise<string | undefined> => {
|
|
||||||
const token =
|
|
||||||
typeof callback === 'function' ? await callback(auth) : callback;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.scheme === 'bearer') {
|
|
||||||
return `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auth.scheme === 'basic') {
|
|
||||||
return `Basic ${btoa(token)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
};
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ArrayStyle,
|
|
||||||
ObjectStyle,
|
|
||||||
SerializerOptions,
|
|
||||||
} from './pathSerializer.gen';
|
|
||||||
|
|
||||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
|
||||||
|
|
||||||
export type BodySerializer = (body: any) => any;
|
|
||||||
|
|
||||||
type QuerySerializerOptionsObject = {
|
|
||||||
allowReserved?: boolean;
|
|
||||||
array?: Partial<SerializerOptions<ArrayStyle>>;
|
|
||||||
object?: Partial<SerializerOptions<ObjectStyle>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
|
||||||
/**
|
|
||||||
* Per-parameter serialization overrides. When provided, these settings
|
|
||||||
* override the global array/object settings for specific parameter names.
|
|
||||||
*/
|
|
||||||
parameters?: Record<string, QuerySerializerOptionsObject>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const serializeFormDataPair = (
|
|
||||||
data: FormData,
|
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
): void => {
|
|
||||||
if (typeof value === 'string' || value instanceof Blob) {
|
|
||||||
data.append(key, value);
|
|
||||||
} else if (value instanceof Date) {
|
|
||||||
data.append(key, value.toISOString());
|
|
||||||
} else {
|
|
||||||
data.append(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const serializeUrlSearchParamsPair = (
|
|
||||||
data: URLSearchParams,
|
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
): void => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
data.append(key, value);
|
|
||||||
} else {
|
|
||||||
data.append(key, JSON.stringify(value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const formDataBodySerializer = {
|
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
|
||||||
body: T,
|
|
||||||
): FormData => {
|
|
||||||
const data = new FormData();
|
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((v) => serializeFormDataPair(data, key, v));
|
|
||||||
} else {
|
|
||||||
serializeFormDataPair(data, key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const jsonBodySerializer = {
|
|
||||||
bodySerializer: <T>(body: T): string =>
|
|
||||||
JSON.stringify(body, (_key, value) =>
|
|
||||||
typeof value === 'bigint' ? value.toString() : value,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const urlSearchParamsBodySerializer = {
|
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
|
||||||
body: T,
|
|
||||||
): string => {
|
|
||||||
const data = new URLSearchParams();
|
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
|
||||||
} else {
|
|
||||||
serializeUrlSearchParamsPair(data, key, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return data.toString();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
type Slot = 'body' | 'headers' | 'path' | 'query';
|
|
||||||
|
|
||||||
export type Field =
|
|
||||||
| {
|
|
||||||
in: Exclude<Slot, 'body'>;
|
|
||||||
/**
|
|
||||||
* Field name. This is the name we want the user to see and use.
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/**
|
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
|
||||||
* If omitted, we use the same value as `key`.
|
|
||||||
*/
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
in: Extract<Slot, 'body'>;
|
|
||||||
/**
|
|
||||||
* Key isn't required for bodies.
|
|
||||||
*/
|
|
||||||
key?: string;
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
/**
|
|
||||||
* Field name. This is the name we want the user to see and use.
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/**
|
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
|
||||||
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
|
||||||
*/
|
|
||||||
map: Slot;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface Fields {
|
|
||||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
|
||||||
args?: ReadonlyArray<Field>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
|
||||||
|
|
||||||
const extraPrefixesMap: Record<string, Slot> = {
|
|
||||||
$body_: 'body',
|
|
||||||
$headers_: 'headers',
|
|
||||||
$path_: 'path',
|
|
||||||
$query_: 'query',
|
|
||||||
};
|
|
||||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
|
||||||
|
|
||||||
type KeyMap = Map<
|
|
||||||
string,
|
|
||||||
| {
|
|
||||||
in: Slot;
|
|
||||||
map?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
in?: never;
|
|
||||||
map: Slot;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
|
|
||||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
|
||||||
if (!map) {
|
|
||||||
map = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const config of fields) {
|
|
||||||
if ('in' in config) {
|
|
||||||
if (config.key) {
|
|
||||||
map.set(config.key, {
|
|
||||||
in: config.in,
|
|
||||||
map: config.map,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if ('key' in config) {
|
|
||||||
map.set(config.key, {
|
|
||||||
map: config.map,
|
|
||||||
});
|
|
||||||
} else if (config.args) {
|
|
||||||
buildKeyMap(config.args, map);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Params {
|
|
||||||
body: unknown;
|
|
||||||
headers: Record<string, unknown>;
|
|
||||||
path: Record<string, unknown>;
|
|
||||||
query: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stripEmptySlots = (params: Params) => {
|
|
||||||
for (const [slot, value] of Object.entries(params)) {
|
|
||||||
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
|
||||||
delete params[slot as Slot];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const buildClientParams = (
|
|
||||||
args: ReadonlyArray<unknown>,
|
|
||||||
fields: FieldsConfig,
|
|
||||||
) => {
|
|
||||||
const params: Params = {
|
|
||||||
body: {},
|
|
||||||
headers: {},
|
|
||||||
path: {},
|
|
||||||
query: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const map = buildKeyMap(fields);
|
|
||||||
|
|
||||||
let config: FieldsConfig[number] | undefined;
|
|
||||||
|
|
||||||
for (const [index, arg] of args.entries()) {
|
|
||||||
if (fields[index]) {
|
|
||||||
config = fields[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('in' in config) {
|
|
||||||
if (config.key) {
|
|
||||||
const field = map.get(config.key)!;
|
|
||||||
const name = field.map || config.key;
|
|
||||||
if (field.in) {
|
|
||||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
params.body = arg;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
|
||||||
const field = map.get(key);
|
|
||||||
|
|
||||||
if (field) {
|
|
||||||
if (field.in) {
|
|
||||||
const name = field.map || key;
|
|
||||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
|
||||||
} else {
|
|
||||||
params[field.map] = value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const extra = extraPrefixes.find(([prefix]) =>
|
|
||||||
key.startsWith(prefix),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (extra) {
|
|
||||||
const [prefix, slot] = extra;
|
|
||||||
(params[slot] as Record<string, unknown>)[
|
|
||||||
key.slice(prefix.length)
|
|
||||||
] = value;
|
|
||||||
} else if ('allowExtra' in config && config.allowExtra) {
|
|
||||||
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
|
||||||
if (allowed) {
|
|
||||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stripEmptySlots(params);
|
|
||||||
|
|
||||||
return params;
|
|
||||||
};
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
interface SerializeOptions<T>
|
|
||||||
extends SerializePrimitiveOptions,
|
|
||||||
SerializerOptions<T> {}
|
|
||||||
|
|
||||||
interface SerializePrimitiveOptions {
|
|
||||||
allowReserved?: boolean;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SerializerOptions<T> {
|
|
||||||
/**
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
explode: boolean;
|
|
||||||
style: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
|
||||||
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
|
||||||
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
|
||||||
export type ObjectStyle = 'form' | 'deepObject';
|
|
||||||
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
|
||||||
|
|
||||||
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return '.';
|
|
||||||
case 'matrix':
|
|
||||||
return ';';
|
|
||||||
case 'simple':
|
|
||||||
return ',';
|
|
||||||
default:
|
|
||||||
return '&';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'form':
|
|
||||||
return ',';
|
|
||||||
case 'pipeDelimited':
|
|
||||||
return '|';
|
|
||||||
case 'spaceDelimited':
|
|
||||||
return '%20';
|
|
||||||
default:
|
|
||||||
return ',';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return '.';
|
|
||||||
case 'matrix':
|
|
||||||
return ';';
|
|
||||||
case 'simple':
|
|
||||||
return ',';
|
|
||||||
default:
|
|
||||||
return '&';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeArrayParam = ({
|
|
||||||
allowReserved,
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value,
|
|
||||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
|
||||||
value: unknown[];
|
|
||||||
}) => {
|
|
||||||
if (!explode) {
|
|
||||||
const joinedValues = (
|
|
||||||
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
|
||||||
).join(separatorArrayNoExplode(style));
|
|
||||||
switch (style) {
|
|
||||||
case 'label':
|
|
||||||
return `.${joinedValues}`;
|
|
||||||
case 'matrix':
|
|
||||||
return `;${name}=${joinedValues}`;
|
|
||||||
case 'simple':
|
|
||||||
return joinedValues;
|
|
||||||
default:
|
|
||||||
return `${name}=${joinedValues}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const separator = separatorArrayExplode(style);
|
|
||||||
const joinedValues = value
|
|
||||||
.map((v) => {
|
|
||||||
if (style === 'label' || style === 'simple') {
|
|
||||||
return allowReserved ? v : encodeURIComponent(v as string);
|
|
||||||
}
|
|
||||||
|
|
||||||
return serializePrimitiveParam({
|
|
||||||
allowReserved,
|
|
||||||
name,
|
|
||||||
value: v as string,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.join(separator);
|
|
||||||
return style === 'label' || style === 'matrix'
|
|
||||||
? separator + joinedValues
|
|
||||||
: joinedValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializePrimitiveParam = ({
|
|
||||||
allowReserved,
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
}: SerializePrimitiveParam) => {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
throw new Error(
|
|
||||||
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const serializeObjectParam = ({
|
|
||||||
allowReserved,
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value,
|
|
||||||
valueOnly,
|
|
||||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
|
||||||
value: Record<string, unknown> | Date;
|
|
||||||
valueOnly?: boolean;
|
|
||||||
}) => {
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style !== 'deepObject' && !explode) {
|
|
||||||
let values: string[] = [];
|
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
|
||||||
values = [
|
|
||||||
...values,
|
|
||||||
key,
|
|
||||||
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
const joinedValues = values.join(',');
|
|
||||||
switch (style) {
|
|
||||||
case 'form':
|
|
||||||
return `${name}=${joinedValues}`;
|
|
||||||
case 'label':
|
|
||||||
return `.${joinedValues}`;
|
|
||||||
case 'matrix':
|
|
||||||
return `;${name}=${joinedValues}`;
|
|
||||||
default:
|
|
||||||
return joinedValues;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const separator = separatorObjectExplode(style);
|
|
||||||
const joinedValues = Object.entries(value)
|
|
||||||
.map(([key, v]) =>
|
|
||||||
serializePrimitiveParam({
|
|
||||||
allowReserved,
|
|
||||||
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
|
||||||
value: v as string,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.join(separator);
|
|
||||||
return style === 'label' || style === 'matrix'
|
|
||||||
? separator + joinedValues
|
|
||||||
: joinedValues;
|
|
||||||
};
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
|
||||||
*/
|
|
||||||
export type JsonValue =
|
|
||||||
| null
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| JsonValue[]
|
|
||||||
| { [key: string]: JsonValue };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
|
||||||
*/
|
|
||||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
|
||||||
if (
|
|
||||||
value === undefined ||
|
|
||||||
typeof value === 'function' ||
|
|
||||||
typeof value === 'symbol'
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if (typeof value === 'bigint') {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely stringifies a value and parses it back into a JsonValue.
|
|
||||||
*/
|
|
||||||
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
|
||||||
try {
|
|
||||||
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
|
||||||
if (json === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return JSON.parse(json) as JsonValue;
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detects plain objects (including objects with a null prototype).
|
|
||||||
*/
|
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
||||||
if (value === null || typeof value !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const prototype = Object.getPrototypeOf(value as object);
|
|
||||||
return prototype === Object.prototype || prototype === null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
|
||||||
*/
|
|
||||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
|
||||||
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
|
||||||
a.localeCompare(b),
|
|
||||||
);
|
|
||||||
const result: Record<string, JsonValue> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
const existing = result[key];
|
|
||||||
if (existing === undefined) {
|
|
||||||
result[key] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(existing)) {
|
|
||||||
(existing as string[]).push(value);
|
|
||||||
} else {
|
|
||||||
result[key] = [existing, value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
|
||||||
*/
|
|
||||||
export const serializeQueryKeyValue = (
|
|
||||||
value: unknown,
|
|
||||||
): JsonValue | undefined => {
|
|
||||||
if (value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof value === 'string' ||
|
|
||||||
typeof value === 'number' ||
|
|
||||||
typeof value === 'boolean'
|
|
||||||
) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
value === undefined ||
|
|
||||||
typeof value === 'function' ||
|
|
||||||
typeof value === 'symbol'
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'bigint') {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return value.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return stringifyToJsonValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof URLSearchParams !== 'undefined' &&
|
|
||||||
value instanceof URLSearchParams
|
|
||||||
) {
|
|
||||||
return serializeSearchParams(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
|
||||||
return stringifyToJsonValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Config } from './types.gen';
|
|
||||||
|
|
||||||
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
|
||||||
RequestInit,
|
|
||||||
'method'
|
|
||||||
> &
|
|
||||||
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
|
||||||
/**
|
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
|
||||||
* fetch instance.
|
|
||||||
*
|
|
||||||
* @default globalThis.fetch
|
|
||||||
*/
|
|
||||||
fetch?: typeof fetch;
|
|
||||||
/**
|
|
||||||
* Implementing clients can call request interceptors inside this hook.
|
|
||||||
*/
|
|
||||||
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
|
||||||
/**
|
|
||||||
* Callback invoked when a network or parsing error occurs during streaming.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @param error The error that occurred.
|
|
||||||
*/
|
|
||||||
onSseError?: (error: unknown) => void;
|
|
||||||
/**
|
|
||||||
* Callback invoked when an event is streamed from the server.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @param event Event streamed from the server.
|
|
||||||
* @returns Nothing (void).
|
|
||||||
*/
|
|
||||||
onSseEvent?: (event: StreamEvent<TData>) => void;
|
|
||||||
serializedBody?: RequestInit['body'];
|
|
||||||
/**
|
|
||||||
* Default retry delay in milliseconds.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @default 3000
|
|
||||||
*/
|
|
||||||
sseDefaultRetryDelay?: number;
|
|
||||||
/**
|
|
||||||
* Maximum number of retry attempts before giving up.
|
|
||||||
*/
|
|
||||||
sseMaxRetryAttempts?: number;
|
|
||||||
/**
|
|
||||||
* Maximum retry delay in milliseconds.
|
|
||||||
*
|
|
||||||
* Applies only when exponential backoff is used.
|
|
||||||
*
|
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
|
||||||
*
|
|
||||||
* @default 30000
|
|
||||||
*/
|
|
||||||
sseMaxRetryDelay?: number;
|
|
||||||
/**
|
|
||||||
* Optional sleep function for retry backoff.
|
|
||||||
*
|
|
||||||
* Defaults to using `setTimeout`.
|
|
||||||
*/
|
|
||||||
sseSleepFn?: (ms: number) => Promise<void>;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface StreamEvent<TData = unknown> {
|
|
||||||
data: TData;
|
|
||||||
event?: string;
|
|
||||||
id?: string;
|
|
||||||
retry?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerSentEventsResult<
|
|
||||||
TData = unknown,
|
|
||||||
TReturn = void,
|
|
||||||
TNext = unknown,
|
|
||||||
> = {
|
|
||||||
stream: AsyncGenerator<
|
|
||||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
|
||||||
TReturn,
|
|
||||||
TNext
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createSseClient = <TData = unknown>({
|
|
||||||
onRequest,
|
|
||||||
onSseError,
|
|
||||||
onSseEvent,
|
|
||||||
responseTransformer,
|
|
||||||
responseValidator,
|
|
||||||
sseDefaultRetryDelay,
|
|
||||||
sseMaxRetryAttempts,
|
|
||||||
sseMaxRetryDelay,
|
|
||||||
sseSleepFn,
|
|
||||||
url,
|
|
||||||
...options
|
|
||||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
|
||||||
let lastEventId: string | undefined;
|
|
||||||
|
|
||||||
const sleep =
|
|
||||||
sseSleepFn ??
|
|
||||||
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
||||||
|
|
||||||
const createStream = async function* () {
|
|
||||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
|
||||||
let attempt = 0;
|
|
||||||
const signal = options.signal ?? new AbortController().signal;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (signal.aborted) break;
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
|
|
||||||
const headers =
|
|
||||||
options.headers instanceof Headers
|
|
||||||
? options.headers
|
|
||||||
: new Headers(options.headers as Record<string, string> | undefined);
|
|
||||||
|
|
||||||
if (lastEventId !== undefined) {
|
|
||||||
headers.set('Last-Event-ID', lastEventId);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const requestInit: RequestInit = {
|
|
||||||
redirect: 'follow',
|
|
||||||
...options,
|
|
||||||
body: options.serializedBody,
|
|
||||||
headers,
|
|
||||||
signal,
|
|
||||||
};
|
|
||||||
let request = new Request(url, requestInit);
|
|
||||||
if (onRequest) {
|
|
||||||
request = await onRequest(url, requestInit);
|
|
||||||
}
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
|
||||||
const _fetch = options.fetch ?? globalThis.fetch;
|
|
||||||
const response = await _fetch(request);
|
|
||||||
|
|
||||||
if (!response.ok)
|
|
||||||
throw new Error(
|
|
||||||
`SSE failed: ${response.status} ${response.statusText}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.body) throw new Error('No body in SSE response');
|
|
||||||
|
|
||||||
const reader = response.body
|
|
||||||
.pipeThrough(new TextDecoderStream())
|
|
||||||
.getReader();
|
|
||||||
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
const abortHandler = () => {
|
|
||||||
try {
|
|
||||||
reader.cancel();
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
signal.addEventListener('abort', abortHandler);
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
buffer += value;
|
|
||||||
// Normalize line endings: CRLF -> LF, then CR -> LF
|
|
||||||
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
||||||
|
|
||||||
const chunks = buffer.split('\n\n');
|
|
||||||
buffer = chunks.pop() ?? '';
|
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
const lines = chunk.split('\n');
|
|
||||||
const dataLines: Array<string> = [];
|
|
||||||
let eventName: string | undefined;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith('data:')) {
|
|
||||||
dataLines.push(line.replace(/^data:\s*/, ''));
|
|
||||||
} else if (line.startsWith('event:')) {
|
|
||||||
eventName = line.replace(/^event:\s*/, '');
|
|
||||||
} else if (line.startsWith('id:')) {
|
|
||||||
lastEventId = line.replace(/^id:\s*/, '');
|
|
||||||
} else if (line.startsWith('retry:')) {
|
|
||||||
const parsed = Number.parseInt(
|
|
||||||
line.replace(/^retry:\s*/, ''),
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
if (!Number.isNaN(parsed)) {
|
|
||||||
retryDelay = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data: unknown;
|
|
||||||
let parsedJson = false;
|
|
||||||
|
|
||||||
if (dataLines.length) {
|
|
||||||
const rawData = dataLines.join('\n');
|
|
||||||
try {
|
|
||||||
data = JSON.parse(rawData);
|
|
||||||
parsedJson = true;
|
|
||||||
} catch {
|
|
||||||
data = rawData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedJson) {
|
|
||||||
if (responseValidator) {
|
|
||||||
await responseValidator(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseTransformer) {
|
|
||||||
data = await responseTransformer(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSseEvent?.({
|
|
||||||
data,
|
|
||||||
event: eventName,
|
|
||||||
id: lastEventId,
|
|
||||||
retry: retryDelay,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (dataLines.length) {
|
|
||||||
yield data as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
signal.removeEventListener('abort', abortHandler);
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
break; // exit loop on normal completion
|
|
||||||
} catch (error) {
|
|
||||||
// connection failed or aborted; retry after delay
|
|
||||||
onSseError?.(error);
|
|
||||||
|
|
||||||
if (
|
|
||||||
sseMaxRetryAttempts !== undefined &&
|
|
||||||
attempt >= sseMaxRetryAttempts
|
|
||||||
) {
|
|
||||||
break; // stop after firing error
|
|
||||||
}
|
|
||||||
|
|
||||||
// exponential backoff: double retry each attempt, cap at 30s
|
|
||||||
const backoff = Math.min(
|
|
||||||
retryDelay * 2 ** (attempt - 1),
|
|
||||||
sseMaxRetryDelay ?? 30000,
|
|
||||||
);
|
|
||||||
await sleep(backoff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stream = createStream();
|
|
||||||
|
|
||||||
return { stream };
|
|
||||||
};
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Auth, AuthToken } from './auth.gen';
|
|
||||||
import type {
|
|
||||||
BodySerializer,
|
|
||||||
QuerySerializer,
|
|
||||||
QuerySerializerOptions,
|
|
||||||
} from './bodySerializer.gen';
|
|
||||||
|
|
||||||
export type HttpMethod =
|
|
||||||
| 'connect'
|
|
||||||
| 'delete'
|
|
||||||
| 'get'
|
|
||||||
| 'head'
|
|
||||||
| 'options'
|
|
||||||
| 'patch'
|
|
||||||
| 'post'
|
|
||||||
| 'put'
|
|
||||||
| 'trace';
|
|
||||||
|
|
||||||
export type Client<
|
|
||||||
RequestFn = never,
|
|
||||||
Config = unknown,
|
|
||||||
MethodFn = never,
|
|
||||||
BuildUrlFn = never,
|
|
||||||
SseFn = never,
|
|
||||||
> = {
|
|
||||||
/**
|
|
||||||
* Returns the final request URL.
|
|
||||||
*/
|
|
||||||
buildUrl: BuildUrlFn;
|
|
||||||
getConfig: () => Config;
|
|
||||||
request: RequestFn;
|
|
||||||
setConfig: (config: Config) => Config;
|
|
||||||
} & {
|
|
||||||
[K in HttpMethod]: MethodFn;
|
|
||||||
} & ([SseFn] extends [never]
|
|
||||||
? { sse?: never }
|
|
||||||
: { sse: { [K in HttpMethod]: SseFn } });
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
/**
|
|
||||||
* Auth token or a function returning auth token. The resolved value will be
|
|
||||||
* added to the request payload as defined by its `security` array.
|
|
||||||
*/
|
|
||||||
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
|
||||||
/**
|
|
||||||
* A function for serializing request body parameter. By default,
|
|
||||||
* {@link JSON.stringify()} will be used.
|
|
||||||
*/
|
|
||||||
bodySerializer?: BodySerializer | null;
|
|
||||||
/**
|
|
||||||
* An object containing any HTTP headers that you want to pre-populate your
|
|
||||||
* `Headers` object with.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
|
||||||
*/
|
|
||||||
headers?:
|
|
||||||
| RequestInit['headers']
|
|
||||||
| Record<
|
|
||||||
string,
|
|
||||||
| string
|
|
||||||
| number
|
|
||||||
| boolean
|
|
||||||
| (string | number | boolean)[]
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
| unknown
|
|
||||||
>;
|
|
||||||
/**
|
|
||||||
* The request method.
|
|
||||||
*
|
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
|
||||||
*/
|
|
||||||
method?: Uppercase<HttpMethod>;
|
|
||||||
/**
|
|
||||||
* A function for serializing request query parameters. By default, arrays
|
|
||||||
* will be exploded in form style, objects will be exploded in deepObject
|
|
||||||
* style, and reserved characters are percent-encoded.
|
|
||||||
*
|
|
||||||
* This method will have no effect if the native `paramsSerializer()` Axios
|
|
||||||
* API function is used.
|
|
||||||
*
|
|
||||||
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
|
||||||
*/
|
|
||||||
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
|
||||||
/**
|
|
||||||
* A function validating request data. This is useful if you want to ensure
|
|
||||||
* the request conforms to the desired shape, so it can be safely sent to
|
|
||||||
* the server.
|
|
||||||
*/
|
|
||||||
requestValidator?: (data: unknown) => Promise<unknown>;
|
|
||||||
/**
|
|
||||||
* A function transforming response data before it's returned. This is useful
|
|
||||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
|
||||||
*/
|
|
||||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
|
||||||
/**
|
|
||||||
* A function validating response data. This is useful if you want to ensure
|
|
||||||
* the response conforms to the desired shape, so it can be safely passed to
|
|
||||||
* the transformers and returned to the user.
|
|
||||||
*/
|
|
||||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
|
||||||
? true
|
|
||||||
: [T] extends [never | undefined]
|
|
||||||
? [undefined] extends [T]
|
|
||||||
? false
|
|
||||||
: true
|
|
||||||
: false;
|
|
||||||
|
|
||||||
export type OmitNever<T extends Record<string, unknown>> = {
|
|
||||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
|
||||||
? never
|
|
||||||
: K]: T[K];
|
|
||||||
};
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
|
||||||
import {
|
|
||||||
type ArraySeparatorStyle,
|
|
||||||
serializeArrayParam,
|
|
||||||
serializeObjectParam,
|
|
||||||
serializePrimitiveParam,
|
|
||||||
} from './pathSerializer.gen';
|
|
||||||
|
|
||||||
export interface PathSerializer {
|
|
||||||
path: Record<string, unknown>;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
|
||||||
|
|
||||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
|
||||||
let url = _url;
|
|
||||||
const matches = _url.match(PATH_PARAM_RE);
|
|
||||||
if (matches) {
|
|
||||||
for (const match of matches) {
|
|
||||||
let explode = false;
|
|
||||||
let name = match.substring(1, match.length - 1);
|
|
||||||
let style: ArraySeparatorStyle = 'simple';
|
|
||||||
|
|
||||||
if (name.endsWith('*')) {
|
|
||||||
explode = true;
|
|
||||||
name = name.substring(0, name.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name.startsWith('.')) {
|
|
||||||
name = name.substring(1);
|
|
||||||
style = 'label';
|
|
||||||
} else if (name.startsWith(';')) {
|
|
||||||
name = name.substring(1);
|
|
||||||
style = 'matrix';
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = path[name];
|
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
serializeArrayParam({ explode, name, style, value }),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
serializeObjectParam({
|
|
||||||
explode,
|
|
||||||
name,
|
|
||||||
style,
|
|
||||||
value: value as Record<string, unknown>,
|
|
||||||
valueOnly: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (style === 'matrix') {
|
|
||||||
url = url.replace(
|
|
||||||
match,
|
|
||||||
`;${serializePrimitiveParam({
|
|
||||||
name,
|
|
||||||
value: value as string,
|
|
||||||
})}`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceValue = encodeURIComponent(
|
|
||||||
style === 'label' ? `.${value as string}` : (value as string),
|
|
||||||
);
|
|
||||||
url = url.replace(match, replaceValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUrl = ({
|
|
||||||
baseUrl,
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
querySerializer,
|
|
||||||
url: _url,
|
|
||||||
}: {
|
|
||||||
baseUrl?: string;
|
|
||||||
path?: Record<string, unknown>;
|
|
||||||
query?: Record<string, unknown>;
|
|
||||||
querySerializer: QuerySerializer;
|
|
||||||
url: string;
|
|
||||||
}) => {
|
|
||||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
|
||||||
let url = (baseUrl ?? '') + pathUrl;
|
|
||||||
if (path) {
|
|
||||||
url = defaultPathSerializer({ path, url });
|
|
||||||
}
|
|
||||||
let search = query ? querySerializer(query) : '';
|
|
||||||
if (search.startsWith('?')) {
|
|
||||||
search = search.substring(1);
|
|
||||||
}
|
|
||||||
if (search) {
|
|
||||||
url += `?${search}`;
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getValidRequestBody(options: {
|
|
||||||
body?: unknown;
|
|
||||||
bodySerializer?: BodySerializer | null;
|
|
||||||
serializedBody?: unknown;
|
|
||||||
}) {
|
|
||||||
const hasBody = options.body !== undefined;
|
|
||||||
const isSerializedBody = hasBody && options.bodySerializer;
|
|
||||||
|
|
||||||
if (isSerializedBody) {
|
|
||||||
if ('serializedBody' in options) {
|
|
||||||
const hasSerializedBody =
|
|
||||||
options.serializedBody !== undefined && options.serializedBody !== '';
|
|
||||||
|
|
||||||
return hasSerializedBody ? options.serializedBody : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
|
||||||
return options.body !== '' ? options.body : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// plain/text body
|
|
||||||
if (hasBody) {
|
|
||||||
return options.body;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no body was provided
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
export { getAuthRedirect, getEventAttendance, getEventCheckin, getEventCheckinQuery, getEventInfo, getEventJoined, getEventList, getUserInfo, getUserInfoByUserId, getUserList, type Options, patchUserUpdate, postAgendaSubmit, postAuthExchange, postAuthMagic, postAuthRefresh, postAuthToken, postEventCheckinSubmit, postEventJoin, postKycQuery, postKycSession } from './sdk.gen';
|
|
||||||
export type { ClientOptions, DataEventIndexDoc, DataUserIndexDoc, GetAuthRedirectData, GetAuthRedirectError, GetAuthRedirectErrors, GetEventAttendanceData, GetEventAttendanceError, GetEventAttendanceErrors, GetEventAttendanceResponse, GetEventAttendanceResponses, GetEventCheckinData, GetEventCheckinError, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryError, GetEventCheckinQueryErrors, GetEventCheckinQueryResponse, GetEventCheckinQueryResponses, GetEventCheckinResponse, GetEventCheckinResponses, GetEventInfoData, GetEventInfoError, GetEventInfoErrors, GetEventInfoResponse, GetEventInfoResponses, GetEventJoinedData, GetEventJoinedError, GetEventJoinedErrors, GetEventJoinedResponse, GetEventJoinedResponses, GetEventListData, GetEventListError, GetEventListErrors, GetEventListResponse, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdError, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponse, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoError, GetUserInfoErrors, GetUserInfoResponse, GetUserInfoResponses, GetUserListData, GetUserListError, GetUserListErrors, GetUserListResponse, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateError, PatchUserUpdateErrors, PatchUserUpdateResponse, PatchUserUpdateResponses, PostAgendaSubmitData, PostAgendaSubmitError, PostAgendaSubmitErrors, PostAgendaSubmitResponse, PostAgendaSubmitResponses, PostAuthExchangeData, PostAuthExchangeError, PostAuthExchangeErrors, PostAuthExchangeResponse, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicError, PostAuthMagicErrors, PostAuthMagicResponse, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshError, PostAuthRefreshErrors, PostAuthRefreshResponse, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenError, PostAuthTokenErrors, PostAuthTokenResponse, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitError, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponse, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinError, PostEventJoinErrors, PostEventJoinResponse, PostEventJoinResponses, PostKycQueryData, PostKycQueryError, PostKycQueryErrors, PostKycQueryResponse, PostKycQueryResponses, PostKycSessionData, PostKycSessionError, PostKycSessionErrors, PostKycSessionResponse, PostKycSessionResponses, ServiceAgendaSubmitData, ServiceAgendaSubmitResponse, ServiceAuthExchangeData, ServiceAuthExchangeResponse, ServiceAuthMagicData, ServiceAuthMagicResponse, ServiceAuthRefreshData, ServiceAuthTokenData, ServiceAuthTokenResponse, ServiceEventAttendanceListResponse, ServiceEventCheckinQueryResponse, ServiceEventCheckinResponse, ServiceEventCheckinSubmitData, ServiceEventEventJoinData, ServiceEventEventJoinResponse, ServiceKycKycQueryData, ServiceKycKycQueryResponse, ServiceKycKycSessionData, ServiceKycKycSessionResponse, ServiceUserUserInfoData, UtilsRespStatus } from './types.gen';
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import type { Client, Options as Options2, TDataShape } from './client';
|
|
||||||
import { client } from './client.gen';
|
|
||||||
import type { GetAuthRedirectData, GetAuthRedirectErrors, GetEventAttendanceData, GetEventAttendanceErrors, GetEventAttendanceResponses, GetEventCheckinData, GetEventCheckinErrors, GetEventCheckinQueryData, GetEventCheckinQueryErrors, GetEventCheckinQueryResponses, GetEventCheckinResponses, GetEventInfoData, GetEventInfoErrors, GetEventInfoResponses, GetEventJoinedData, GetEventJoinedErrors, GetEventJoinedResponses, GetEventListData, GetEventListErrors, GetEventListResponses, GetUserInfoByUserIdData, GetUserInfoByUserIdErrors, GetUserInfoByUserIdResponses, GetUserInfoData, GetUserInfoErrors, GetUserInfoResponses, GetUserListData, GetUserListErrors, GetUserListResponses, PatchUserUpdateData, PatchUserUpdateErrors, PatchUserUpdateResponses, PostAgendaSubmitData, PostAgendaSubmitErrors, PostAgendaSubmitResponses, PostAuthExchangeData, PostAuthExchangeErrors, PostAuthExchangeResponses, PostAuthMagicData, PostAuthMagicErrors, PostAuthMagicResponses, PostAuthRefreshData, PostAuthRefreshErrors, PostAuthRefreshResponses, PostAuthTokenData, PostAuthTokenErrors, PostAuthTokenResponses, PostEventCheckinSubmitData, PostEventCheckinSubmitErrors, PostEventCheckinSubmitResponses, PostEventJoinData, PostEventJoinErrors, PostEventJoinResponses, PostKycQueryData, PostKycQueryErrors, PostKycQueryResponses, PostKycSessionData, PostKycSessionErrors, PostKycSessionResponses } from './types.gen';
|
|
||||||
|
|
||||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
|
||||||
/**
|
|
||||||
* You can provide a client instance returned by `createClient()` instead of
|
|
||||||
* individual options. This might be also useful if you want to implement a
|
|
||||||
* custom client.
|
|
||||||
*/
|
|
||||||
client?: Client;
|
|
||||||
/**
|
|
||||||
* You can pass arbitrary values through the `meta` object. This can be
|
|
||||||
* used to access values that aren't defined as part of the SDK function.
|
|
||||||
*/
|
|
||||||
meta?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit Agenda
|
|
||||||
*
|
|
||||||
* Creates a new agenda item for a specific attendance record.
|
|
||||||
*/
|
|
||||||
export const postAgendaSubmit = <ThrowOnError extends boolean = false>(options: Options<PostAgendaSubmitData, ThrowOnError>) => (options.client ?? client).post<PostAgendaSubmitResponses, PostAgendaSubmitErrors, ThrowOnError>({
|
|
||||||
url: '/agenda/submit',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Auth Code
|
|
||||||
*
|
|
||||||
* Exchanges client credentials and user session for a specific redirect authorization code.
|
|
||||||
*/
|
|
||||||
export const postAuthExchange = <ThrowOnError extends boolean = false>(options: Options<PostAuthExchangeData, ThrowOnError>) => (options.client ?? client).post<PostAuthExchangeResponses, PostAuthExchangeErrors, ThrowOnError>({
|
|
||||||
url: '/auth/exchange',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request Magic Link
|
|
||||||
*
|
|
||||||
* Verifies Turnstile token and sends an authentication link via email. Returns the URI directly if debug mode is enabled.
|
|
||||||
*/
|
|
||||||
export const postAuthMagic = <ThrowOnError extends boolean = false>(options: Options<PostAuthMagicData, ThrowOnError>) => (options.client ?? client).post<PostAuthMagicResponses, PostAuthMagicErrors, ThrowOnError>({
|
|
||||||
url: '/auth/magic',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle Auth Callback and Redirect
|
|
||||||
*
|
|
||||||
* Verifies the temporary email code, ensures the user exists (or creates one), validates the client's redirect URI, and finally performs a 302 redirect with a new authorization code.
|
|
||||||
*/
|
|
||||||
export const getAuthRedirect = <ThrowOnError extends boolean = false>(options: Options<GetAuthRedirectData, ThrowOnError>) => (options.client ?? client).get<unknown, GetAuthRedirectErrors, ThrowOnError>({ url: '/auth/redirect', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh Access Token
|
|
||||||
*
|
|
||||||
* Accepts a valid refresh token to issue a new access token and a rotated refresh token.
|
|
||||||
*/
|
|
||||||
export const postAuthRefresh = <ThrowOnError extends boolean = false>(options: Options<PostAuthRefreshData, ThrowOnError>) => (options.client ?? client).post<PostAuthRefreshResponses, PostAuthRefreshErrors, ThrowOnError>({
|
|
||||||
url: '/auth/refresh',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Code for Token
|
|
||||||
*
|
|
||||||
* Verifies the provided authorization code and issues a pair of JWT tokens (Access and Refresh).
|
|
||||||
*/
|
|
||||||
export const postAuthToken = <ThrowOnError extends boolean = false>(options: Options<PostAuthTokenData, ThrowOnError>) => (options.client ?? client).post<PostAuthTokenResponses, PostAuthTokenErrors, ThrowOnError>({
|
|
||||||
url: '/auth/token',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Attendance List
|
|
||||||
*
|
|
||||||
* Retrieves the list of attendees, including user info and decrypted KYC data for a specified event.
|
|
||||||
*/
|
|
||||||
export const getEventAttendance = <ThrowOnError extends boolean = false>(options: Options<GetEventAttendanceData, ThrowOnError>) => (options.client ?? client).get<GetEventAttendanceResponses, GetEventAttendanceErrors, ThrowOnError>({ url: '/event/attendance', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Check-in Code
|
|
||||||
*
|
|
||||||
* Creates a temporary check-in code for the authenticated user and event.
|
|
||||||
*/
|
|
||||||
export const getEventCheckin = <ThrowOnError extends boolean = false>(options: Options<GetEventCheckinData, ThrowOnError>) => (options.client ?? client).get<GetEventCheckinResponses, GetEventCheckinErrors, ThrowOnError>({ url: '/event/checkin', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query Check-in Status
|
|
||||||
*
|
|
||||||
* Returns the timestamp of when the user checked in, or null if not yet checked in.
|
|
||||||
*/
|
|
||||||
export const getEventCheckinQuery = <ThrowOnError extends boolean = false>(options: Options<GetEventCheckinQueryData, ThrowOnError>) => (options.client ?? client).get<GetEventCheckinQueryResponses, GetEventCheckinQueryErrors, ThrowOnError>({ url: '/event/checkin/query', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit Check-in Code
|
|
||||||
*
|
|
||||||
* Submits the generated code to mark the user as attended.
|
|
||||||
*/
|
|
||||||
export const postEventCheckinSubmit = <ThrowOnError extends boolean = false>(options: Options<PostEventCheckinSubmitData, ThrowOnError>) => (options.client ?? client).post<PostEventCheckinSubmitResponses, PostEventCheckinSubmitErrors, ThrowOnError>({
|
|
||||||
url: '/event/checkin/submit',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Event Information
|
|
||||||
*
|
|
||||||
* Fetches the name, start time, and end time of an event using its UUID.
|
|
||||||
*/
|
|
||||||
export const getEventInfo = <ThrowOnError extends boolean = false>(options: Options<GetEventInfoData, ThrowOnError>) => (options.client ?? client).get<GetEventInfoResponses, GetEventInfoErrors, ThrowOnError>({ url: '/event/info', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join an Event
|
|
||||||
*
|
|
||||||
* Allows an authenticated user to join an event by providing the event ID. The user's role and state are initialized by the service.
|
|
||||||
*/
|
|
||||||
export const postEventJoin = <ThrowOnError extends boolean = false>(options: Options<PostEventJoinData, ThrowOnError>) => (options.client ?? client).post<PostEventJoinResponses, PostEventJoinErrors, ThrowOnError>({
|
|
||||||
url: '/event/join',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Joined Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events where the authenticated user is a participant. Supports pagination.
|
|
||||||
*/
|
|
||||||
export const getEventJoined = <ThrowOnError extends boolean = false>(options?: Options<GetEventJoinedData, ThrowOnError>) => (options?.client ?? client).get<GetEventJoinedResponses, GetEventJoinedErrors, ThrowOnError>({ url: '/event/joined', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Events
|
|
||||||
*
|
|
||||||
* Fetches a list of events with support for pagination via limit and offset. Data is retrieved directly from the database for consistency.
|
|
||||||
*/
|
|
||||||
export const getEventList = <ThrowOnError extends boolean = false>(options?: Options<GetEventListData, ThrowOnError>) => (options?.client ?? client).get<GetEventListResponses, GetEventListErrors, ThrowOnError>({ url: '/event/list', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query KYC Status
|
|
||||||
*
|
|
||||||
* Checks the current state of a KYC session and updates local database if approved.
|
|
||||||
*/
|
|
||||||
export const postKycQuery = <ThrowOnError extends boolean = false>(options: Options<PostKycQueryData, ThrowOnError>) => (options.client ?? client).post<PostKycQueryResponses, PostKycQueryErrors, ThrowOnError>({
|
|
||||||
url: '/kyc/query',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create KYC Session
|
|
||||||
*
|
|
||||||
* Initializes a KYC process (CNRid or Passport) and returns the status or redirect URI.
|
|
||||||
*/
|
|
||||||
export const postKycSession = <ThrowOnError extends boolean = false>(options: Options<PostKycSessionData, ThrowOnError>) => (options.client ?? client).post<PostKycSessionResponses, PostKycSessionErrors, ThrowOnError>({
|
|
||||||
url: '/kyc/session',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get My User Information
|
|
||||||
*
|
|
||||||
* Fetches the complete profile data for the user associated with the provided session/token.
|
|
||||||
*/
|
|
||||||
export const getUserInfo = <ThrowOnError extends boolean = false>(options?: Options<GetUserInfoData, ThrowOnError>) => (options?.client ?? client).get<GetUserInfoResponses, GetUserInfoErrors, ThrowOnError>({ url: '/user/info', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Other User Information
|
|
||||||
*
|
|
||||||
* Fetches the complete profile data for the user associated with the provided session/token.
|
|
||||||
*/
|
|
||||||
export const getUserInfoByUserId = <ThrowOnError extends boolean = false>(options: Options<GetUserInfoByUserIdData, ThrowOnError>) => (options.client ?? client).get<GetUserInfoByUserIdResponses, GetUserInfoByUserIdErrors, ThrowOnError>({ url: '/user/info/{user_id}', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List Users
|
|
||||||
*
|
|
||||||
* Fetches a list of users with support for pagination via limit and offset. Data is sourced from the search engine for high performance.
|
|
||||||
*/
|
|
||||||
export const getUserList = <ThrowOnError extends boolean = false>(options: Options<GetUserListData, ThrowOnError>) => (options.client ?? client).get<GetUserListResponses, GetUserListErrors, ThrowOnError>({ url: '/user/list', ...options });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update User Information
|
|
||||||
*
|
|
||||||
* Updates specific profile fields such as username, nickname, subtitle, avatar (URL), and bio (Base64).
|
|
||||||
* Validation: Username (5-255 chars), Nickname (max 24 chars), Subtitle (max 32 chars).
|
|
||||||
*/
|
|
||||||
export const patchUserUpdate = <ThrowOnError extends boolean = false>(options: Options<PatchUserUpdateData, ThrowOnError>) => (options.client ?? client).patch<PatchUserUpdateResponses, PatchUserUpdateErrors, ThrowOnError>({
|
|
||||||
url: '/user/update',
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
});
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,417 +0,0 @@
|
|||||||
// This file is auto-generated by @hey-api/openapi-ts
|
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const zDataEventIndexDoc = z.object({
|
|
||||||
checkin_count: z.number().int().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
enable_kyc: z.boolean().optional(),
|
|
||||||
end_time: z.string().optional(),
|
|
||||||
event_id: z.string().optional(),
|
|
||||||
is_checked_in: z.boolean().optional(),
|
|
||||||
is_joined: z.boolean().optional(),
|
|
||||||
join_count: z.number().int().optional(),
|
|
||||||
name: z.string().optional(),
|
|
||||||
start_time: z.string().optional(),
|
|
||||||
thumbnail: z.string().optional(),
|
|
||||||
type: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zDataUserIndexDoc = z.object({
|
|
||||||
avatar: z.string().optional(),
|
|
||||||
email: z.string().optional(),
|
|
||||||
nickname: z.string().optional(),
|
|
||||||
subtitle: z.string().optional(),
|
|
||||||
type: z.string().optional(),
|
|
||||||
user_id: z.string().optional(),
|
|
||||||
username: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAgendaSubmitData = z.object({
|
|
||||||
description: z.string().optional(),
|
|
||||||
event_id: z.string().optional(),
|
|
||||||
name: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAgendaSubmitResponse = z.object({
|
|
||||||
agenda_id: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthExchangeData = z.object({
|
|
||||||
client_id: z.string().optional(),
|
|
||||||
redirect_uri: z.string().optional(),
|
|
||||||
state: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthExchangeResponse = z.object({
|
|
||||||
redirect_uri: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthMagicData = z.object({
|
|
||||||
client_id: z.string().optional(),
|
|
||||||
client_ip: z.string().optional(),
|
|
||||||
email: z.string().optional(),
|
|
||||||
redirect_uri: z.string().optional(),
|
|
||||||
state: z.string().optional(),
|
|
||||||
turnstile_token: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthMagicResponse = z.object({
|
|
||||||
uri: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthRefreshData = z.object({
|
|
||||||
refresh_token: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthTokenData = z.object({
|
|
||||||
code: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceAuthTokenResponse = z.object({
|
|
||||||
access_token: z.string().optional(),
|
|
||||||
refresh_token: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventCheckinQueryResponse = z.object({
|
|
||||||
checkin_at: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventCheckinResponse = z.object({
|
|
||||||
checkin_code: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventCheckinSubmitData = z.object({
|
|
||||||
checkin_code: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventEventJoinData = z.object({
|
|
||||||
event_id: z.string().optional(),
|
|
||||||
kyc_id: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventEventJoinResponse = z.object({
|
|
||||||
attendance_id: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceKycKycQueryData = z.object({
|
|
||||||
kyc_id: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceKycKycQueryResponse = z.object({
|
|
||||||
status: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceKycKycSessionData = z.object({
|
|
||||||
identity: z.string().optional(),
|
|
||||||
type: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceKycKycSessionResponse = z.object({
|
|
||||||
kyc_id: z.string().optional(),
|
|
||||||
redirect_uri: z.string().optional(),
|
|
||||||
status: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceUserUserInfoData = z.object({
|
|
||||||
allow_public: z.boolean().optional(),
|
|
||||||
avatar: z.string().optional(),
|
|
||||||
bio: z.string().optional(),
|
|
||||||
email: z.string().optional(),
|
|
||||||
nickname: z.string().optional(),
|
|
||||||
permission_level: z.number().int().optional(),
|
|
||||||
subtitle: z.string().optional(),
|
|
||||||
user_id: z.string().optional(),
|
|
||||||
username: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zServiceEventAttendanceListResponse = z.object({
|
|
||||||
attendance_id: z.string().optional(),
|
|
||||||
kyc_info: z.unknown().optional(),
|
|
||||||
kyc_type: z.string().optional(),
|
|
||||||
user_info: zServiceUserUserInfoData.optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zUtilsRespStatus = z.object({
|
|
||||||
code: z.number().int().optional(),
|
|
||||||
data: z.unknown().optional(),
|
|
||||||
error_id: z.string().optional(),
|
|
||||||
status: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zPostAgendaSubmitData = z.object({
|
|
||||||
body: zServiceAgendaSubmitData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OK
|
|
||||||
*/
|
|
||||||
export const zPostAgendaSubmitResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceAgendaSubmitResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostAuthExchangeData = z.object({
|
|
||||||
body: zServiceAuthExchangeData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful exchange
|
|
||||||
*/
|
|
||||||
export const zPostAuthExchangeResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceAuthExchangeResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostAuthMagicData = z.object({
|
|
||||||
body: zServiceAuthMagicData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful request
|
|
||||||
*/
|
|
||||||
export const zPostAuthMagicResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceAuthMagicResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetAuthRedirectData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
client_id: z.string(),
|
|
||||||
redirect_uri: z.string(),
|
|
||||||
code: z.string(),
|
|
||||||
state: z.string().optional()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export const zPostAuthRefreshData = z.object({
|
|
||||||
body: zServiceAuthRefreshData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful rotation
|
|
||||||
*/
|
|
||||||
export const zPostAuthRefreshResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceAuthTokenResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostAuthTokenData = z.object({
|
|
||||||
body: zServiceAuthTokenData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful token issuance
|
|
||||||
*/
|
|
||||||
export const zPostAuthTokenResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceAuthTokenResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventAttendanceData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
event_id: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful retrieval
|
|
||||||
*/
|
|
||||||
export const zGetEventAttendanceResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.array(zServiceEventAttendanceListResponse).optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventCheckinData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
event_id: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successfully generated code
|
|
||||||
*/
|
|
||||||
export const zGetEventCheckinResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceEventCheckinResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventCheckinQueryData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
event_id: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current attendance status
|
|
||||||
*/
|
|
||||||
export const zGetEventCheckinQueryResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceEventCheckinQueryResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostEventCheckinSubmitData = z.object({
|
|
||||||
body: zServiceEventCheckinSubmitData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attendance marked successfully
|
|
||||||
*/
|
|
||||||
export const zPostEventCheckinSubmitResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.record(z.unknown()).optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventInfoData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
event_id: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful retrieval
|
|
||||||
*/
|
|
||||||
export const zGetEventInfoResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zDataEventIndexDoc.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostEventJoinData = z.object({
|
|
||||||
body: zServiceEventEventJoinData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successfully joined the event
|
|
||||||
*/
|
|
||||||
export const zPostEventJoinResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceEventEventJoinResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventJoinedData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
limit: z.number().int().optional(),
|
|
||||||
offset: z.number().int().optional()
|
|
||||||
}).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful retrieval of joined events
|
|
||||||
*/
|
|
||||||
export const zGetEventJoinedResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.array(zDataEventIndexDoc).optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetEventListData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
limit: z.number().int().optional(),
|
|
||||||
offset: z.number().int().optional()
|
|
||||||
}).optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful paginated list retrieval
|
|
||||||
*/
|
|
||||||
export const zGetEventListResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.array(zDataEventIndexDoc).optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostKycQueryData = z.object({
|
|
||||||
body: zServiceKycKycQueryData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query processed (success/pending/failed)
|
|
||||||
*/
|
|
||||||
export const zPostKycQueryResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceKycKycQueryResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPostKycSessionData = z.object({
|
|
||||||
body: zServiceKycKycSessionData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session created successfully
|
|
||||||
*/
|
|
||||||
export const zPostKycSessionResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceKycKycSessionResponse.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetUserInfoData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful profile retrieval
|
|
||||||
*/
|
|
||||||
export const zGetUserInfoResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceUserUserInfoData.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetUserInfoByUserIdData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.object({
|
|
||||||
user_id: z.string()
|
|
||||||
}),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful profile retrieval
|
|
||||||
*/
|
|
||||||
export const zGetUserInfoByUserIdResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: zServiceUserUserInfoData.optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zGetUserListData = z.object({
|
|
||||||
body: z.never().optional(),
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.object({
|
|
||||||
limit: z.string().optional(),
|
|
||||||
offset: z.string()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful paginated list retrieval
|
|
||||||
*/
|
|
||||||
export const zGetUserListResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.array(zDataUserIndexDoc).optional()
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const zPatchUserUpdateData = z.object({
|
|
||||||
body: zServiceUserUserInfoData,
|
|
||||||
path: z.never().optional(),
|
|
||||||
query: z.never().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Successful profile update
|
|
||||||
*/
|
|
||||||
export const zPatchUserUpdateResponse = zUtilsRespStatus.and(z.object({
|
|
||||||
data: z.record(z.unknown()).optional()
|
|
||||||
}));
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { useCheckinSubmit } from '@/hooks/data/useCheckinSubmit';
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Calendar } from 'lucide-react';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Badge } from '../ui/badge';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function EventCardSkeleton() {
|
|
||||||
return (
|
|
||||||
<Card className="relative mx-auto w-full max-w-sm pt-0">
|
|
||||||
<div className="absolute inset-0 z-30 aspect-video bg-black/10" />
|
|
||||||
<Skeleton
|
|
||||||
className="relative z-20 aspect-video w-full object-cover rounded-t-xl"
|
|
||||||
/>
|
|
||||||
<CardHeader>
|
|
||||||
<CardAction>
|
|
||||||
<Badge variant="secondary" className="bg-accent animate-pulse text-accent select-none">Official</Badge>
|
|
||||||
</CardAction>
|
|
||||||
<CardTitle>
|
|
||||||
<Skeleton className="h-4 max-w-48" />
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="flex flex-row items-center text-xs">
|
|
||||||
<Calendar className="size-4 mr-2" />
|
|
||||||
<Skeleton className="h-4 w-24" />
|
|
||||||
</CardDescription>
|
|
||||||
<CardDescription className="mt-1">
|
|
||||||
<Skeleton className="h-5 max-w-64" />
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
<Skeleton className="h-9 px-4 py-2 w-full"></Skeleton>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import type { EventInfo } from './types';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { Calendar } from 'lucide-react';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function EventCardView({ eventInfo, actionFooter }: { eventInfo: EventInfo; actionFooter: React.ReactNode }) {
|
|
||||||
const { type, coverImage, eventName, description, startTime, endTime } = eventInfo;
|
|
||||||
const startDayJs = dayjs(startTime);
|
|
||||||
const endDayJs = dayjs(endTime);
|
|
||||||
return (
|
|
||||||
<Card className="relative mx-auto w-full max-w-sm pt-0">
|
|
||||||
<div className="absolute inset-0 z-30 aspect-video bg-black/10" />
|
|
||||||
<img
|
|
||||||
src={coverImage}
|
|
||||||
alt="Event cover"
|
|
||||||
className="relative z-20 aspect-video w-full object-cover rounded-t-xl"
|
|
||||||
/>
|
|
||||||
<Skeleton
|
|
||||||
className="absolute z-15 aspect-video w-full object-cover rounded-t-xl"
|
|
||||||
/>
|
|
||||||
<CardHeader>
|
|
||||||
<CardAction>
|
|
||||||
{type === 'official' ? <Badge variant="secondary">Official</Badge> : <Badge variant="destructive">Party</Badge>}
|
|
||||||
</CardAction>
|
|
||||||
<CardTitle>{eventName}</CardTitle>
|
|
||||||
<CardDescription className="flex flex-row items-center text-xs">
|
|
||||||
<Calendar className="size-4 mr-2" />
|
|
||||||
{`${startDayJs.format('YYYY/MM/DD')} - ${endDayJs.format('YYYY/MM/DD')}`}
|
|
||||||
</CardDescription>
|
|
||||||
<CardDescription className="mt-1">
|
|
||||||
{description}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardFooter>
|
|
||||||
{actionFooter}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useEvents } from '@/hooks/data/useEvents';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { DialogTrigger } from '../../ui/dialog';
|
|
||||||
import { EventJoinDialogContainer } from '../event-join.dialog.container';
|
|
||||||
import { KycDialogContainer } from '../kyc/kyc.dialog.container';
|
|
||||||
import { toEventInfo } from '../types';
|
|
||||||
import { EventGridSkeleton } from './event-grid.skeleton';
|
|
||||||
import { EventGridView } from './event-grid.view';
|
|
||||||
|
|
||||||
export function EventGridContainer() {
|
|
||||||
const { data, isLoading } = useEvents();
|
|
||||||
|
|
||||||
const events = useMemo(() => {
|
|
||||||
return data?.pages.flatMap(page => page.data ?? []).map(toEventInfo) ?? [];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
isLoading
|
|
||||||
? <EventGridSkeleton />
|
|
||||||
: (
|
|
||||||
<EventGridView
|
|
||||||
events={events}
|
|
||||||
footer={(eventInfo) => {
|
|
||||||
const Container = eventInfo.requireKyc ? KycDialogContainer : EventJoinDialogContainer;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container event={eventInfo}>
|
|
||||||
{eventInfo.isJoined
|
|
||||||
? (
|
|
||||||
<Button className="w-full" disabled>
|
|
||||||
已加入
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="w-full">加入活动</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { FileQuestionMark } from 'lucide-react';
|
|
||||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../../ui/empty';
|
|
||||||
|
|
||||||
export function EventGridEmpty() {
|
|
||||||
return (
|
|
||||||
<Empty className="h-full">
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia variant="icon">
|
|
||||||
<FileQuestionMark />
|
|
||||||
</EmptyMedia>
|
|
||||||
<EmptyTitle>暂无活动</EmptyTitle>
|
|
||||||
<EmptyDescription>前面的区域 以后再来探索吧</EmptyDescription>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { FileExclamationPoint } from 'lucide-react';
|
|
||||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../../ui/empty';
|
|
||||||
|
|
||||||
export function EventGridError() {
|
|
||||||
return (
|
|
||||||
<Empty className="h-full">
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia variant="icon">
|
|
||||||
<FileExclamationPoint />
|
|
||||||
</EmptyMedia>
|
|
||||||
<EmptyTitle>活动列表加载失败</EmptyTitle>
|
|
||||||
<EmptyDescription>前面的区域 以后再来探索吧</EmptyDescription>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { EventCardSkeleton } from '../event-card.skeleton';
|
|
||||||
|
|
||||||
export function EventGridSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
|
||||||
{Array.from({ length: 4 }).map((_, i) => (
|
|
||||||
// eslint-disable-next-line react/no-array-index-key
|
|
||||||
<EventCardSkeleton key={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { EventInfo } from '../types';
|
|
||||||
import { EventCardView } from '../event-card.view';
|
|
||||||
import { EventGridEmpty } from './event-grid.empty';
|
|
||||||
|
|
||||||
export function EventGridView({ events, footer }: { events: EventInfo[]; footer: (event: EventInfo) => React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{events.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
|
||||||
{events.map(event => (
|
|
||||||
<EventCardView key={event.eventId} eventInfo={event} actionFooter={footer(event)} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{events.length === 0 && <EventGridEmpty />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { EventInfo } from './types';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useJoinEvent } from '@/hooks/data/useJoinEvent';
|
|
||||||
import { Dialog } from '../ui/dialog';
|
|
||||||
import { EventJoinDialogView } from './event-join.dialog.view';
|
|
||||||
|
|
||||||
export function EventJoinDialogContainer({ event, children }: { event: EventInfo; children: React.ReactNode }) {
|
|
||||||
const { mutateAsync, isPending } = useJoinEvent();
|
|
||||||
const join = useCallback(() => {
|
|
||||||
mutateAsync({ body: { event_id: event.eventId } }).then(() => {
|
|
||||||
toast('加入活动成功');
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error('加入活动失败');
|
|
||||||
});
|
|
||||||
}, [event.eventId, mutateAsync]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
{children}
|
|
||||||
<EventJoinDialogView event={event} onJoinEvent={join} isPending={isPending} />
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { EventInfo } from './types';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
|
|
||||||
import { Spinner } from '../ui/spinner';
|
|
||||||
|
|
||||||
export function EventJoinDialogView({ event, onJoinEvent, isPending }: { event: EventInfo; onJoinEvent: () => void; isPending: boolean }) {
|
|
||||||
return (
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>加入活动</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
是否确认要加入活动
|
|
||||||
{' '}
|
|
||||||
{event.eventName}
|
|
||||||
?
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button onClick={onJoinEvent} disabled={isPending}>{isPending ? <Spinner /> : '加入'}</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import type { EventInfo } from './types';
|
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useJoinedEvents } from '@/hooks/data/useJoinedEvents';
|
|
||||||
import { isInDateRange } from '@/lib/utils';
|
|
||||||
import { CheckinQrDialogContainer } from '../checkin/checkin-qr.dialog.container';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { DialogTrigger } from '../ui/dialog';
|
|
||||||
import { EventGridSkeleton } from './event-grid/event-grid.skeleton';
|
|
||||||
import { EventGridView } from './event-grid/event-grid.view';
|
|
||||||
import { toEventInfo } from './types';
|
|
||||||
|
|
||||||
export function JoinedEventGridFooter({ event }: { event: EventInfo }) {
|
|
||||||
const isOutOfDateRange = !isInDateRange(event.startTime, event.endTime);
|
|
||||||
const isCheckedIn = event.isCheckedIn;
|
|
||||||
const canCheckIn = !isOutOfDateRange && !isCheckedIn;
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row justify-between w-full gap-4">
|
|
||||||
<CheckinQrDialogContainer event={event}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="flex-1" disabled={!canCheckIn}>
|
|
||||||
{isOutOfDateRange && '未到签到时间'}
|
|
||||||
{isCheckedIn && '已签到'}
|
|
||||||
{canCheckIn && '签到'}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
</CheckinQrDialogContainer>
|
|
||||||
<Button className="flex-1" onClick={() => void navigate({ to: `/events/$eventId`, params: { eventId: event.eventId } })}>查看详情</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function JoinedEventsContainer() {
|
|
||||||
const { data, isLoading } = useJoinedEvents();
|
|
||||||
|
|
||||||
return (
|
|
||||||
isLoading
|
|
||||||
? <EventGridSkeleton />
|
|
||||||
: (
|
|
||||||
<EventGridView
|
|
||||||
events={data.pages.flatMap(page => page.data ?? []).map(toEventInfo)}
|
|
||||||
footer={event => (
|
|
||||||
<JoinedEventGridFooter event={event} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { X } from 'lucide-react';
|
|
||||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
|
||||||
|
|
||||||
export function KycFailedDialogView() {
|
|
||||||
return (
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>失败</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<p>提交身份认证失败,请重试。</p>
|
|
||||||
<div className="flex justify-center my-12">
|
|
||||||
<X size={100} />
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import type { KycSubmission } from './kyc.types';
|
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import z from 'zod';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
|
||||||
import { Button } from '../../ui/button';
|
|
||||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
|
||||||
|
|
||||||
const CnridSchema = z.object({
|
|
||||||
cnrid: z.string().min(18, '身份证号应为18位').max(18, '身份证号应为18位'),
|
|
||||||
name: z.string().min(2, '姓名应至少2个字符').max(10, '姓名应不超过10个字符'),
|
|
||||||
});
|
|
||||||
|
|
||||||
function CnridForm({ onSubmit }: { onSubmit: OnSubmit }) {
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
cnrid: '',
|
|
||||||
name: '',
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: CnridSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async (values) => {
|
|
||||||
await onSubmit({
|
|
||||||
method: 'cnrid',
|
|
||||||
...values.value,
|
|
||||||
}).catch(() => {
|
|
||||||
toast('认证失败,请稍后再试');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit();
|
|
||||||
}}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
<form.Field name="name">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="name">姓名</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<form.Field name="cnrid">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="cnrid">身份证号</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="cnrid"
|
|
||||||
name="cnrid"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<DialogFooter>
|
|
||||||
<form.Subscribe
|
|
||||||
selector={state => [state.canSubmit, state.isPristine, state.isSubmitting]}
|
|
||||||
children={([canSubmit, isPristine, isSubmitting]) => (
|
|
||||||
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? <Spinner /> : '开始认证'}</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PassportSchema = z.object({
|
|
||||||
passportId: z.string().min(9, '护照号应为9个字符').max(9, '护照号应为9个字符'),
|
|
||||||
});
|
|
||||||
|
|
||||||
function PassportForm({ onSubmit }: { onSubmit: OnSubmit }) {
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
passportId: '',
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: PassportSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async (values) => {
|
|
||||||
await onSubmit({
|
|
||||||
method: 'passport',
|
|
||||||
...values.value,
|
|
||||||
}).catch(() => {
|
|
||||||
toast('认证失败,请稍后再试');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit();
|
|
||||||
}}
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
>
|
|
||||||
<form.Field name="passportId">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="passportId">护照号</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="passportId"
|
|
||||||
name="passportId"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<DialogFooter>
|
|
||||||
<form.Subscribe
|
|
||||||
selector={state => [state.canSubmit, state.isPristine, state.isSubmitting]}
|
|
||||||
children={([canSubmit, isPristine, isSubmitting]) => (
|
|
||||||
<Button type="submit" disabled={!canSubmit || isPristine || isSubmitting}>{isSubmitting ? '...' : '开始认证'}</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type OnSubmit = (submission: KycSubmission) => Promise<void>;
|
|
||||||
|
|
||||||
export function KycMethodSelectionDialogView({ onSubmit }: { onSubmit: OnSubmit }) {
|
|
||||||
const [kycMethod, setKycMethod] = useState<string | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>选择身份认证模式</DialogTitle>
|
|
||||||
<DialogDescription className="prose">
|
|
||||||
<p>我们支持身份证和护照认证。</p>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<Label htmlFor="selection">身份认证模式</Label>
|
|
||||||
<Select onValueChange={setKycMethod}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="请选择..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectGroup id="selection">
|
|
||||||
<SelectItem value="cnrid">身份证</SelectItem>
|
|
||||||
<SelectItem value="passport">护照</SelectItem>
|
|
||||||
</SelectGroup>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{kycMethod === 'cnrid' && <CnridForm onSubmit={onSubmit} />}
|
|
||||||
{kycMethod === 'passport' && <PassportForm onSubmit={onSubmit} />}
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { HashLoader } from 'react-spinners/esm';
|
|
||||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
|
||||||
|
|
||||||
export function KycPendingDialogView() {
|
|
||||||
return (
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>等待身份认证结果</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<p>认证页面已打开。正在等待认证服务器回传数据...</p>
|
|
||||||
<div className="flex justify-center my-12">
|
|
||||||
<HashLoader color="#e0e0e0" size={100} />
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { Button } from '../../ui/button';
|
|
||||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../../ui/dialog';
|
|
||||||
|
|
||||||
export function KycPromptDialogView({ next }: { next: () => void }) {
|
|
||||||
return (
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>需要身份认证</DialogTitle>
|
|
||||||
<DialogDescription className="prose">
|
|
||||||
<p>为了确保会议的安全性及合规性,我们需要对参会者进行实名认证。</p>
|
|
||||||
<p>您的个人隐私对我们至关重要:</p>
|
|
||||||
<ul>
|
|
||||||
<li>数据加密:您的所有个人敏感信息均会通过 AES-256 标准进行强加密存储。</li>
|
|
||||||
<li>用途限制:收集的信息仅用于本次活动的身份核实,不会用于任何商业用途。</li>
|
|
||||||
<li>按时销毁:所有身份证明文件及关联的敏感原始数据将在活动完成后最多 30 天内从服务器中彻底删除。</li>
|
|
||||||
</ul>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={next}>下一步</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Check } from 'lucide-react';
|
|
||||||
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
|
||||||
|
|
||||||
export function KycSuccessDialogView() {
|
|
||||||
return (
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>成功</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<p>已完成身份认证。</p>
|
|
||||||
<div className="flex justify-center my-12">
|
|
||||||
<Check size={100} />
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import type { EventInfo } from '../types';
|
|
||||||
import type { KycSubmission } from './kyc.types';
|
|
||||||
import { Dialog } from '@radix-ui/react-dialog';
|
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { postKycQuery } from '@/client';
|
|
||||||
import { useCreateKycSession } from '@/hooks/data/useCreateKycSession';
|
|
||||||
import { useJoinEvent } from '@/hooks/data/useJoinEvent';
|
|
||||||
import { KycFailedDialogView } from './kyc-failed.dialog.view';
|
|
||||||
import { KycMethodSelectionDialogView } from './kyc-method-selection.dialog.view';
|
|
||||||
import { KycPendingDialogView } from './kyc-pending.dialog.view';
|
|
||||||
import { KycPromptDialogView } from './kyc-prompt.dialog.view';
|
|
||||||
import { KycSuccessDialogView } from './kyc-success.dialog.view';
|
|
||||||
import { createKycStore } from './kyc.state';
|
|
||||||
|
|
||||||
export function KycDialogContainer({ event, children }: { event: EventInfo; children: React.ReactNode }) {
|
|
||||||
const [store] = useState(() => createKycStore(event.eventId));
|
|
||||||
const isDialogOpen = useStore(store, s => s.isDialogOpen);
|
|
||||||
const setIsDialogOpen = useStore(store, s => s.setIsDialogOpen);
|
|
||||||
const stage = useStore(store, s => s.stage);
|
|
||||||
const setStage = useStore(store, s => s.setStage);
|
|
||||||
const setKycId = useStore(store, s => s.setKycId);
|
|
||||||
|
|
||||||
const { mutateAsync: createKycSessionAsync } = useCreateKycSession();
|
|
||||||
const { mutateAsync: joinEventAsync } = useJoinEvent();
|
|
||||||
|
|
||||||
const joinEvent = useCallback(async (eventId: string, kycId: string, abortSignal?: AbortSignal) => {
|
|
||||||
try {
|
|
||||||
await joinEventAsync({
|
|
||||||
signal: abortSignal,
|
|
||||||
body: { event_id: eventId, kyc_id: kycId },
|
|
||||||
});
|
|
||||||
setStage('success');
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error('Error joining event:', e);
|
|
||||||
setStage('failed');
|
|
||||||
}
|
|
||||||
}, [joinEventAsync, setStage]);
|
|
||||||
|
|
||||||
const onKycSessionCreate = useCallback(async (submission: KycSubmission) => {
|
|
||||||
try {
|
|
||||||
const { data } = await createKycSessionAsync(submission);
|
|
||||||
setKycId(data!.kyc_id!);
|
|
||||||
if (data!.status === 'success') {
|
|
||||||
await joinEvent(event.eventId, data!.kyc_id!, undefined);
|
|
||||||
}
|
|
||||||
else if (data!.status === 'processing') {
|
|
||||||
window.open(data!.redirect_uri, '_blank');
|
|
||||||
setStage('pending');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setStage('failed');
|
|
||||||
}
|
|
||||||
}, [event.eventId, joinEvent, createKycSessionAsync, setKycId, setStage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (stage !== 'pending' || !isDialogOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
let timer: NodeJS.Timeout;
|
|
||||||
|
|
||||||
const poll = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await postKycQuery({
|
|
||||||
signal: controller.signal,
|
|
||||||
body: { kyc_id: store.getState().kycId! },
|
|
||||||
});
|
|
||||||
|
|
||||||
const status = data?.data?.status;
|
|
||||||
|
|
||||||
if (status === 'success') {
|
|
||||||
void joinEvent(event.eventId, store.getState().kycId!, controller.signal);
|
|
||||||
}
|
|
||||||
else if (status === 'failed') {
|
|
||||||
setStage('failed');
|
|
||||||
}
|
|
||||||
else if (status === 'pending') {
|
|
||||||
timer = setTimeout(() => void poll(), 1000);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// What the fuck?
|
|
||||||
setStage('failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e) {
|
|
||||||
if ((e as Error).name === 'AbortError')
|
|
||||||
return;
|
|
||||||
console.error('Error fetching KYC status:', e);
|
|
||||||
setStage('failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void poll();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
clearTimeout(timer);
|
|
||||||
};
|
|
||||||
}, [stage, store, setStage, isDialogOpen, joinEvent, event.eventId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={isDialogOpen}
|
|
||||||
onOpenChange={setIsDialogOpen}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{stage === 'prompt' && <KycPromptDialogView next={() => setStage('methodSelection')} />}
|
|
||||||
{stage === 'methodSelection' && <KycMethodSelectionDialogView onSubmit={onKycSessionCreate} />}
|
|
||||||
{stage === 'pending' && <KycPendingDialogView />}
|
|
||||||
{stage === 'success' && <KycSuccessDialogView />}
|
|
||||||
{stage === 'failed' && <KycFailedDialogView />}
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { createStore } from 'zustand';
|
|
||||||
import { devtools } from 'zustand/middleware';
|
|
||||||
|
|
||||||
interface KycState {
|
|
||||||
isDialogOpen: boolean;
|
|
||||||
eventIdToJoin: string;
|
|
||||||
kycId: string | null;
|
|
||||||
stage: 'prompt' | 'methodSelection' | 'pending' | 'success' | 'failed';
|
|
||||||
|
|
||||||
setIsDialogOpen: (open: boolean) => void;
|
|
||||||
setStage: (stage: KycState['stage']) => void;
|
|
||||||
setKycId: (kycId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createKycStore(eventIdToJoin: string) {
|
|
||||||
const initialState = {
|
|
||||||
isDialogOpen: false,
|
|
||||||
eventIdToJoin,
|
|
||||||
kycId: null,
|
|
||||||
stage: 'prompt' as const,
|
|
||||||
};
|
|
||||||
return createStore<KycState>()(devtools(set => ({
|
|
||||||
...initialState,
|
|
||||||
setIsDialogOpen: (open: boolean) => set(() =>
|
|
||||||
open
|
|
||||||
? { ...initialState, isDialogOpen: true }
|
|
||||||
: { ...initialState, isDialogOpen: false },
|
|
||||||
),
|
|
||||||
setStage: (stage: KycState['stage']) => set(() => ({ stage })),
|
|
||||||
setKycId: (kycId: string) => set(() => ({ kycId })),
|
|
||||||
})));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KycStore = ReturnType<typeof createKycStore>;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
export type KycSubmission = {
|
|
||||||
method: 'cnrid';
|
|
||||||
cnrid: string;
|
|
||||||
name: string;
|
|
||||||
} | {
|
|
||||||
method: 'passport';
|
|
||||||
passportId: string;
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { isEmpty } from 'lodash-es';
|
|
||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { Dialog } from '../ui/dialog';
|
|
||||||
import { NicknameNeededDialogView } from './nickname-needed.dialog.view';
|
|
||||||
|
|
||||||
export function NicknameNeededDialogContainer() {
|
|
||||||
const { data } = useUserInfo();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
return (
|
|
||||||
<Dialog open={isEmpty(data?.data?.nickname)}>
|
|
||||||
<NicknameNeededDialogView onAction={() => void navigate({ to: '/profile' })} />
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { HatGlasses } from 'lucide-react';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
|
|
||||||
|
|
||||||
export function NicknameNeededDialogView({ onAction }: { onAction: () => void }) {
|
|
||||||
return (
|
|
||||||
<DialogContent
|
|
||||||
showCloseButton={false}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>请先填写昵称</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
如要加入活动,请先于「个人资料」页面填写昵称。
|
|
||||||
<div className="flex justify-center my-12">
|
|
||||||
<HatGlasses size={100} className="stroke-[1.5]" />
|
|
||||||
</div>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={onAction}>去填写</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import type { DataEventIndexDoc } from '@/client';
|
|
||||||
import PlaceholderImage from '@/assets/event-placeholder.png';
|
|
||||||
|
|
||||||
export interface EventInfo {
|
|
||||||
type: 'official' | 'party';
|
|
||||||
eventId: string;
|
|
||||||
isJoined: boolean;
|
|
||||||
isCheckedIn: boolean;
|
|
||||||
requireKyc: boolean;
|
|
||||||
coverImage: string;
|
|
||||||
eventName: string;
|
|
||||||
description: string;
|
|
||||||
startTime: Date;
|
|
||||||
endTime: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toEventInfo(raw: DataEventIndexDoc): EventInfo {
|
|
||||||
return {
|
|
||||||
type: raw.type! as EventInfo['type'],
|
|
||||||
eventId: raw.event_id!,
|
|
||||||
isJoined: raw.is_joined!,
|
|
||||||
requireKyc: raw.enable_kyc!,
|
|
||||||
isCheckedIn: raw.is_checked_in ?? false,
|
|
||||||
coverImage: raw.thumbnail! || PlaceholderImage,
|
|
||||||
eventName: raw.name!,
|
|
||||||
description: raw.description!,
|
|
||||||
startTime: new Date(raw.start_time!),
|
|
||||||
endTime: new Date(raw.end_time!),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { RawError } from '@/lib/types';
|
|
||||||
import { TriangleAlert } from 'lucide-react';
|
|
||||||
import { isRawError } from '@/lib/types';
|
|
||||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './ui/empty';
|
|
||||||
|
|
||||||
export function GlobalError({ error }: { error: Error | RawError }) {
|
|
||||||
return (
|
|
||||||
<Empty className="h-screen w-full">
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia variant="icon">
|
|
||||||
<TriangleAlert />
|
|
||||||
</EmptyMedia>
|
|
||||||
<EmptyTitle>出错了</EmptyTitle>
|
|
||||||
<EmptyDescription>{isRawError(error) ? error.error_id : error.message}</EmptyDescription>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import type { TurnstileInstance } from '@marsidev/react-turnstile';
|
|
||||||
import type { AuthorizeSearchParams } from '@/routes/authorize';
|
|
||||||
import { Turnstile } from '@marsidev/react-turnstile';
|
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import z from 'zod';
|
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { useGetMagicLink } from '@/hooks/data/useGetMagicLink';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
import { Spinner } from './ui/spinner';
|
|
||||||
|
|
||||||
export function LoginForm({
|
|
||||||
oauthParams,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
oauthParams: AuthorizeSearchParams;
|
|
||||||
}) {
|
|
||||||
const turnstileRef = useRef<TurnstileInstance>(null);
|
|
||||||
const [token, setToken] = useState<string | null>(import.meta.env.DEV ? 'turnstile_token' : null);
|
|
||||||
const { mutateAsync, isPending } = useGetMagicLink();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: '',
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: z.object({
|
|
||||||
email: z.string().email('请输入有效的邮箱地址'),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
try {
|
|
||||||
await mutateAsync({ body: { email: value.email, turnstile_token: token!, ...oauthParams } });
|
|
||||||
await navigate({ to: '/magicLinkSent', search: { email: value.email } });
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error('请求登录链接失败');
|
|
||||||
turnstileRef.current?.reset();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isLoading = isPending || token === null;
|
|
||||||
return (
|
|
||||||
<div className={cn('flex flex-col gap-6', className)} {...props}>
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FieldGroup>
|
|
||||||
<div className="flex flex-col items-center gap-2 text-center">
|
|
||||||
<div className="flex size-8 items-center justify-center rounded-md">
|
|
||||||
<NixOSLogo className="size-6" />
|
|
||||||
</div>
|
|
||||||
<span className="sr-only">Nix CN CMS</span>
|
|
||||||
<h1 className="text-xl font-bold">欢迎来到 Nix CN CMS</h1>
|
|
||||||
</div>
|
|
||||||
<form.Field name="email">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="email">参会登记Email</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="edolstra@gmail.com"
|
|
||||||
required
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<Field>
|
|
||||||
<Button type="submit" disabled={isLoading}>
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
{token === null ? '等待 Turnstile' : isPending ? '发送中...' : '发送登录链接'}
|
|
||||||
</Button>
|
|
||||||
</Field>
|
|
||||||
</FieldGroup>
|
|
||||||
</form>
|
|
||||||
<Turnstile
|
|
||||||
ref={turnstileRef}
|
|
||||||
siteKey="0x4AAAAAACI5pu-lNWFc6Wu1"
|
|
||||||
options={{
|
|
||||||
refreshExpired: 'auto',
|
|
||||||
}}
|
|
||||||
onSuccess={(token) => {
|
|
||||||
setToken(token);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { ServiceUserUserInfoData } from '@/client';
|
|
||||||
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
|
||||||
import { EditProfileDialogView } from './edit-profile.dialog.view';
|
|
||||||
|
|
||||||
export function EditProfileDialogContainer({ data }: { data: ServiceUserUserInfoData }) {
|
|
||||||
const { mutateAsync } = useUpdateUser();
|
|
||||||
return (
|
|
||||||
<EditProfileDialogView
|
|
||||||
user={data}
|
|
||||||
updateProfile={async (data) => {
|
|
||||||
await mutateAsync({ body: data });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import type { ServiceUserUserInfoData } from '@/client';
|
|
||||||
import { useForm } from '@tanstack/react-form';
|
|
||||||
import {
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import z from 'zod';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldError,
|
|
||||||
FieldLabel,
|
|
||||||
} from '@/components/ui/field';
|
|
||||||
import {
|
|
||||||
Input,
|
|
||||||
} from '@/components/ui/input';
|
|
||||||
import { Spinner } from '../ui/spinner';
|
|
||||||
import { Switch } from '../ui/switch';
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
|
||||||
username: z.string().min(5, '用户名长度至少为5个字符'),
|
|
||||||
nickname: z.string().nonempty('昵称不能为空'),
|
|
||||||
subtitle: z.string(),
|
|
||||||
avatar: z.string().url().or(z.literal('')),
|
|
||||||
allow_public: z.boolean(),
|
|
||||||
});
|
|
||||||
export function EditProfileDialogView({ user, updateProfile }: { user: ServiceUserUserInfoData; updateProfile: (data: ServiceUserUserInfoData) => Promise<void> }) {
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
avatar: user.avatar,
|
|
||||||
username: user.username,
|
|
||||||
nickname: user.nickname,
|
|
||||||
subtitle: user.subtitle,
|
|
||||||
allow_public: user.allow_public,
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onBlur: formSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async ({
|
|
||||||
value,
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
await updateProfile(value);
|
|
||||||
toast.success('个人资料更新成功');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Form submission error', error);
|
|
||||||
toast.error('更新个人资料失败,请重试');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
const id = setTimeout(() => {
|
|
||||||
form.reset();
|
|
||||||
}, 200);
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
}
|
|
||||||
}, [open, form]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline" className="w-full" size="lg">编辑个人资料</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
void form.handleSubmit().then(() => setOpen(false));
|
|
||||||
}}
|
|
||||||
className="grid gap-4"
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>编辑个人资料</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<form.Field name="username">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="username">用户名</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
placeholder={user.username}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<form.Field name="nickname">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="nickname">昵称</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="nickname"
|
|
||||||
name="nickname"
|
|
||||||
placeholder={user.nickname}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<form.Field name="subtitle">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="subtitle">副标题</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="subtitle"
|
|
||||||
name="subtitle"
|
|
||||||
placeholder={user.subtitle}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<form.Field name="avatar">
|
|
||||||
{field => (
|
|
||||||
<Field>
|
|
||||||
<FieldLabel htmlFor="avatar">头像链接</FieldLabel>
|
|
||||||
<Input
|
|
||||||
id="avatar"
|
|
||||||
name="avatar"
|
|
||||||
placeholder={user.avatar}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={e => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
<FieldError errors={field.state.meta.errors} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<form.Field name="allow_public">
|
|
||||||
{field => (
|
|
||||||
<Field orientation="horizontal" className="my-2">
|
|
||||||
<FieldLabel htmlFor="allow_public">公开个人资料</FieldLabel>
|
|
||||||
<Switch id="allow_public" onCheckedChange={e => field.handleChange(e)} defaultChecked={user.allow_public} />
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="outline">取消</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<form.Subscribe
|
|
||||||
selector={state => [state.canSubmit, state.isSubmitting]}
|
|
||||||
children={([canSubmit, isSubmitting]) => (
|
|
||||||
<Button type="submit" disabled={!canSubmit}>
|
|
||||||
{isSubmitting ? <Spinner /> : '保存'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { useUpdateUser } from '@/hooks/data/useUpdateUser';
|
|
||||||
import { useOtherUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { utf8ToBase64 } from '@/lib/utils';
|
|
||||||
import { ProfileView } from './profile.view';
|
|
||||||
|
|
||||||
export function ProfileContainer({ userId }: { userId: string }) {
|
|
||||||
const { data } = useOtherUserInfo(userId);
|
|
||||||
const { mutateAsync } = useUpdateUser();
|
|
||||||
return (
|
|
||||||
<ProfileView
|
|
||||||
user={data.data!}
|
|
||||||
onSaveBio={async (bio) => {
|
|
||||||
await mutateAsync({ body: { bio: utf8ToBase64(bio) } });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { UserLock } from 'lucide-react';
|
|
||||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '../ui/empty';
|
|
||||||
|
|
||||||
export function ProfileError({ reason }: { reason: string }) {
|
|
||||||
return (
|
|
||||||
<Empty className="h-full">
|
|
||||||
<EmptyHeader>
|
|
||||||
<EmptyMedia variant="icon">
|
|
||||||
<UserLock />
|
|
||||||
</EmptyMedia>
|
|
||||||
<EmptyTitle>无法查看此个人资料</EmptyTitle>
|
|
||||||
<EmptyDescription>{reason}</EmptyDescription>
|
|
||||||
</EmptyHeader>
|
|
||||||
</Empty>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Mail } from 'lucide-react';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function ProfileSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
|
|
||||||
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
|
|
||||||
<div className="flex flex-col w-full gap-3">
|
|
||||||
<div className="flex flex-col w-full gap-3">
|
|
||||||
<div className="flex flex-row gap-3 w-full lg:flex-col">
|
|
||||||
<Skeleton className="size-16 rounded-full border-2 border-muted lg:size-64" />
|
|
||||||
<div className="flex flex-1 flex-col justify-center lg:mt-3 gap-2">
|
|
||||||
<Skeleton className="w-32 h-8" />
|
|
||||||
<Skeleton className="w-20 h-6" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
|
|
||||||
<Mail className="h-4 w-4 stroke-muted-foreground" />
|
|
||||||
<Skeleton className="w-32 h-4" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Skeleton className="w-64 h-[40px]" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Skeleton className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import type { ServiceUserUserInfoData } from '@/client';
|
|
||||||
import { identicon } from '@dicebear/collection';
|
|
||||||
import { createAvatar } from '@dicebear/core';
|
|
||||||
import MDEditor from '@uiw/react-md-editor';
|
|
||||||
import {
|
|
||||||
isEmpty,
|
|
||||||
isNil,
|
|
||||||
} from 'lodash-es';
|
|
||||||
import { Mail, Pencil } from 'lucide-react';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import Markdown from 'react-markdown';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { Avatar, AvatarImage } from '@/components/ui/avatar';
|
|
||||||
import { base64ToUtf8 } from '@/lib/utils';
|
|
||||||
import { Button } from '../ui/button';
|
|
||||||
import { Spinner } from '../ui/spinner';
|
|
||||||
import { EditProfileDialogContainer } from './edit-profile.dialog.container';
|
|
||||||
|
|
||||||
export function ProfileView({ user, onSaveBio }: { user: ServiceUserUserInfoData; onSaveBio: (bio: string) => Promise<void> }) {
|
|
||||||
const [bio, setBio] = useState<string | undefined>(() => base64ToUtf8(user.bio ?? ''));
|
|
||||||
const [enableBioEdit, setEnableBioEdit] = useState(false);
|
|
||||||
const [isSubmittingBio, setIsSubmittingBio] = useState(false);
|
|
||||||
|
|
||||||
const IdentIcon = useMemo(() => {
|
|
||||||
const avatar = createAvatar(identicon, {
|
|
||||||
size: 128,
|
|
||||||
seed: user.user_id,
|
|
||||||
}).toDataUri();
|
|
||||||
return <img src={avatar} alt="Avatar" />;
|
|
||||||
}, [user.user_id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col justify-center w-full lg:w-auto h-full lg:h-auto lg:flex-row lg:gap-8">
|
|
||||||
<div className="flex w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
|
|
||||||
<div className="flex flex-col w-full gap-3">
|
|
||||||
<div className="flex flex-col w-full gap-3">
|
|
||||||
<div className="flex flex-row gap-3 w-full lg:flex-col">
|
|
||||||
<Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
|
|
||||||
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-1 flex-col justify-center lg:mt-3">
|
|
||||||
<span className="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
|
|
||||||
<span className="text-[20px] text-muted-foreground" aria-hidden="true">{user.subtitle}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row gap-2 items-center text-sm px-1 lg:px-0">
|
|
||||||
<Mail className="h-4 w-4 stroke-muted-foreground" />
|
|
||||||
{user.email}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<EditProfileDialogContainer data={user} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<section className="relative rounded-md border border-muted w-full flex-1 lg:flex-auto min-h-72 lg:h-full mt-4 lg:mt-0 prose dark:prose-invert max-w-[1012px] self-center">
|
|
||||||
{/* Bio */}
|
|
||||||
{enableBioEdit
|
|
||||||
? (
|
|
||||||
<MDEditor
|
|
||||||
value={bio}
|
|
||||||
onChange={setBio}
|
|
||||||
height="100%"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
|
|
||||||
<Button
|
|
||||||
className="absolute bottom-4 right-4"
|
|
||||||
// eslint-disable-next-line ts/no-misused-promises
|
|
||||||
onClick={async () => {
|
|
||||||
if (!enableBioEdit) {
|
|
||||||
setEnableBioEdit(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!isNil(bio)) {
|
|
||||||
try {
|
|
||||||
setIsSubmittingBio(true);
|
|
||||||
await onSaveBio(bio);
|
|
||||||
setEnableBioEdit(false);
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error('个人简介更新失败');
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
setIsSubmittingBio(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="icon-sm"
|
|
||||||
variant={enableBioEdit ? 'default' : 'outline'}
|
|
||||||
disabled={isSubmittingBio}
|
|
||||||
>
|
|
||||||
{isSubmittingBio ? <Spinner /> : <Pencil />}
|
|
||||||
</Button>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { NavData } from '@/lib/navData';
|
|
||||||
import * as React from 'react';
|
|
||||||
import NixOSLogo from '@/assets/nixos.svg?react';
|
|
||||||
import { NavMain } from '@/components/sidebar/nav-main.view';
|
|
||||||
import { NavSecondary } from '@/components/sidebar/nav-secondary.view';
|
|
||||||
import {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function AppSidebar({ navData, footerWidget, secondaryNavExtra, ...props }: React.ComponentProps<typeof Sidebar> & { navData: NavData; footerWidget: React.ReactNode; secondaryNavExtra?: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<Sidebar collapsible="offcanvas" {...props}>
|
|
||||||
<SidebarHeader>
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<SidebarMenuButton
|
|
||||||
asChild
|
|
||||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
|
||||||
>
|
|
||||||
<a href="#">
|
|
||||||
<NixOSLogo />
|
|
||||||
<span className="text-base font-semibold">Nix CN CMS</span>
|
|
||||||
</a>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarHeader>
|
|
||||||
<SidebarContent>
|
|
||||||
<NavMain items={navData.navMain} />
|
|
||||||
<NavSecondary items={navData.navSecondary} className="mt-auto">
|
|
||||||
{secondaryNavExtra}
|
|
||||||
</NavSecondary>
|
|
||||||
</SidebarContent>
|
|
||||||
<SidebarFooter>
|
|
||||||
{footerWidget}
|
|
||||||
</SidebarFooter>
|
|
||||||
</Sidebar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import type { Icon } from '@tabler/icons-react';
|
|
||||||
|
|
||||||
import { Link } from '@tanstack/react-router';
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function NavMain({
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
icon?: Icon;
|
|
||||||
}[];
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup>
|
|
||||||
<SidebarGroupContent className="flex flex-col gap-2">
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map(item => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<Link
|
|
||||||
to={item.url}
|
|
||||||
>
|
|
||||||
{({ isActive }) => {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton isActive={isActive} tooltip={item.title}>
|
|
||||||
{item.icon && <item.icon />}
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { Icon } from '@tabler/icons-react';
|
|
||||||
import { Link } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
import * as React from 'react';
|
|
||||||
import {
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function NavSecondary({
|
|
||||||
items,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: {
|
|
||||||
items: {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
icon: Icon;
|
|
||||||
}[];
|
|
||||||
children?: React.ReactNode;
|
|
||||||
} & React.ComponentPropsWithoutRef<typeof SidebarGroup>) {
|
|
||||||
return (
|
|
||||||
<SidebarGroup {...props}>
|
|
||||||
<SidebarGroupContent>
|
|
||||||
<SidebarMenu>
|
|
||||||
{items.map(item => (
|
|
||||||
<SidebarMenuItem key={item.title}>
|
|
||||||
<Link to={item.url}>
|
|
||||||
{({ isActive }) => {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton isActive={isActive} tooltip={item.title}>
|
|
||||||
<item.icon />
|
|
||||||
<span>{item.title}</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
{children}
|
|
||||||
</SidebarMenu>
|
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { NavUserView } from './nav-user.view';
|
|
||||||
|
|
||||||
export function NavUserContainer() {
|
|
||||||
const { data } = useUserInfo();
|
|
||||||
return (
|
|
||||||
<NavUserView
|
|
||||||
user={data.data!}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { IconDotsVertical } from '@tabler/icons-react';
|
|
||||||
import { SidebarMenuButton } from '../ui/sidebar';
|
|
||||||
import { Skeleton } from '../ui/skeleton';
|
|
||||||
|
|
||||||
export function NavUserSkeleton() {
|
|
||||||
return (
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
>
|
|
||||||
<Skeleton className="h-8 w-8 rounded-lg" />
|
|
||||||
<div className="flex flex-col flex-1 gap-1">
|
|
||||||
<Skeleton className="h-3 w-16" />
|
|
||||||
<Skeleton className="h-3 w-24" />
|
|
||||||
</div>
|
|
||||||
<IconDotsVertical className="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import type { ServiceUserUserInfoData } from '@/client';
|
|
||||||
|
|
||||||
import { identicon } from '@dicebear/collection';
|
|
||||||
import { createAvatar } from '@dicebear/core';
|
|
||||||
import {
|
|
||||||
IconDotsVertical,
|
|
||||||
IconLogout,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { isEmpty } from 'lodash-es';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarImage,
|
|
||||||
} from '@/components/ui/avatar';
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from '@/components/ui/dropdown-menu';
|
|
||||||
import {
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
useSidebar,
|
|
||||||
} from '@/components/ui/sidebar';
|
|
||||||
import { logout } from '@/lib/token';
|
|
||||||
|
|
||||||
export function NavUserView({ user }: { user: ServiceUserUserInfoData }) {
|
|
||||||
const { isMobile } = useSidebar();
|
|
||||||
|
|
||||||
const IdentIcon = useMemo(() => {
|
|
||||||
const avatar = createAvatar(identicon, {
|
|
||||||
size: 128,
|
|
||||||
seed: user.user_id,
|
|
||||||
}).toDataUri();
|
|
||||||
return <img src={avatar} alt="Avatar" />;
|
|
||||||
}, [user.user_id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarMenu>
|
|
||||||
<SidebarMenuItem>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<SidebarMenuButton
|
|
||||||
size="lg"
|
|
||||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
|
||||||
>
|
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
|
||||||
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.nickname}</span>
|
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<IconDotsVertical className="ml-auto size-4" />
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
|
||||||
side={isMobile ? 'bottom' : 'right'}
|
|
||||||
align="end"
|
|
||||||
sideOffset={4}
|
|
||||||
>
|
|
||||||
<DropdownMenuLabel className="p-0 font-normal">
|
|
||||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
|
||||||
{!isEmpty(user.avatar) ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
|
|
||||||
</Avatar>
|
|
||||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
||||||
<span className="truncate font-medium">{user.nickname}</span>
|
|
||||||
<span className="text-muted-foreground truncate text-xs">
|
|
||||||
{user.email}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem onClick={_e => logout()}>
|
|
||||||
<IconLogout />
|
|
||||||
登出
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
</SidebarMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
|
||||||
|
|
||||||
export function SiteHeader({ title }: { title: string }) {
|
|
||||||
return (
|
|
||||||
<header className="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
|
||||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
|
||||||
<SidebarTrigger className="-ml-1" />
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="mx-2 data-[orientation=vertical]:h-4"
|
|
||||||
/>
|
|
||||||
<h1 className="text-base font-medium">{title}</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { Theme } from '@/hooks/useTheme';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ThemeProviderContext } from '@/hooks/useTheme';
|
|
||||||
|
|
||||||
interface ThemeProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
defaultTheme?: Theme;
|
|
||||||
storageKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ThemeProvider({
|
|
||||||
children,
|
|
||||||
defaultTheme = 'dark',
|
|
||||||
storageKey = 'vite-ui-theme',
|
|
||||||
...props
|
|
||||||
}: ThemeProviderProps) {
|
|
||||||
const [theme, setTheme] = useState<Theme>(
|
|
||||||
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme,
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const root = window.document.documentElement;
|
|
||||||
|
|
||||||
root.classList.remove('light', 'dark');
|
|
||||||
|
|
||||||
if (theme === 'system') {
|
|
||||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
.matches
|
|
||||||
? 'dark'
|
|
||||||
: 'light';
|
|
||||||
|
|
||||||
root.classList.add(systemTheme);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
root.classList.add(theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
const value = {
|
|
||||||
theme,
|
|
||||||
setTheme: (theme: Theme) => {
|
|
||||||
localStorage.setItem(storageKey, theme);
|
|
||||||
setTheme(theme);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProviderContext {...props} value={value}>
|
|
||||||
{children}
|
|
||||||
</ThemeProviderContext>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Avatar({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
data-slot="avatar"
|
|
||||||
className={cn(
|
|
||||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
data-slot="avatar-image"
|
|
||||||
className={cn("aspect-square size-full", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
data-slot="avatar-fallback"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const badgeVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
||||||
secondary:
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
||||||
destructive:
|
|
||||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
||||||
outline:
|
|
||||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function Badge({
|
|
||||||
className,
|
|
||||||
variant,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span"> &
|
|
||||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "span"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="badge"
|
|
||||||
className={cn(badgeVariants({ variant }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Badge, badgeVariants }
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
|
||||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
|
||||||
return (
|
|
||||||
<ol
|
|
||||||
data-slot="breadcrumb-list"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-item"
|
|
||||||
className={cn("inline-flex items-center gap-1.5", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbLink({
|
|
||||||
asChild,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="breadcrumb-link"
|
|
||||||
className={cn("hover:text-foreground transition-colors", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-page"
|
|
||||||
role="link"
|
|
||||||
aria-disabled="true"
|
|
||||||
aria-current="page"
|
|
||||||
className={cn("text-foreground font-normal", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="breadcrumb-separator"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("[&>svg]:size-3.5", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children ?? <ChevronRight />}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function BreadcrumbEllipsis({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="breadcrumb-ellipsis"
|
|
||||||
role="presentation"
|
|
||||||
aria-hidden="true"
|
|
||||||
className={cn("flex size-9 items-center justify-center", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<MoreHorizontal className="size-4" />
|
|
||||||
<span className="sr-only">More</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbEllipsis,
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import type { VariantProps } from 'class-variance-authority';
|
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
|
||||||
import { cva } from 'class-variance-authority';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const buttonVariants = cva(
|
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*=\'size-\'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
||||||
destructive:
|
|
||||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
|
||||||
outline:
|
|
||||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
|
||||||
secondary:
|
|
||||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
||||||
ghost:
|
|
||||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
|
||||||
link: 'text-primary underline-offset-4 hover:underline',
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
'default': 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
||||||
'sm': 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
|
||||||
'lg': 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
|
||||||
'icon': 'size-9',
|
|
||||||
'icon-sm': 'size-8',
|
|
||||||
'icon-lg': 'size-10',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: 'default',
|
|
||||||
size: 'default',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Button({
|
|
||||||
className,
|
|
||||||
variant = 'default',
|
|
||||||
size = 'default',
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'button'>
|
|
||||||
& VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean;
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : 'button';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
data-variant={variant}
|
|
||||||
data-size={size}
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Button, buttonVariants };
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card"
|
|
||||||
className={cn(
|
|
||||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-header"
|
|
||||||
className={cn(
|
|
||||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-title"
|
|
||||||
className={cn("leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-action"
|
|
||||||
className={cn(
|
|
||||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-content"
|
|
||||||
className={cn("px-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="card-footer"
|
|
||||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardFooter,
|
|
||||||
CardTitle,
|
|
||||||
CardAction,
|
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
}
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as RechartsPrimitive from "recharts"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
|
||||||
|
|
||||||
export type ChartConfig = {
|
|
||||||
[k in string]: {
|
|
||||||
label?: React.ReactNode
|
|
||||||
icon?: React.ComponentType
|
|
||||||
} & (
|
|
||||||
| { color?: string; theme?: never }
|
|
||||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
|
||||||
config: ChartConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChartContainer({
|
|
||||||
id,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
config,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
config: ChartConfig
|
|
||||||
children: React.ComponentProps<
|
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
|
||||||
>["children"]
|
|
||||||
}) {
|
|
||||||
const uniqueId = React.useId()
|
|
||||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-slot="chart"
|
|
||||||
data-chart={chartId}
|
|
||||||
className={cn(
|
|
||||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
||||||
const colorConfig = Object.entries(config).filter(
|
|
||||||
([, config]) => config.theme || config.color
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!colorConfig.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(
|
|
||||||
([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color =
|
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
||||||
|
|
||||||
function ChartTooltipContent({
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
labelClassName,
|
|
||||||
formatter,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload
|
|
||||||
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const value =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return (
|
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
||||||
}, [
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload
|
|
||||||
.filter((item) => item.type !== "none")
|
|
||||||
.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
|
||||||
indicator === "dot" && "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
|
||||||
|
|
||||||
function ChartLegendContent({
|
|
||||||
className,
|
|
||||||
hideIcon = false,
|
|
||||||
payload,
|
|
||||||
verticalAlign = "bottom",
|
|
||||||
nameKey,
|
|
||||||
}: React.ComponentProps<"div"> &
|
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean
|
|
||||||
nameKey?: string
|
|
||||||
}) {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{payload
|
|
||||||
.filter((item) => item.type !== "none")
|
|
||||||
.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(
|
|
||||||
config: ChartConfig,
|
|
||||||
payload: unknown,
|
|
||||||
key: string
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload &&
|
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey: string = key
|
|
||||||
|
|
||||||
if (
|
|
||||||
key in payload &&
|
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[
|
|
||||||
key as keyof typeof payloadPayload
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key as keyof typeof config]
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Checkbox({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="grid place-content-center text-current transition-none"
|
|
||||||
>
|
|
||||||
<CheckIcon className="size-3.5" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react"
|
|
||||||
import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupInput,
|
|
||||||
} from "@/components/ui/input-group"
|
|
||||||
|
|
||||||
const Combobox = ComboboxPrimitive.Root
|
|
||||||
|
|
||||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
|
||||||
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxTrigger({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Trigger.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Trigger
|
|
||||||
data-slot="combobox-trigger"
|
|
||||||
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDownIcon
|
|
||||||
data-slot="combobox-trigger-icon"
|
|
||||||
className="text-muted-foreground pointer-events-none size-4"
|
|
||||||
/>
|
|
||||||
</ComboboxPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Clear
|
|
||||||
data-slot="combobox-clear"
|
|
||||||
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<XIcon className="pointer-events-none" />
|
|
||||||
</ComboboxPrimitive.Clear>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxInput({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
disabled = false,
|
|
||||||
showTrigger = true,
|
|
||||||
showClear = false,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Input.Props & {
|
|
||||||
showTrigger?: boolean
|
|
||||||
showClear?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<InputGroup className={cn("w-auto", className)}>
|
|
||||||
<ComboboxPrimitive.Input
|
|
||||||
render={<InputGroupInput disabled={disabled} />}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<InputGroupAddon align="inline-end">
|
|
||||||
{showTrigger && (
|
|
||||||
<InputGroupButton
|
|
||||||
size="icon-xs"
|
|
||||||
variant="ghost"
|
|
||||||
asChild
|
|
||||||
data-slot="input-group-button"
|
|
||||||
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<ComboboxTrigger />
|
|
||||||
</InputGroupButton>
|
|
||||||
)}
|
|
||||||
{showClear && <ComboboxClear disabled={disabled} />}
|
|
||||||
</InputGroupAddon>
|
|
||||||
{children}
|
|
||||||
</InputGroup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxContent({
|
|
||||||
className,
|
|
||||||
side = "bottom",
|
|
||||||
sideOffset = 6,
|
|
||||||
align = "start",
|
|
||||||
alignOffset = 0,
|
|
||||||
anchor,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Popup.Props &
|
|
||||||
Pick<
|
|
||||||
ComboboxPrimitive.Positioner.Props,
|
|
||||||
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
|
|
||||||
>) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Portal>
|
|
||||||
<ComboboxPrimitive.Positioner
|
|
||||||
side={side}
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
align={align}
|
|
||||||
alignOffset={alignOffset}
|
|
||||||
anchor={anchor}
|
|
||||||
className="isolate z-50"
|
|
||||||
>
|
|
||||||
<ComboboxPrimitive.Popup
|
|
||||||
data-slot="combobox-content"
|
|
||||||
data-chips={!!anchor}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 group/combobox-content relative max-h-96 w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md shadow-md ring-1 duration-100 data-[chips=true]:min-w-(--anchor-width) *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ComboboxPrimitive.Positioner>
|
|
||||||
</ComboboxPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.List
|
|
||||||
data-slot="combobox-list"
|
|
||||||
className={cn(
|
|
||||||
"max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Item.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Item
|
|
||||||
data-slot="combobox-item"
|
|
||||||
className={cn(
|
|
||||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ComboboxPrimitive.ItemIndicator
|
|
||||||
data-slot="combobox-item-indicator"
|
|
||||||
render={
|
|
||||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CheckIcon className="pointer-events-none size-4 pointer-coarse:size-5" />
|
|
||||||
</ComboboxPrimitive.ItemIndicator>
|
|
||||||
</ComboboxPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Group
|
|
||||||
data-slot="combobox-group"
|
|
||||||
className={cn(className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.GroupLabel
|
|
||||||
data-slot="combobox-label"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground px-2 py-1.5 text-xs pointer-coarse:px-3 pointer-coarse:py-2 pointer-coarse:text-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Empty
|
|
||||||
data-slot="combobox-empty"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Separator.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Separator
|
|
||||||
data-slot="combobox-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChips({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
|
||||||
ComboboxPrimitive.Chips.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Chips
|
|
||||||
data-slot="combobox-chips"
|
|
||||||
className={cn(
|
|
||||||
"dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-[3px] has-aria-invalid:ring-[3px] has-data-[slot=combobox-chip]:px-1.5",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChip({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showRemove = true,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Chip.Props & {
|
|
||||||
showRemove?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Chip
|
|
||||||
data-slot="combobox-chip"
|
|
||||||
className={cn(
|
|
||||||
"bg-muted text-foreground flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showRemove && (
|
|
||||||
<ComboboxPrimitive.ChipRemove
|
|
||||||
render={<Button variant="ghost" size="icon-sm" />}
|
|
||||||
className="-ml-1 opacity-50 hover:opacity-100"
|
|
||||||
data-slot="combobox-chip-remove"
|
|
||||||
>
|
|
||||||
<XIcon className="pointer-events-none" />
|
|
||||||
</ComboboxPrimitive.ChipRemove>
|
|
||||||
)}
|
|
||||||
</ComboboxPrimitive.Chip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ComboboxChipsInput({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: ComboboxPrimitive.Input.Props) {
|
|
||||||
return (
|
|
||||||
<ComboboxPrimitive.Input
|
|
||||||
data-slot="combobox-chip-input"
|
|
||||||
className={cn("min-w-16 flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useComboboxAnchor() {
|
|
||||||
return React.useRef<HTMLDivElement | null>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Combobox,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxContent,
|
|
||||||
ComboboxList,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxGroup,
|
|
||||||
ComboboxLabel,
|
|
||||||
ComboboxCollection,
|
|
||||||
ComboboxEmpty,
|
|
||||||
ComboboxSeparator,
|
|
||||||
ComboboxChips,
|
|
||||||
ComboboxChip,
|
|
||||||
ComboboxChipsInput,
|
|
||||||
ComboboxTrigger,
|
|
||||||
ComboboxValue,
|
|
||||||
useComboboxAnchor,
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Dialog({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Overlay
|
|
||||||
data-slot="dialog-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
showCloseButton = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
||||||
showCloseButton?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DialogPortal data-slot="dialog-portal">
|
|
||||||
<DialogOverlay />
|
|
||||||
<DialogPrimitive.Content
|
|
||||||
data-slot="dialog-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
{showCloseButton && (
|
|
||||||
<DialogPrimitive.Close
|
|
||||||
data-slot="dialog-close"
|
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
||||||
>
|
|
||||||
<XIcon />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</DialogPrimitive.Close>
|
|
||||||
)}
|
|
||||||
</DialogPrimitive.Content>
|
|
||||||
</DialogPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-header"
|
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="dialog-footer"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Title
|
|
||||||
data-slot="dialog-title"
|
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DialogDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DialogPrimitive.Description
|
|
||||||
data-slot="dialog-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogOverlay,
|
|
||||||
DialogPortal,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Drawer({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
|
||||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
|
||||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
|
||||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
|
||||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Overlay
|
|
||||||
data-slot="drawer-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DrawerPortal data-slot="drawer-portal">
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerPrimitive.Content
|
|
||||||
data-slot="drawer-content"
|
|
||||||
className={cn(
|
|
||||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
|
||||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
|
||||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
|
||||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
|
||||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
|
||||||
{children}
|
|
||||||
</DrawerPrimitive.Content>
|
|
||||||
</DrawerPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-header"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
data-slot="drawer-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Description
|
|
||||||
data-slot="drawer-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerDescription,
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function DropdownMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
|
||||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Trigger
|
|
||||||
data-slot="dropdown-menu-trigger"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuContent({
|
|
||||||
className,
|
|
||||||
sideOffset = 4,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Portal>
|
|
||||||
<DropdownMenuPrimitive.Content
|
|
||||||
data-slot="dropdown-menu-content"
|
|
||||||
sideOffset={sideOffset}
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</DropdownMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Item
|
|
||||||
data-slot="dropdown-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="dropdown-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioGroup
|
|
||||||
data-slot="dropdown-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.RadioItem
|
|
||||||
data-slot="dropdown-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Label
|
|
||||||
data-slot="dropdown-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.Separator
|
|
||||||
data-slot="dropdown-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="dropdown-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
|
||||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
|
||||||
data-slot="dropdown-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto size-4" />
|
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DropdownMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<DropdownMenuPrimitive.SubContent
|
|
||||||
data-slot="dropdown-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuShortcut,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Empty({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty"
|
|
||||||
className={cn(
|
|
||||||
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-header"
|
|
||||||
className={cn(
|
|
||||||
"flex max-w-sm flex-col items-center gap-2 text-center",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyMediaVariants = cva(
|
|
||||||
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "bg-transparent",
|
|
||||||
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function EmptyMedia({
|
|
||||||
className,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-icon"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(emptyMediaVariants({ variant, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-title"
|
|
||||||
className={cn("text-lg font-medium tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-description"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="empty-content"
|
|
||||||
className={cn(
|
|
||||||
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Empty,
|
|
||||||
EmptyHeader,
|
|
||||||
EmptyTitle,
|
|
||||||
EmptyDescription,
|
|
||||||
EmptyContent,
|
|
||||||
EmptyMedia,
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import type { VariantProps } from 'class-variance-authority';
|
|
||||||
import { cva } from 'class-variance-authority';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function FieldSet({ className, ...props }: React.ComponentProps<'fieldset'>) {
|
|
||||||
return (
|
|
||||||
<fieldset
|
|
||||||
data-slot="field-set"
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col gap-6',
|
|
||||||
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLegend({
|
|
||||||
className,
|
|
||||||
variant = 'legend',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
|
|
||||||
return (
|
|
||||||
<legend
|
|
||||||
data-slot="field-legend"
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
'mb-3 font-medium',
|
|
||||||
'data-[variant=legend]:text-base',
|
|
||||||
'data-[variant=label]:text-sm',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-group"
|
|
||||||
className={cn(
|
|
||||||
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldVariants = cva(
|
|
||||||
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
orientation: {
|
|
||||||
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
|
|
||||||
horizontal: [
|
|
||||||
'flex-row items-center',
|
|
||||||
'[&>[data-slot=field-label]]:flex-auto',
|
|
||||||
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
||||||
],
|
|
||||||
responsive: [
|
|
||||||
'flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto',
|
|
||||||
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
|
|
||||||
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
orientation: 'vertical',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function Field({
|
|
||||||
className,
|
|
||||||
orientation = 'vertical',
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="field"
|
|
||||||
data-orientation={orientation}
|
|
||||||
className={cn(fieldVariants({ orientation }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-content"
|
|
||||||
className={cn(
|
|
||||||
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Label>) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
|
|
||||||
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4',
|
|
||||||
'has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-label"
|
|
||||||
className={cn(
|
|
||||||
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
data-slot="field-description"
|
|
||||||
className={cn(
|
|
||||||
'text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance',
|
|
||||||
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
|
|
||||||
'[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldSeparator({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="field-separator"
|
|
||||||
data-content={!!children}
|
|
||||||
className={cn(
|
|
||||||
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<Separator className="absolute inset-0 top-1/2" />
|
|
||||||
{children && (
|
|
||||||
<span
|
|
||||||
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
||||||
data-slot="field-separator-content"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FieldError({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
errors,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'div'> & {
|
|
||||||
errors?: Array<{ message?: string } | undefined>;
|
|
||||||
}) {
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (children) {
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!errors?.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueErrors = [
|
|
||||||
...new Map(errors.map(error => [error?.message, error])).values(),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (uniqueErrors?.length == 1) {
|
|
||||||
return uniqueErrors[0]?.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
||||||
{uniqueErrors.map(
|
|
||||||
(error, index) =>
|
|
||||||
error?.message && <li key={index}>{error.message}</li>,
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}, [children, errors]);
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="alert"
|
|
||||||
data-slot="field-error"
|
|
||||||
className={cn('text-destructive text-sm font-normal', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Field,
|
|
||||||
FieldContent,
|
|
||||||
FieldDescription,
|
|
||||||
FieldError,
|
|
||||||
FieldGroup,
|
|
||||||
FieldLabel,
|
|
||||||
FieldLegend,
|
|
||||||
FieldSeparator,
|
|
||||||
FieldSet,
|
|
||||||
FieldTitle,
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
|
||||||
|
|
||||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="input-group"
|
|
||||||
role="group"
|
|
||||||
className={cn(
|
|
||||||
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
||||||
|
|
||||||
// Variants based on alignment.
|
|
||||||
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
||||||
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
||||||
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
||||||
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
||||||
|
|
||||||
// Focus state.
|
|
||||||
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
|
||||||
|
|
||||||
// Error state.
|
|
||||||
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
||||||
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupAddonVariants = cva(
|
|
||||||
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
align: {
|
|
||||||
"inline-start":
|
|
||||||
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
||||||
"inline-end":
|
|
||||||
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
||||||
"block-start":
|
|
||||||
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
||||||
"block-end":
|
|
||||||
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
align: "inline-start",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupAddon({
|
|
||||||
className,
|
|
||||||
align = "inline-start",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="group"
|
|
||||||
data-slot="input-group-addon"
|
|
||||||
data-align={align}
|
|
||||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
||||||
onClick={(e) => {
|
|
||||||
if ((e.target as HTMLElement).closest("button")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.currentTarget.parentElement?.querySelector("input")?.focus()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputGroupButtonVariants = cva(
|
|
||||||
"text-sm shadow-none flex gap-2 items-center",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
size: {
|
|
||||||
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
||||||
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
||||||
"icon-xs":
|
|
||||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
||||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
size: "xs",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function InputGroupButton({
|
|
||||||
className,
|
|
||||||
type = "button",
|
|
||||||
variant = "ghost",
|
|
||||||
size = "xs",
|
|
||||||
...props
|
|
||||||
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
||||||
VariantProps<typeof inputGroupButtonVariants>) {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
type={type}
|
|
||||||
data-size={size}
|
|
||||||
variant={variant}
|
|
||||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"input">) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InputGroupTextarea({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"textarea">) {
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
data-slot="input-group-control"
|
|
||||||
className={cn(
|
|
||||||
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
InputGroup,
|
|
||||||
InputGroupAddon,
|
|
||||||
InputGroupButton,
|
|
||||||
InputGroupText,
|
|
||||||
InputGroupInput,
|
|
||||||
InputGroupTextarea,
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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 }
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
||||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
||||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Label({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<LabelPrimitive.Root
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Label };
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Select({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
|
||||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
|
||||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectValue({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
|
||||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectTrigger({
|
|
||||||
className,
|
|
||||||
size = "default",
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
|
||||||
size?: "sm" | "default"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
data-slot="select-trigger"
|
|
||||||
data-size={size}
|
|
||||||
className={cn(
|
|
||||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<ChevronDownIcon className="size-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
position = "item-aligned",
|
|
||||||
align = "center",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
data-slot="select-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
align={align}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
data-slot="select-label"
|
|
||||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
data-slot="select-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-slot="select-item-indicator"
|
|
||||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
|
||||||
>
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
data-slot="select-separator"
|
|
||||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollUpButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
data-slot="select-scroll-up-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectScrollDownButton({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
|
||||||
return (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
data-slot="select-scroll-down-button"
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon className="size-4" />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectGroup,
|
|
||||||
SelectItem,
|
|
||||||
SelectLabel,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
function Separator({
|
|
||||||
className,
|
|
||||||
orientation = 'horizontal',
|
|
||||||
decorative = true,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<SeparatorPrimitive.Root
|
|
||||||
data-slot="separator"
|
|
||||||
decorative={decorative}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Separator };
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { formatHex, oklch } from 'culori';
|
|
||||||
import QR from 'qrcode';
|
|
||||||
import { type HTMLAttributes, useEffect, useState } from 'react';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
export type QRCodeProps = HTMLAttributes<HTMLDivElement> & {
|
|
||||||
data: string;
|
|
||||||
foreground?: string;
|
|
||||||
background?: string;
|
|
||||||
robustness?: 'L' | 'M' | 'Q' | 'H';
|
|
||||||
};
|
|
||||||
|
|
||||||
const oklchRegex = /oklch\(([0-9.]+)\s+([0-9.]+)\s+([0-9.]+)\)/;
|
|
||||||
|
|
||||||
const getOklch = (color: string, fallback: [number, number, number]) => {
|
|
||||||
const oklchMatch = color.match(oklchRegex);
|
|
||||||
|
|
||||||
if (!oklchMatch) {
|
|
||||||
return { l: fallback[0], c: fallback[1], h: fallback[2] };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
l: Number.parseFloat(oklchMatch[1]),
|
|
||||||
c: Number.parseFloat(oklchMatch[2]),
|
|
||||||
h: Number.parseFloat(oklchMatch[3]),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QRCode = ({
|
|
||||||
data,
|
|
||||||
foreground,
|
|
||||||
background,
|
|
||||||
robustness = 'M',
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: QRCodeProps) => {
|
|
||||||
const [svg, setSVG] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const generateQR = async () => {
|
|
||||||
try {
|
|
||||||
const styles = getComputedStyle(document.documentElement);
|
|
||||||
const foregroundColor =
|
|
||||||
foreground ?? styles.getPropertyValue('--foreground');
|
|
||||||
const backgroundColor =
|
|
||||||
background ?? styles.getPropertyValue('--background');
|
|
||||||
|
|
||||||
const foregroundOklch = getOklch(
|
|
||||||
foregroundColor,
|
|
||||||
[0.21, 0.006, 285.885]
|
|
||||||
);
|
|
||||||
const backgroundOklch = getOklch(backgroundColor, [0.985, 0, 0]);
|
|
||||||
|
|
||||||
const newSvg = await QR.toString(data, {
|
|
||||||
type: 'svg',
|
|
||||||
color: {
|
|
||||||
dark: formatHex(oklch({ mode: 'oklch', ...foregroundOklch })),
|
|
||||||
light: formatHex(oklch({ mode: 'oklch', ...backgroundOklch })),
|
|
||||||
},
|
|
||||||
width: 200,
|
|
||||||
errorCorrectionLevel: robustness,
|
|
||||||
margin: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
setSVG(newSvg);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateQR();
|
|
||||||
}, [data, foreground, background, robustness]);
|
|
||||||
|
|
||||||
if (!svg) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn('size-full', '[&_svg]:size-full', className)}
|
|
||||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required for SVG"
|
|
||||||
dangerouslySetInnerHTML={{ __html: svg }}
|
|
||||||
{...(props as any)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user