feat(client): profile improvements
Some checks failed
Client CMS Check Build (NixCN CMS) TeamCity build failed
Backend Check Build (NixCN CMS) TeamCity build finished

Signed-off-by: Noa Virellia <noa@requiem.garden>
This commit is contained in:
2026-01-29 22:17:16 +08:00
parent 5da6e9ce25
commit b70095c99e
21 changed files with 1114 additions and 83 deletions

View File

@@ -3,7 +3,7 @@ import pluginQuery from '@tanstack/eslint-plugin-query';
export default antfu({ export default antfu({
gitignore: true, gitignore: true,
ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*'], ignores: ['**/node_modules/**', '**/dist/**', 'bun.lock', '**/routeTree.gen.ts', '**/ui/**', 'src/components/editor/**/*', 'src/client/**/*'],
react: true, react: true,
stylistic: { stylistic: {
semi: true, semi: true,

View File

@@ -3,6 +3,7 @@
"type": "module", "type": "module",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
@@ -11,6 +12,10 @@
"gen": "openapi-ts" "gen": "openapi-ts"
}, },
"dependencies": { "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/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
@@ -25,6 +30,7 @@
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10", "@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
@@ -98,6 +104,5 @@
}, },
"lint-staged": { "lint-staged": {
"*": "eslint --fix" "*": "eslint --fix"
}, }
"packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316"
} }

View File

@@ -8,6 +8,18 @@ importers:
.: .:
dependencies: dependencies:
'@base-ui/react':
specifier: ^1.1.0
version: 1.1.0(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@dicebear/collection':
specifier: ^9.3.1
version: 9.3.1(@dicebear/core@9.3.1)
'@dicebear/core':
specifier: ^9.3.1
version: 9.3.1
'@dicebear/identicon':
specifier: ^9.3.1
version: 9.3.1(@dicebear/core@9.3.1)
'@dnd-kit/core': '@dnd-kit/core':
specifier: ^6.3.1 specifier: ^6.3.1
version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -50,6 +62,9 @@ importers:
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4(@types/react@19.2.8)(react@19.2.3) version: 1.2.4(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-switch':
specifier: ^1.2.6
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-tabs': '@radix-ui/react-tabs':
specifier: ^1.1.13 specifier: ^1.1.13
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -406,12 +421,229 @@ packages:
resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@base-ui/react@1.1.0':
resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==}
engines: {node: '>=14.0.0'}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@base-ui/utils@0.2.4':
resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==}
peerDependencies:
'@types/react': ^17 || ^18 || ^19
react: ^17 || ^18 || ^19
react-dom: ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@clack/core@0.5.0': '@clack/core@0.5.0':
resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==}
'@clack/prompts@0.11.0': '@clack/prompts@0.11.0':
resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==}
'@dicebear/adventurer-neutral@9.3.1':
resolution: {integrity: sha512-MKrzLkAGx0cdBVD+XJu6ERhdJjWsjoFS+0nF9MZT17h/m/Q12FSoj+ACoKTEXBS/LBQfQqjA9HstBlSxMzmBdw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/adventurer@9.3.1':
resolution: {integrity: sha512-MBCA8QtRC4mWbYncFDxI67LxxXMccsORqJS8osD4F/MgOPMJsdoN9QrRfsY/MjO+4NbTSxsVzOhn2nf1WzoLbA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/avataaars-neutral@9.3.1':
resolution: {integrity: sha512-d9enbUJcRfMui0ZESJ9ofJXKJPdqrzKgqefT9fcC8EfOvP0WqVtsUzcPj9l6FYhG1fMDdTsx+A8e//1lCynbQQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/avataaars@9.3.1':
resolution: {integrity: sha512-gQwtaTfPVwNAvVktdTjyhGnQtt5ifeE/6XyMX/fUJTTo/uI2NLy4LedzjsibA/DW8xi+TbgUyXlyTaJs0H6MGA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-ears-neutral@9.3.1':
resolution: {integrity: sha512-Fvw/GoT+3q77zwUbHOujGujQ4oVgtoOXE7ByfxcPeVcaUUTRARpWXlNwUBg0zt+o/Dfv875awpt3sIgKuecGsw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-ears@9.3.1':
resolution: {integrity: sha512-JN2ZlrSvpKJNyRAFzyeg+Y5wBG0EZQc8Ds5bZIHkf2/uaLUQIeDT1At2Sr7hSJDKSYZ8z83H6ckbzpDl5b9MzQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/big-smile@9.3.1':
resolution: {integrity: sha512-c5USb4n3Zw32WIJUZqc2+mCe3vbN6XJtZjKtFbisFujMAX6I3avRf6S1JBbm5oT86ynGH6P1/EZ2K7WkThfEBg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/bottts-neutral@9.3.1':
resolution: {integrity: sha512-Ea3dZ7/absDmedpFIZp+yoeS6Dq0sZ8W87xw39SS45Mr1s3i4lVd/0XWc9U5QBl0XrP4CQKB7b2QpcSY4tIYtg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/bottts@9.3.1':
resolution: {integrity: sha512-qIPokserYLIwpScbsvFADwspBfa1Mg8JFEtYcXYcbPLnNek8bZiAhpQSc1bHSqHjm10bFEjvTr0opSNr72CBzw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/collection@9.3.1':
resolution: {integrity: sha512-3hDYu9K4quu9jiXQTno2e0AyBzmrqm1PE6Mw7u2gYOZZ5GsSqrDdNHQODShyzqDF1LuyypZY4XN4YjFJ6fWqig==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/core@9.3.1':
resolution: {integrity: sha512-N6Gl9z3SxYp1OVtOzQegtURFqr0D62l3QcXgvshDAVXDNjkziZ5gWj//JxYJRWldNZfVp9/pm97V3ExKI5AXPg==}
engines: {node: '>=18.0.0'}
'@dicebear/croodles-neutral@9.3.1':
resolution: {integrity: sha512-NEOV/j+pqxhFmxSC4EFjPgjbTsnOXkX3WgLLVz0PZBpVpS2kPOwBQXZT2fUGZIq7zHucWWatnMaKBvd2LYmFhA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/croodles@9.3.1':
resolution: {integrity: sha512-p40OXll38AYpWh7vOVuapv6ClQuzaMh77e++QJjJGNr6n1OE6YmmQbp6XzE7iELzz2yGoCPIm/FjI+zcH0aAwA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/dylan@9.3.1':
resolution: {integrity: sha512-CWP8S9heivya/KSSF72IJ6QKE5bUsoxKSlnLD21uO+NAm5Mzkw00PM0cgA4RvnPNf0Et7HmoJXrrvOBavWE65A==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/fun-emoji@9.3.1':
resolution: {integrity: sha512-oSJPxHvAnORxa3FJYwYGQcUuP5LIFRKzMJJ9RP4D5GTYmpjsdG0K895eo4vKkXrY/BVNVROBlfK0KcTX1xOU8g==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/glass@9.3.1':
resolution: {integrity: sha512-yNxDgIE9A+/n5VgOMH8P0qb1EGsMhMSSrl0s6ZnTBpHLGwRv1iGXezJaZrkx/ZSPsp8KlOOfTodeqi2vkVdFHg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/icons@9.3.1':
resolution: {integrity: sha512-p8BrJ/6C2smLKU8vFFu+B54zD/GFbdjumVubzcURjvbOh1YOWU2CD4TruSBZ4d2zbAwgJIAjE+5oANB0a1gfdg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/identicon@9.3.1':
resolution: {integrity: sha512-P3TmN7pRqlS8S9/1E+lGEMrBbQvjjXGNgXnw+Okviq+41172iLVg6Wv0nbNsOyF9QjRTjrJMq4VT3XgOuU4JAg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/initials@9.3.1':
resolution: {integrity: sha512-1O61oYxKVeeGL6QcNCcxH7zsqbp37NmHbR/Y5CVqr6AVv0bBswvCzVzUv/Zmmsp70DoYQB+lbX+oNIdcqUWAaw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/lorelei-neutral@9.3.1':
resolution: {integrity: sha512-GP2EX3w8Di4b3XN0uM7lARbg1iZ9r0zaZHlUbCE2CzFy3xxrKSrRDYf2BvVt7x76doijXR5SLm4DMbEA9ARJWA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/lorelei@9.3.1':
resolution: {integrity: sha512-4btARyv+ITuL3GWKA68/h6hAPL52lN1034JHx+dJCjy7zXrsXvFKkQj62LbCkQKHQOihTkAW1dfccVQ7mlGn4w==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/micah@9.3.1':
resolution: {integrity: sha512-LAPY6Zlw/nh0Xts4aIY5d0hlaJEfSah+M5GoBRzKFKlieYdee7hvE8gsCE+OZ4pZUc98Dh7h0XXqVt/ojYW3jw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/miniavs@9.3.1':
resolution: {integrity: sha512-LRLKxDAIk8fW/88YB0vbYiJ850FaO2EcdznOfyW0izDp0ghTGZXsO5B5RUiLTunH8ZCnDdA+DtaugaFTdvOx/Q==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/notionists-neutral@9.3.1':
resolution: {integrity: sha512-Z9dikJjibAc94EtFnHQb1+ADMISLedgLls5+ARiKwKjPcYZcuRm4U8kR9tMLmqggro10uJlT7YrLSCC/5abUXA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/notionists@9.3.1':
resolution: {integrity: sha512-xSukD2J+iKaKq/kEOZ6svwon9sQYRpgIeNC7Gfskb7uyC+iUHQmCy6hSxLFGIOFVqEbw6Ow8uNpn9NqaFpQA4w==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/open-peeps@9.3.1':
resolution: {integrity: sha512-g4A3XcLrKPy44ajlhWfmGXYUDzXfogzB/H7Z46k+mxvrhVSF0jsmReYjX80jqHNeEZ9ikIpR5g4Hbo6vmOmjGQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/personas@9.3.1':
resolution: {integrity: sha512-2xqaiY0/uHKFNhC+ZEBINJZM9/fC8gUMFCqP4N6QuXkFbqNZn4RjgbTITkGtRE5Z4m2q9hEfPey4Dc9jep5lzA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/pixel-art-neutral@9.3.1':
resolution: {integrity: sha512-N3mcC4CFTAMk3TqRvZVsZAGY2NONnQwoGpP+MD4E2GF+kVWoQYpvzOybVgFoOz2G0Oe4HAwSO5Qt7KTbAiD7KA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/pixel-art@9.3.1':
resolution: {integrity: sha512-yUufylvVqkb9wpG/sYRzNTeSk1YbzVgSq/ZSMyxy1kx/R4BhOkiZBSs6Ra3VjeKWVNDBzUWERaVdylLbFvAQaw==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/rings@9.3.1':
resolution: {integrity: sha512-1bQTKJbVzpBPbhSyHS5bzlRjYIRQKO1hR0JGmC/ZWFiE9+ySk/NNwkNghvcDxvDUaz02NLSJXlSp2T8nrsdNHg==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/shapes@9.3.1':
resolution: {integrity: sha512-xzw/BWSQCznRDFBp8DKQtg1Jxawq+R3upOM2pURwbCPC+9bi8f8CAz1SExA3tlAbbrVx0HQdRKIYS3GW6/GBBA==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/thumbs@9.3.1':
resolution: {integrity: sha512-HS14oyT9HXLT8OPqEz8n0Bdob3oRWoNZ5PSZrxT4nyYXxh0rDSxCCOFwPKanXznk1qCAngtAvuzzID3vo7UG3A==}
engines: {node: '>=18.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dicebear/toon-head@9.3.1':
resolution: {integrity: sha512-9a9ydhbrVG57NuscH92yzIMQ0yxEPgJtzOMG1QR6jWctgbeEuzQvJDPvJQTxtfFjx71VlQNsSL40/5rnMtCaTw==}
engines: {node: '>=16.0.0'}
peerDependencies:
'@dicebear/core': ^9.0.0
'@dnd-kit/accessibility@3.1.1': '@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies: peerDependencies:
@@ -1105,6 +1337,19 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-switch@1.2.6':
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-tabs@1.1.13': '@radix-ui/react-tabs@1.1.13':
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
peerDependencies: peerDependencies:
@@ -3793,6 +4038,9 @@ packages:
require-main-filename@2.0.0: require-main-filename@2.0.0:
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
reserved-identifiers@1.2.0: reserved-identifiers@1.2.0:
resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -3960,6 +4208,9 @@ packages:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
tabbable@6.4.0:
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
tagged-tag@1.0.0: tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'} engines: {node: '>=20'}
@@ -4470,6 +4721,31 @@ snapshots:
'@babel/helper-string-parser': 7.27.1 '@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5 '@babel/helper-validator-identifier': 7.28.5
'@base-ui/react@1.1.0(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.28.6
'@base-ui/utils': 0.2.4(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@floating-ui/utils': 0.2.10
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
reselect: 5.1.1
tabbable: 6.4.0
use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.8
'@base-ui/utils@0.2.4(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@babel/runtime': 7.28.6
'@floating-ui/utils': 0.2.10
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
reselect: 5.1.1
use-sync-external-store: 1.6.0(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.8
'@clack/core@0.5.0': '@clack/core@0.5.0':
dependencies: dependencies:
picocolors: 1.1.1 picocolors: 1.1.1
@@ -4481,6 +4757,169 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
sisteransi: 1.0.5 sisteransi: 1.0.5
'@dicebear/adventurer-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/adventurer@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/avataaars-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/avataaars@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/big-ears-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/big-ears@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/big-smile@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/bottts-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/bottts@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/collection@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/adventurer': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/adventurer-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/avataaars': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/avataaars-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/big-ears': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/big-ears-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/big-smile': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/bottts': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/bottts-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/core': 9.3.1
'@dicebear/croodles': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/croodles-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/dylan': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/fun-emoji': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/glass': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/icons': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/identicon': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/initials': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/lorelei': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/lorelei-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/micah': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/miniavs': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/notionists': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/notionists-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/open-peeps': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/personas': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/pixel-art': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/pixel-art-neutral': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/rings': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/shapes': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/thumbs': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/toon-head': 9.3.1(@dicebear/core@9.3.1)
'@dicebear/core@9.3.1':
dependencies:
'@types/json-schema': 7.0.15
'@dicebear/croodles-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/croodles@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/dylan@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/fun-emoji@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/glass@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/icons@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/identicon@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/initials@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/lorelei-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/lorelei@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/micah@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/miniavs@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/notionists-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/notionists@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/open-peeps@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/personas@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/pixel-art-neutral@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/pixel-art@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/rings@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/shapes@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/thumbs@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dicebear/toon-head@9.3.1(@dicebear/core@9.3.1)':
dependencies:
'@dicebear/core': 9.3.1
'@dnd-kit/accessibility@3.1.1(react@19.2.3)': '@dnd-kit/accessibility@3.1.1(react@19.2.3)':
dependencies: dependencies:
react: 19.2.3 react: 19.2.3
@@ -5166,6 +5605,21 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.8 '@types/react': 19.2.8
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.8)(react@19.2.3)
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.8)(react@19.2.3)
react: 19.2.3
react-dom: 19.2.3(react@19.2.3)
optionalDependencies:
'@types/react': 19.2.8
'@types/react-dom': 19.2.3(@types/react@19.2.8)
'@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.8))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
dependencies: dependencies:
'@radix-ui/primitive': 1.1.3 '@radix-ui/primitive': 1.1.3
@@ -8263,6 +8717,8 @@ snapshots:
require-main-filename@2.0.0: {} require-main-filename@2.0.0: {}
reselect@5.1.1: {}
reserved-identifiers@1.2.0: {} reserved-identifiers@1.2.0: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
@@ -8429,6 +8885,8 @@ snapshots:
dependencies: dependencies:
'@pkgr/core': 0.2.9 '@pkgr/core': 0.2.9
tabbable@6.4.0: {}
tagged-tag@1.0.0: {} tagged-tag@1.0.0: {}
tailwind-merge@3.4.0: {} tailwind-merge@3.4.0: {}

View File

@@ -38,20 +38,20 @@ function QrSection({ eventId, enabled }: { eventId: string; enabled: boolean })
const data = { data: { checkin_code: `dummy${eventId}${enabled}` } }; const data = { data: { checkin_code: `dummy${eventId}${enabled}` } };
return data return data
? ( ? (
<> <>
<div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4"> <div className="border-2 px-4 py-8 border-muted rounded-xl flex flex-col items-center justify-center p-4">
<QRCode data={data.data.checkin_code} className="size-60" /> <QRCode data={data.data.checkin_code} className="size-60" />
</div>
<DialogFooter>
<div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
{data.data.checkin_code}
</div> </div>
</DialogFooter> <DialogFooter>
</> <div className="flex flex-1 items-center ml-2 font-mono text-3xl tracking-widest text-primary/80 justify-center">
) {data.data.checkin_code}
</div>
</DialogFooter>
</>
)
: ( : (
<QrSectionSkeleton /> <QrSectionSkeleton />
); );
} }
function QrSectionSkeleton() { function QrSectionSkeleton() {

View File

@@ -1,4 +1,5 @@
import { useForm } from '@tanstack/react-form'; import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import z from 'zod'; import z from 'zod';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -21,12 +22,14 @@ import {
} from '@/components/ui/input'; } from '@/components/ui/input';
import { useUpdateUser } from '@/hooks/data/useUpdateUser'; import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo'; import { useUserInfo } from '@/hooks/data/useUserInfo';
import { Switch } from '../ui/switch';
const formSchema = z.object({ const formSchema = z.object({
username: z.string().min(5), username: z.string().min(5),
nickname: z.string().min(1), nickname: z.string(),
subtitle: z.string().min(1), subtitle: z.string(),
avatar: z.url().min(1), avatar: z.url().or(z.literal('')),
allow_public: z.boolean(),
}); });
export function EditProfileDialog() { export function EditProfileDialog() {
const { data } = useUserInfo(); const { data } = useUserInfo();
@@ -39,6 +42,7 @@ export function EditProfileDialog() {
username: user.username, username: user.username,
nickname: user.nickname, nickname: user.nickname,
subtitle: user.subtitle, subtitle: user.subtitle,
allow_public: user.allow_public,
}, },
validators: { validators: {
onBlur: formSchema, onBlur: formSchema,
@@ -57,8 +61,16 @@ export function EditProfileDialog() {
}, },
}); });
const [open, setOpen] = useState(false);
if (!open) {
setTimeout(() => {
form.reset();
}, 200);
}
return ( return (
<Dialog> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="w-full" size="lg"></Button> <Button variant="outline" className="w-full" size="lg"></Button>
</DialogTrigger> </DialogTrigger>
@@ -67,7 +79,7 @@ export function EditProfileDialog() {
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
void form.handleSubmit(); form.handleSubmit().then(() => setOpen(false));
}} }}
className="grid gap-4" className="grid gap-4"
> >
@@ -138,13 +150,24 @@ export function EditProfileDialog() {
</Field> </Field>
)} )}
</form.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> <DialogFooter>
<DialogClose asChild> <DialogClose asChild>
<Button variant="outline"></Button> <Button variant="outline"></Button>
</DialogClose> </DialogClose>
<DialogClose asChild> <form.Subscribe
<Button type="submit"></Button> selector={state => [state.canSubmit]}
</DialogClose> children={([canSubmit]) => (
<Button type="submit" disabled={!canSubmit}></Button>
)}
/>
</DialogFooter> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>

View File

@@ -1,10 +1,12 @@
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import MDEditor from '@uiw/react-md-editor'; import MDEditor from '@uiw/react-md-editor';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { Mail, Pencil } from 'lucide-react'; import { Mail, Pencil } from 'lucide-react';
import { useState } from 'react'; import { useMemo, useState } from 'react';
import Markdown from 'react-markdown'; import Markdown from 'react-markdown';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { useUpdateUser } from '@/hooks/data/useUpdateUser'; import { useUpdateUser } from '@/hooks/data/useUpdateUser';
import { useUserInfo } from '@/hooks/data/useUserInfo'; import { useUserInfo } from '@/hooks/data/useUserInfo';
import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils'; import { base64ToUtf8, utf8ToBase64 } from '@/lib/utils';
@@ -18,6 +20,14 @@ export function MainProfile() {
const [enableBioEdit, setEnableBioEdit] = useState(false); const [enableBioEdit, setEnableBioEdit] = useState(false);
const { mutateAsync } = useUpdateUser(); const { mutateAsync } = useUpdateUser();
const IdentIcon = useMemo(() => {
const avatar = createAvatar(identicon, {
size: 128,
seed: user.user_id,
}).toDataUri();
return <img src={avatar} alt="Avatar" />;
}, [user.user_id]);
return ( 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 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 w-full flex-row mt-2 lg:mt-0 lg:flex-col lg:w-max">
@@ -25,8 +35,7 @@ export function MainProfile() {
<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"> <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"> <Avatar className="size-16 rounded-full border-2 border-muted lg:size-64">
<AvatarImage src={user.avatar} alt={user.nickname} /> {user.avatar ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-1 flex-col justify-center lg:mt-3"> <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="font-semibold text-2xl" aria-hidden="true">{user.nickname}</span>
@@ -45,12 +54,12 @@ export function MainProfile() {
{/* Bio */} {/* Bio */}
{enableBioEdit {enableBioEdit
? ( ? (
<MDEditor <MDEditor
value={bio} value={bio}
onChange={setBio} onChange={setBio}
height="100%" height="100%"
/> />
) )
: <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>} : <div className="p-6 prose dark:prose-invert"><Markdown>{bio}</Markdown></div>}
<Button <Button
className="absolute bottom-4 right-4" className="absolute bottom-4 right-4"

View File

@@ -1,11 +1,13 @@
import { identicon } from '@dicebear/collection';
import { createAvatar } from '@dicebear/core';
import { import {
IconDotsVertical, IconDotsVertical,
IconLogout, IconLogout,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useMemo } from 'react';
import { import {
Avatar, Avatar,
AvatarFallback,
AvatarImage, AvatarImage,
} from '@/components/ui/avatar'; } from '@/components/ui/avatar';
import { import {
@@ -33,6 +35,14 @@ function NavUser_() {
const user = data.data!; const user = data.data!;
const { logout } = useLogout(); const { logout } = useLogout();
const IdentIcon = useMemo(() => {
const avatar = createAvatar(identicon, {
size: 128,
seed: user.user_id,
}).toDataUri();
return <img src={avatar} alt="Avatar" />;
}, [user.user_id]);
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
@@ -43,8 +53,7 @@ function NavUser_() {
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground" className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
> >
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.nickname} /> {user.avatar ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}</span> <span className="truncate font-medium">{user.nickname}</span>
@@ -64,8 +73,7 @@ function NavUser_() {
<DropdownMenuLabel className="p-0 font-normal"> <DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm"> <div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.nickname} /> {user.avatar ? <AvatarImage src={user.avatar} alt={user.nickname} /> : IdentIcon}
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{user.nickname}</span> <span className="truncate font-medium">{user.nickname}</span>

View File

@@ -36,7 +36,6 @@ export function ThemeProvider({
root.classList.add(theme); root.classList.add(theme);
}, [theme]); }, [theme]);
// eslint-disable-next-line react/no-unstable-context-value
const value = { const value = {
theme, theme,
setTheme: (theme: Theme) => { setTheme: (theme: Theme) => {

View File

@@ -0,0 +1,310 @@
"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-xs" />}
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,
}

View File

@@ -0,0 +1,168 @@
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,
}

View File

@@ -0,0 +1,33 @@
import * as React from "react"
import * as SwitchPrimitive from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
function Switch({
className,
size = "default",
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root> & {
size?: "sm" | "default"
}) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-size={size}
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 group/switch inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-[1.15rem] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block rounded-full ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
)
}
export { Switch }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -1,6 +1,6 @@
import { postAuthExchangeMutation } from "@/client/@tanstack/react-query.gen"; import { useMutation } from '@tanstack/react-query';
import { useMutation } from "@tanstack/react-query"; import { toast } from 'sonner';
import { toast } from "sonner"; import { postAuthExchangeMutation } from '@/client/@tanstack/react-query.gen';
export function useExchangeToken() { export function useExchangeToken() {
return useMutation({ return useMutation({
@@ -10,7 +10,7 @@ export function useExchangeToken() {
}, },
onError: (error) => { onError: (error) => {
console.error(error); console.error(error);
toast("An error occurred while exchanging the token. Please login manually."); toast('An error occurred while exchanging the token. Please login manually.');
} },
}) });
} }

View File

@@ -1,8 +1,8 @@
import { postAuthMagicMutation } from '@/client/@tanstack/react-query.gen';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { postAuthMagicMutation } from '@/client/@tanstack/react-query.gen';
export function useGetMagicLink() { export function useGetMagicLink() {
return useMutation({ return useMutation({
...postAuthMagicMutation() ...postAuthMagicMutation(),
}) });
} }

View File

@@ -1,5 +1,5 @@
import { getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen';
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { getUserInfoQueryKey, patchUserUpdateMutation } from '@/client/@tanstack/react-query.gen';
export function useUpdateUser() { export function useUpdateUser() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -8,5 +8,5 @@ export function useUpdateUser() {
onSuccess: async () => { onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() }); await queryClient.invalidateQueries({ queryKey: getUserInfoQueryKey() });
}, },
}) });
} }

View File

@@ -1,9 +1,9 @@
import { getUserInfoOptions } from '@/client/@tanstack/react-query.gen';
import { useSuspenseQuery } from '@tanstack/react-query'; import { useSuspenseQuery } from '@tanstack/react-query';
import { getUserInfoOptions } from '@/client/@tanstack/react-query.gen';
export function useUserInfo() { export function useUserInfo() {
return useSuspenseQuery({ return useSuspenseQuery({
...getUserInfoOptions(), ...getUserInfoOptions(),
staleTime: 10 * 60 * 1000 staleTime: 10 * 60 * 1000,
}); });
} }

View File

@@ -1,15 +1,14 @@
import { isEmpty, isNil } from 'lodash-es';
import { client } from '@/client/client.gen';
import { router } from './router';
import { import {
getToken,
getRefreshToken,
setToken,
setRefreshToken,
clearTokens, clearTokens,
doRefreshToken doRefreshToken,
} from "./token"; getRefreshToken,
import { router } from "./router"; getToken,
import { isEmpty, setRefreshToken,
isNil } from "lodash-es"; setToken,
import { client } from "@/client/client.gen"; } from './token';
export function configInternalApiClient() { export function configInternalApiClient() {
client.setConfig({ client.setConfig({
@@ -50,7 +49,8 @@ export function configInternalApiClient() {
signal: request.signal, signal: request.signal,
}); });
} }
} catch (e) { }
catch (e) {
clearTokens(); clearTokens();
await router.navigate({ to: '/authorize' }); await router.navigate({ to: '/authorize' });
return response; return response;
@@ -59,5 +59,4 @@ export function configInternalApiClient() {
} }
return response; return response;
}); });
} }

View File

@@ -1,4 +1,5 @@
import { postAuthRefresh, type ServiceAuthTokenResponse } from '@/client'; import type { ServiceAuthTokenResponse } from '@/client';
import { postAuthRefresh } from '@/client';
export function setToken(token: string) { export function setToken(token: string) {
localStorage.setItem('token', token); localStorage.setItem('token', token);
@@ -32,8 +33,8 @@ export function clearTokens() {
export async function doRefreshToken(): Promise<ServiceAuthTokenResponse | undefined> { export async function doRefreshToken(): Promise<ServiceAuthTokenResponse | undefined> {
const { data } = await postAuthRefresh({ const { data } = await postAuthRefresh({
body: { body: {
refresh_token: getRefreshToken()! refresh_token: getRefreshToken()!,
} },
}); });
return data?.data; return data?.data;
} }

View File

@@ -1,13 +1,12 @@
import { createFileRoute } from '@tanstack/react-router'; import { createFileRoute } from '@tanstack/react-router';
import { zodValidator } from '@tanstack/zod-adapter'; import { zodValidator } from '@tanstack/zod-adapter';
import { isNil } from 'lodash-es'; import { isNil } from 'lodash-es';
import { useEffect } from 'react';
import z from 'zod'; import z from 'zod';
import { LoginForm } from '@/components/login-form'; import { LoginForm } from '@/components/login-form';
import { useExchangeToken } from '@/hooks/data/useExchangeToken';
import { generateOAuthState } from '@/lib/random'; import { generateOAuthState } from '@/lib/random';
import { getToken } from '@/lib/token'; import { getToken } from '@/lib/token';
import { useExchangeToken } from '@/hooks/data/useExchangeToken';
import { useEffect } from 'react';
const baseUrl = import.meta.env.VITE_APP_BASE_URL; const baseUrl = import.meta.env.VITE_APP_BASE_URL;
@@ -39,7 +38,7 @@ function RouteComponent() {
client_id: oauthParams.client_id, client_id: oauthParams.client_id,
redirect_uri: oauthParams.redirect_uri, redirect_uri: oauthParams.redirect_uri,
state: oauthParams.state, state: oauthParams.state,
} },
}); });
} }
}, [token, mutation.isIdle]); }, [token, mutation.isIdle]);

View File

@@ -1,10 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { import {
useEffect, useEffect,
useState } from 'react'; useState,
} from 'react';
import z from 'zod'; import z from 'zod';
import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen'; import { postAuthTokenMutation } from '@/client/@tanstack/react-query.gen';
import { useMutation } from '@tanstack/react-query';
import { setRefreshToken, setToken } from '@/lib/token'; import { setRefreshToken, setToken } from '@/lib/token';
const tokenCodeSchema = z.object({ const tokenCodeSchema = z.object({
@@ -24,20 +25,20 @@ function RouteComponent() {
const mutation = useMutation({ const mutation = useMutation({
...postAuthTokenMutation(), ...postAuthTokenMutation(),
onSuccess: (data) => { onSuccess: (data) => {
setToken(data.data?.access_token!) setToken(data.data?.access_token!);
setRefreshToken(data.data?.refresh_token!) setRefreshToken(data.data?.refresh_token!);
void navigate({ to: '/' }); void navigate({ to: '/' });
}, },
onError: () => { onError: () => {
setStatus('Error getting token'); setStatus('Error getting token');
} },
}) });
useEffect(() => { useEffect(() => {
if (mutation.isIdle) { if (mutation.isIdle) {
mutation.mutate({ body: { code } }) mutation.mutate({ body: { code } });
} }
}, []) }, []);
return <div>{status}</div>; return <div>{status}</div>;
} }

View File

@@ -1,11 +1,11 @@
interface ViteTypeOptions { interface ViteTypeOptions {
strictImportMetaEnv: unknown strictImportMetaEnv: unknown;
} }
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_APP_BASE_URL: string readonly VITE_APP_BASE_URL: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv;
} }