Compare commits
13 Commits
170afb4a3b
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
313f9fec43
|
|||
|
337ce15b37
|
|||
|
37f06fe98a
|
|||
|
79fbbd1862
|
|||
|
e8571492f0
|
|||
|
f17c88547b
|
|||
|
5439b6d370
|
|||
|
78fba57f94
|
|||
|
fa50f5c771
|
|||
|
47745bbba8
|
|||
|
e1709f8e50
|
|||
|
11388c4f35
|
|||
|
b4e32d5a6d
|
@@ -82,7 +82,7 @@ func (self *UserHandler) Other(c *gin.Context) {
|
|||||||
} else if userIdFromUrl != userIdFromHeader {
|
} else if userIdFromUrl != userIdFromHeader {
|
||||||
UserInfoPayload = &service_user.UserInfoPayload{
|
UserInfoPayload = &service_user.UserInfoPayload{
|
||||||
Context: c,
|
Context: c,
|
||||||
UserId: userIdFromHeader,
|
UserId: userIdFromUrl,
|
||||||
IsOther: true,
|
IsOther: true,
|
||||||
Data: nil,
|
Data: nil,
|
||||||
}
|
}
|
||||||
|
|||||||
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,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,130 +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",
|
|
||||||
"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'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
11352
client/cms/pnpm-lock.yaml
generated
11352
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,12 +0,0 @@
|
|||||||
import { useUserInfo } from '@/hooks/data/useUserInfo';
|
|
||||||
import { CheckinScannerNavView } from './checkin-scanner-nav.view';
|
|
||||||
|
|
||||||
export function CheckinScannerNavContainer() {
|
|
||||||
const { data } = useUserInfo();
|
|
||||||
|
|
||||||
if ((data.data?.permission_level ?? 0) <= 20) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CheckinScannerNavView />;
|
|
||||||
}
|
|
||||||
@@ -1,28 +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';
|
|
||||||
|
|
||||||
export function CheckinScannerNavView() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleScan = (value: string) => {
|
|
||||||
console.log('Scanned:', 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,20 +0,0 @@
|
|||||||
import { Scanner } from '@yudiel/react-qr-scanner';
|
|
||||||
import { DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
|
|
||||||
|
|
||||||
export function CheckinScannerDialogView({ onScan }: { onScan: (value: string) => void }) {
|
|
||||||
return (
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>扫描签到码</DialogTitle>
|
|
||||||
<Scanner
|
|
||||||
onScan={(result) => {
|
|
||||||
if (result.length > 0) {
|
|
||||||
onScan(result[0].rawValue);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onError={(error) => { throw error; }}
|
|
||||||
/>
|
|
||||||
</DialogHeader>
|
|
||||||
</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,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)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
|
||||||
import { XIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
|
||||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
|
||||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
|
||||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
|
||||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Overlay
|
|
||||||
data-slot="sheet-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 SheetContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
side = "right",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
|
||||||
side?: "top" | "right" | "bottom" | "left"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<SheetPortal>
|
|
||||||
<SheetOverlay />
|
|
||||||
<SheetPrimitive.Content
|
|
||||||
data-slot="sheet-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
|
||||||
side === "right" &&
|
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
||||||
side === "left" &&
|
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
||||||
side === "top" &&
|
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
|
||||||
side === "bottom" &&
|
|
||||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary 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">
|
|
||||||
<XIcon className="size-4" />
|
|
||||||
<span className="sr-only">Close</span>
|
|
||||||
</SheetPrimitive.Close>
|
|
||||||
</SheetPrimitive.Content>
|
|
||||||
</SheetPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-header"
|
|
||||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sheet-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Title
|
|
||||||
data-slot="sheet-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SheetDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<SheetPrimitive.Description
|
|
||||||
data-slot="sheet-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sheet,
|
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
|
||||||
SheetContent,
|
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
|
||||||
}
|
|
||||||
@@ -1,726 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
import {
|
|
||||||
Sheet,
|
|
||||||
SheetContent,
|
|
||||||
SheetDescription,
|
|
||||||
SheetHeader,
|
|
||||||
SheetTitle,
|
|
||||||
} from "@/components/ui/sheet"
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip"
|
|
||||||
|
|
||||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
|
||||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
||||||
const SIDEBAR_WIDTH = "16rem"
|
|
||||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
||||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
||||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
||||||
|
|
||||||
type SidebarContextProps = {
|
|
||||||
state: "expanded" | "collapsed"
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
openMobile: boolean
|
|
||||||
setOpenMobile: (open: boolean) => void
|
|
||||||
isMobile: boolean
|
|
||||||
toggleSidebar: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
|
||||||
|
|
||||||
function useSidebar() {
|
|
||||||
const context = React.useContext(SidebarContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarProvider({
|
|
||||||
defaultOpen = true,
|
|
||||||
open: openProp,
|
|
||||||
onOpenChange: setOpenProp,
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
defaultOpen?: boolean
|
|
||||||
open?: boolean
|
|
||||||
onOpenChange?: (open: boolean) => void
|
|
||||||
}) {
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const [openMobile, setOpenMobile] = React.useState(false)
|
|
||||||
|
|
||||||
// This is the internal state of the sidebar.
|
|
||||||
// We use openProp and setOpenProp for control from outside the component.
|
|
||||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
|
||||||
const open = openProp ?? _open
|
|
||||||
const setOpen = React.useCallback(
|
|
||||||
(value: boolean | ((value: boolean) => boolean)) => {
|
|
||||||
const openState = typeof value === "function" ? value(open) : value
|
|
||||||
if (setOpenProp) {
|
|
||||||
setOpenProp(openState)
|
|
||||||
} else {
|
|
||||||
_setOpen(openState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This sets the cookie to keep the sidebar state.
|
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
|
||||||
},
|
|
||||||
[setOpenProp, open]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Helper to toggle the sidebar.
|
|
||||||
const toggleSidebar = React.useCallback(() => {
|
|
||||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
|
||||||
}, [isMobile, setOpen, setOpenMobile])
|
|
||||||
|
|
||||||
// Adds a keyboard shortcut to toggle the sidebar.
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (
|
|
||||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
|
||||||
(event.metaKey || event.ctrlKey)
|
|
||||||
) {
|
|
||||||
event.preventDefault()
|
|
||||||
toggleSidebar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
||||||
}, [toggleSidebar])
|
|
||||||
|
|
||||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
|
||||||
// This makes it easier to style the sidebar with Tailwind classes.
|
|
||||||
const state = open ? "expanded" : "collapsed"
|
|
||||||
|
|
||||||
const contextValue = React.useMemo<SidebarContextProps>(
|
|
||||||
() => ({
|
|
||||||
state,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
isMobile,
|
|
||||||
openMobile,
|
|
||||||
setOpenMobile,
|
|
||||||
toggleSidebar,
|
|
||||||
}),
|
|
||||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarContext.Provider value={contextValue}>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-wrapper"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH,
|
|
||||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
|
||||||
...style,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Sidebar({
|
|
||||||
side = "left",
|
|
||||||
variant = "sidebar",
|
|
||||||
collapsible = "offcanvas",
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
side?: "left" | "right"
|
|
||||||
variant?: "sidebar" | "floating" | "inset"
|
|
||||||
collapsible?: "offcanvas" | "icon" | "none"
|
|
||||||
}) {
|
|
||||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
|
||||||
|
|
||||||
if (collapsible === "none") {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar"
|
|
||||||
className={cn(
|
|
||||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
|
||||||
<SheetContent
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar"
|
|
||||||
data-mobile="true"
|
|
||||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
side={side}
|
|
||||||
>
|
|
||||||
<SheetHeader className="sr-only">
|
|
||||||
<SheetTitle>Sidebar</SheetTitle>
|
|
||||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
|
||||||
</SheetHeader>
|
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
|
||||||
</SheetContent>
|
|
||||||
</Sheet>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="group peer text-sidebar-foreground hidden md:block"
|
|
||||||
data-state={state}
|
|
||||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
|
||||||
data-variant={variant}
|
|
||||||
data-side={side}
|
|
||||||
data-slot="sidebar"
|
|
||||||
>
|
|
||||||
{/* This is what handles the sidebar gap on desktop */}
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-gap"
|
|
||||||
className={cn(
|
|
||||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
|
||||||
"group-data-[collapsible=offcanvas]:w-0",
|
|
||||||
"group-data-[side=right]:rotate-180",
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-container"
|
|
||||||
className={cn(
|
|
||||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
|
||||||
side === "left"
|
|
||||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
|
||||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
|
||||||
// Adjust the padding for floating and inset variants.
|
|
||||||
variant === "floating" || variant === "inset"
|
|
||||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
|
||||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-sidebar="sidebar"
|
|
||||||
data-slot="sidebar-inner"
|
|
||||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarTrigger({
|
|
||||||
className,
|
|
||||||
onClick,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Button>) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
data-sidebar="trigger"
|
|
||||||
data-slot="sidebar-trigger"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn("size-7", className)}
|
|
||||||
onClick={(event) => {
|
|
||||||
onClick?.(event)
|
|
||||||
toggleSidebar()
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<PanelLeftIcon />
|
|
||||||
<span className="sr-only">Toggle Sidebar</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
|
||||||
const { toggleSidebar } = useSidebar()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
data-sidebar="rail"
|
|
||||||
data-slot="sidebar-rail"
|
|
||||||
aria-label="Toggle Sidebar"
|
|
||||||
tabIndex={-1}
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
title="Toggle Sidebar"
|
|
||||||
className={cn(
|
|
||||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
|
||||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
|
||||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
||||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
|
||||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
||||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
|
||||||
return (
|
|
||||||
<main
|
|
||||||
data-slot="sidebar-inset"
|
|
||||||
className={cn(
|
|
||||||
"bg-background relative flex w-full flex-1 flex-col",
|
|
||||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarInput({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Input>) {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
data-slot="sidebar-input"
|
|
||||||
data-sidebar="input"
|
|
||||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-header"
|
|
||||||
data-sidebar="header"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-footer"
|
|
||||||
data-sidebar="footer"
|
|
||||||
className={cn("flex flex-col gap-2 p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof Separator>) {
|
|
||||||
return (
|
|
||||||
<Separator
|
|
||||||
data-slot="sidebar-separator"
|
|
||||||
data-sidebar="separator"
|
|
||||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-content"
|
|
||||||
data-sidebar="content"
|
|
||||||
className={cn(
|
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group"
|
|
||||||
data-sidebar="group"
|
|
||||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupLabel({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "div"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-group-label"
|
|
||||||
data-sidebar="group-label"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupAction({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-group-action"
|
|
||||||
data-sidebar="group-action"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 md:after:hidden",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarGroupContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-group-content"
|
|
||||||
data-sidebar="group-content"
|
|
||||||
className={cn("w-full text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu"
|
|
||||||
data-sidebar="menu"
|
|
||||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-item"
|
|
||||||
data-sidebar="menu-item"
|
|
||||||
className={cn("group/menu-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
outline:
|
|
||||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-8 text-sm",
|
|
||||||
sm: "h-7 text-xs",
|
|
||||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
function SidebarMenuButton({
|
|
||||||
asChild = false,
|
|
||||||
isActive = false,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
tooltip,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
isActive?: boolean
|
|
||||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
|
||||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
const { isMobile, state } = useSidebar()
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-button"
|
|
||||||
data-sidebar="menu-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!tooltip) {
|
|
||||||
return button
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof tooltip === "string") {
|
|
||||||
tooltip = {
|
|
||||||
children: tooltip,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="right"
|
|
||||||
align="center"
|
|
||||||
hidden={state !== "collapsed" || isMobile}
|
|
||||||
{...tooltip}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuAction({
|
|
||||||
className,
|
|
||||||
asChild = false,
|
|
||||||
showOnHover = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
showOnHover?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-action"
|
|
||||||
data-sidebar="menu-action"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
// Increases the hit area of the button on mobile.
|
|
||||||
"after:absolute after:-inset-2 md:after:hidden",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
showOnHover &&
|
|
||||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuBadge({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-badge"
|
|
||||||
data-sidebar="menu-badge"
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
|
||||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
|
||||||
"peer-data-[size=default]/menu-button:top-1.5",
|
|
||||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSkeleton({
|
|
||||||
className,
|
|
||||||
showIcon = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div"> & {
|
|
||||||
showIcon?: boolean
|
|
||||||
}) {
|
|
||||||
// Random width between 50 to 90%.
|
|
||||||
const width = React.useMemo(() => {
|
|
||||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="sidebar-menu-skeleton"
|
|
||||||
data-sidebar="menu-skeleton"
|
|
||||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{showIcon && (
|
|
||||||
<Skeleton
|
|
||||||
className="size-4 rounded-md"
|
|
||||||
data-sidebar="menu-skeleton-icon"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Skeleton
|
|
||||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
|
||||||
data-sidebar="menu-skeleton-text"
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--skeleton-width": width,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
data-slot="sidebar-menu-sub"
|
|
||||||
data-sidebar="menu-sub"
|
|
||||||
className={cn(
|
|
||||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"li">) {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
data-slot="sidebar-menu-sub-item"
|
|
||||||
data-sidebar="menu-sub-item"
|
|
||||||
className={cn("group/menu-sub-item relative", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SidebarMenuSubButton({
|
|
||||||
asChild = false,
|
|
||||||
size = "md",
|
|
||||||
isActive = false,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"a"> & {
|
|
||||||
asChild?: boolean
|
|
||||||
size?: "sm" | "md"
|
|
||||||
isActive?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "a"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="sidebar-menu-sub-button"
|
|
||||||
data-sidebar="menu-sub-button"
|
|
||||||
data-size={size}
|
|
||||||
data-active={isActive}
|
|
||||||
className={cn(
|
|
||||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
|
||||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
|
||||||
size === "sm" && "text-xs",
|
|
||||||
size === "md" && "text-sm",
|
|
||||||
"group-data-[collapsible=icon]:hidden",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Sidebar,
|
|
||||||
SidebarContent,
|
|
||||||
SidebarFooter,
|
|
||||||
SidebarGroup,
|
|
||||||
SidebarGroupAction,
|
|
||||||
SidebarGroupContent,
|
|
||||||
SidebarGroupLabel,
|
|
||||||
SidebarHeader,
|
|
||||||
SidebarInput,
|
|
||||||
SidebarInset,
|
|
||||||
SidebarMenu,
|
|
||||||
SidebarMenuAction,
|
|
||||||
SidebarMenuBadge,
|
|
||||||
SidebarMenuButton,
|
|
||||||
SidebarMenuItem,
|
|
||||||
SidebarMenuSkeleton,
|
|
||||||
SidebarMenuSub,
|
|
||||||
SidebarMenuSubButton,
|
|
||||||
SidebarMenuSubItem,
|
|
||||||
SidebarProvider,
|
|
||||||
SidebarRail,
|
|
||||||
SidebarSeparator,
|
|
||||||
SidebarTrigger,
|
|
||||||
useSidebar,
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user