feat: add environment variable management page with import/export functionality
- Implemented EnvPage component for managing environment variables. - Added functionality to load, add, remove, and update environment variables. - Included validation for empty and duplicate keys. - Implemented import/export features for environment variables in JSON format. - Integrated autocompletion for environment variable keys based on predefined config. feat: create user profile management with edit and password change modals - Developed ProfileCard component to display user information. - Added EditProfileModal for updating user details. - Implemented ChangePasswordModal for password modification. - Integrated user data fetching and state management using Zustand. feat: establish user store for managing user state and actions - Created user store with Zustand for managing user profile state. - Added actions for updating user information and handling loading states. feat: implement login store for user authentication - Developed login store for managing login state and actions. - Added functionality for user login and registration with error handling. feat: create reusable UI components for input groups and comboboxes - Developed InputGroup and related components for enhanced input handling. - Created Combobox component for improved selection functionality. - Added Badge component for displaying contextual information.
This commit is contained in:
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./dist/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const nextConfig: NextConfig = {
|
|||||||
distDir: 'dist',
|
distDir: 'dist',
|
||||||
basePath: basePath,
|
basePath: basePath,
|
||||||
trailingSlash: true,
|
trailingSlash: true,
|
||||||
transpilePackages: ['@kevisual/api'],
|
transpilePackages: ['@kevisual/api', "@kevisual/use-config"],
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
},
|
},
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -11,10 +11,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
"@kevisual/api": "^0.0.28",
|
"@base-ui/react": "^1.1.0",
|
||||||
|
"@kevisual/api": "^0.0.41",
|
||||||
"@kevisual/cache": "^0.0.5",
|
"@kevisual/cache": "^0.0.5",
|
||||||
"@kevisual/query": "^0.0.38",
|
"@kevisual/query": "^0.0.39",
|
||||||
"@kevisual/router": "^0.0.63",
|
"@kevisual/router": "^0.0.66",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"marked": "^17.0.1",
|
"marked": "^17.0.1",
|
||||||
"next": "16.1.5",
|
"next": "16.1.6",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "^9.13.0",
|
"react-day-picker": "^9.13.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
@@ -48,11 +49,12 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"valtio": "^2.3.0",
|
"valtio": "^2.3.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/types": "^0.0.12",
|
"@kevisual/types": "^0.0.12",
|
||||||
|
"@kevisual/use-config": "^1.0.30",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^25",
|
"@types/node": "^25",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|||||||
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@@ -11,18 +11,21 @@ importers:
|
|||||||
'@ant-design/icons':
|
'@ant-design/icons':
|
||||||
specifier: ^6.1.0
|
specifier: ^6.1.0
|
||||||
version: 6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@base-ui/react':
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
'@kevisual/api':
|
'@kevisual/api':
|
||||||
specifier: ^0.0.28
|
specifier: ^0.0.41
|
||||||
version: 0.0.28
|
version: 0.0.41
|
||||||
'@kevisual/cache':
|
'@kevisual/cache':
|
||||||
specifier: ^0.0.5
|
specifier: ^0.0.5
|
||||||
version: 0.0.5
|
version: 0.0.5
|
||||||
'@kevisual/query':
|
'@kevisual/query':
|
||||||
specifier: ^0.0.38
|
specifier: ^0.0.39
|
||||||
version: 0.0.38
|
version: 0.0.39
|
||||||
'@kevisual/router':
|
'@kevisual/router':
|
||||||
specifier: ^0.0.63
|
specifier: ^0.0.66
|
||||||
version: 0.0.63(typescript@5.9.3)
|
version: 0.0.66(typescript@5.9.3)
|
||||||
'@radix-ui/react-checkbox':
|
'@radix-ui/react-checkbox':
|
||||||
specifier: ^1.3.3
|
specifier: ^1.3.3
|
||||||
version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -96,8 +99,8 @@ importers:
|
|||||||
specifier: ^17.0.1
|
specifier: ^17.0.1
|
||||||
version: 17.0.1
|
version: 17.0.1
|
||||||
next:
|
next:
|
||||||
specifier: 16.1.5
|
specifier: 16.1.6
|
||||||
version: 16.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react:
|
react:
|
||||||
specifier: 19.2.4
|
specifier: 19.2.4
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -123,8 +126,8 @@ importers:
|
|||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.10
|
specifier: ^5.0.11
|
||||||
version: 5.0.10(@types/react@19.2.7)(react@19.2.4)
|
version: 5.0.11(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kevisual/context':
|
'@kevisual/context':
|
||||||
specifier: ^0.0.4
|
specifier: ^0.0.4
|
||||||
@@ -132,6 +135,9 @@ importers:
|
|||||||
'@kevisual/types':
|
'@kevisual/types':
|
||||||
specifier: ^0.0.12
|
specifier: ^0.0.12
|
||||||
version: 0.0.12
|
version: 0.0.12
|
||||||
|
'@kevisual/use-config':
|
||||||
|
specifier: ^1.0.30
|
||||||
|
version: 1.0.30(dotenv@17.2.3)
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4
|
specifier: ^4
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
@@ -211,6 +217,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||||
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
|
||||||
|
|
||||||
'@date-fns/tz@1.4.1':
|
'@date-fns/tz@1.4.1':
|
||||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||||
|
|
||||||
@@ -407,8 +434,8 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
'@kevisual/api@0.0.28':
|
'@kevisual/api@0.0.41':
|
||||||
resolution: {integrity: sha512-WQluRlu2qGM1qktIhPLODie8x382a6jEMfFOcay/rnkCgXK0BRpnqOKwlX7IMLdMqka7GY/BD69kSMnK1Exf5g==}
|
resolution: {integrity: sha512-cNKvdhIQ3Pt98LV0ElprZZRjY9h/l+hcIGzubcD63PtR4Yq9CTaV0d9ARggkzeDkWlFsmH1ALpru1qZ5Zwmn4w==}
|
||||||
|
|
||||||
'@kevisual/cache@0.0.5':
|
'@kevisual/cache@0.0.5':
|
||||||
resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==}
|
resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==}
|
||||||
@@ -426,66 +453,71 @@ packages:
|
|||||||
'@kevisual/load@0.0.6':
|
'@kevisual/load@0.0.6':
|
||||||
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
|
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
|
||||||
|
|
||||||
'@kevisual/query@0.0.38':
|
'@kevisual/query@0.0.39':
|
||||||
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==}
|
resolution: {integrity: sha512-3UEPBIvtdykNkrby3hvrgrHdgd17Uq+Pnr4zs+JBzATkU2eKaOqtTUJqdyIEwuySCwzGTxrnlUzWP4tziDQDLQ==}
|
||||||
|
|
||||||
'@kevisual/router@0.0.63':
|
'@kevisual/router@0.0.66':
|
||||||
resolution: {integrity: sha512-rM/FELiNtTJkjb00sdQ2f8NpWHzfOwrtgQJhsX9Np9KdzAQ5dP6pI5nAHlWvU2QyrC1VH5IibUmsBIeMMV3nWg==}
|
resolution: {integrity: sha512-yoiCfKJ8yxrXToh8ud1+/JFqlRexrZmJ0PhofQX3jyfmmyEBQQJFL+2UYewm4FxbG3l7ndBC/NIhu1v5CdwxiQ==}
|
||||||
|
|
||||||
'@kevisual/types@0.0.12':
|
'@kevisual/types@0.0.12':
|
||||||
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
|
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
|
||||||
|
|
||||||
'@next/env@16.1.5':
|
'@kevisual/use-config@1.0.30':
|
||||||
resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==}
|
resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==}
|
||||||
|
peerDependencies:
|
||||||
|
dotenv: ^17
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@16.1.5':
|
'@next/env@16.1.6':
|
||||||
resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==}
|
resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==}
|
||||||
|
|
||||||
|
'@next/swc-darwin-arm64@16.1.6':
|
||||||
|
resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-darwin-x64@16.1.5':
|
'@next/swc-darwin-x64@16.1.6':
|
||||||
resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==}
|
resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@16.1.5':
|
'@next/swc-linux-arm64-gnu@16.1.6':
|
||||||
resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==}
|
resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@16.1.5':
|
'@next/swc-linux-arm64-musl@16.1.6':
|
||||||
resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==}
|
resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@16.1.5':
|
'@next/swc-linux-x64-gnu@16.1.6':
|
||||||
resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==}
|
resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@16.1.5':
|
'@next/swc-linux-x64-musl@16.1.6':
|
||||||
resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==}
|
resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@16.1.5':
|
'@next/swc-win32-arm64-msvc@16.1.6':
|
||||||
resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==}
|
resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@16.1.5':
|
'@next/swc-win32-x64-msvc@16.1.6':
|
||||||
resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==}
|
resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1504,6 +1536,9 @@ packages:
|
|||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
'@types/spark-md5@3.0.5':
|
||||||
|
resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==}
|
||||||
|
|
||||||
antd@6.2.2:
|
antd@6.2.2:
|
||||||
resolution: {integrity: sha512-f5RvWnhjt2gZTpBMW3msHwA3IeaCJBHDwVyEsskYGp0EXcRhhklWrltkybDki0ysBNywkjLPp3wuuWhIKfplcQ==}
|
resolution: {integrity: sha512-f5RvWnhjt2gZTpBMW3msHwA3IeaCJBHDwVyEsskYGp0EXcRhhklWrltkybDki0ysBNywkjLPp3wuuWhIKfplcQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1749,8 +1784,8 @@ packages:
|
|||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
next@16.1.5:
|
next@16.1.6:
|
||||||
resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==}
|
resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1770,6 +1805,9 @@ packages:
|
|||||||
sass:
|
sass:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
path-browserify-esm@1.0.6:
|
||||||
|
resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==}
|
||||||
|
|
||||||
path-parse@1.0.7:
|
path-parse@1.0.7:
|
||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
|
|
||||||
@@ -1845,6 +1883,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
reselect@5.1.1:
|
||||||
|
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||||
|
|
||||||
resolve@1.22.11:
|
resolve@1.22.11:
|
||||||
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1887,6 +1928,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
spark-md5@3.0.2:
|
||||||
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
|
||||||
string-convert@0.2.1:
|
string-convert@0.2.1:
|
||||||
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==}
|
||||||
|
|
||||||
@@ -1910,6 +1954,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
tabbable@6.4.0:
|
||||||
|
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
||||||
|
|
||||||
tailwind-merge@3.4.0:
|
tailwind-merge@3.4.0:
|
||||||
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==}
|
||||||
|
|
||||||
@@ -1961,6 +2008,11 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
valtio@2.3.0:
|
valtio@2.3.0:
|
||||||
resolution: {integrity: sha512-1MfKNcmOIdBSatiJsYgw420n6jnD+jeoI0V+RkOQbCB0ElLh6GKUfPr0hc9uq/KBGeghivDEarRsKFFdSQQnKw==}
|
resolution: {integrity: sha512-1MfKNcmOIdBSatiJsYgw420n6jnD+jeoI0V+RkOQbCB0ElLh6GKUfPr0hc9uq/KBGeghivDEarRsKFFdSQQnKw==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
@@ -1979,8 +2031,8 @@ packages:
|
|||||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
|
||||||
zustand@5.0.10:
|
zustand@5.0.11:
|
||||||
resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
|
resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': '>=18.0.0'
|
'@types/react': '>=18.0.0'
|
||||||
@@ -2061,6 +2113,31 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/runtime@7.28.6': {}
|
'@babel/runtime@7.28.6': {}
|
||||||
|
|
||||||
|
'@base-ui/react@1.1.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.6
|
||||||
|
'@base-ui/utils': 0.2.4(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
'@floating-ui/utils': 0.2.10
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
reselect: 5.1.1
|
||||||
|
tabbable: 6.4.0
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
|
'@base-ui/utils@0.2.4(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.6
|
||||||
|
'@floating-ui/utils': 0.2.10
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
reselect: 5.1.1
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
'@date-fns/tz@1.4.1': {}
|
'@date-fns/tz@1.4.1': {}
|
||||||
|
|
||||||
'@emnapi/runtime@1.8.1':
|
'@emnapi/runtime@1.8.1':
|
||||||
@@ -2205,14 +2282,17 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
'@kevisual/api@0.0.28':
|
'@kevisual/api@0.0.41':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kevisual/js-filter': 0.0.5
|
'@kevisual/js-filter': 0.0.5
|
||||||
'@kevisual/load': 0.0.6
|
'@kevisual/load': 0.0.6
|
||||||
|
'@types/spark-md5': 3.0.5
|
||||||
es-toolkit: 1.44.0
|
es-toolkit: 1.44.0
|
||||||
eventemitter3: 5.0.4
|
eventemitter3: 5.0.4
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
nanoid: 5.1.6
|
nanoid: 5.1.6
|
||||||
|
path-browserify-esm: 1.0.6
|
||||||
|
spark-md5: 3.0.2
|
||||||
|
|
||||||
'@kevisual/cache@0.0.5':
|
'@kevisual/cache@0.0.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2239,11 +2319,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
eventemitter3: 5.0.4
|
eventemitter3: 5.0.4
|
||||||
|
|
||||||
'@kevisual/query@0.0.38':
|
'@kevisual/query@0.0.39':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@kevisual/router@0.0.63(typescript@5.9.3)':
|
'@kevisual/router@0.0.66(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kevisual/dts': 0.0.3(typescript@5.9.3)
|
'@kevisual/dts': 0.0.3(typescript@5.9.3)
|
||||||
hono: 4.11.7
|
hono: 4.11.7
|
||||||
@@ -2252,30 +2332,35 @@ snapshots:
|
|||||||
|
|
||||||
'@kevisual/types@0.0.12': {}
|
'@kevisual/types@0.0.12': {}
|
||||||
|
|
||||||
'@next/env@16.1.5': {}
|
'@kevisual/use-config@1.0.30(dotenv@17.2.3)':
|
||||||
|
dependencies:
|
||||||
|
'@kevisual/load': 0.0.6
|
||||||
|
dotenv: 17.2.3
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@16.1.5':
|
'@next/env@16.1.6': {}
|
||||||
|
|
||||||
|
'@next/swc-darwin-arm64@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-darwin-x64@16.1.5':
|
'@next/swc-darwin-x64@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@16.1.5':
|
'@next/swc-linux-arm64-gnu@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@16.1.5':
|
'@next/swc-linux-arm64-musl@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@16.1.5':
|
'@next/swc-linux-x64-gnu@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@16.1.5':
|
'@next/swc-linux-x64-musl@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@16.1.5':
|
'@next/swc-win32-arm64-msvc@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@16.1.5':
|
'@next/swc-win32-x64-msvc@16.1.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@@ -3277,6 +3362,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
|
'@types/spark-md5@3.0.5': {}
|
||||||
|
|
||||||
antd@6.2.2(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
antd@6.2.2(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/colors': 8.0.1
|
'@ant-design/colors': 8.0.1
|
||||||
@@ -3506,9 +3593,9 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@5.1.6: {}
|
nanoid@5.1.6: {}
|
||||||
|
|
||||||
next@16.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.5
|
'@next/env': 16.1.6
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
baseline-browser-mapping: 2.9.11
|
baseline-browser-mapping: 2.9.11
|
||||||
caniuse-lite: 1.0.30001762
|
caniuse-lite: 1.0.30001762
|
||||||
@@ -3517,19 +3604,21 @@ snapshots:
|
|||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
styled-jsx: 5.1.6(react@19.2.4)
|
styled-jsx: 5.1.6(react@19.2.4)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.1.5
|
'@next/swc-darwin-arm64': 16.1.6
|
||||||
'@next/swc-darwin-x64': 16.1.5
|
'@next/swc-darwin-x64': 16.1.6
|
||||||
'@next/swc-linux-arm64-gnu': 16.1.5
|
'@next/swc-linux-arm64-gnu': 16.1.6
|
||||||
'@next/swc-linux-arm64-musl': 16.1.5
|
'@next/swc-linux-arm64-musl': 16.1.6
|
||||||
'@next/swc-linux-x64-gnu': 16.1.5
|
'@next/swc-linux-x64-gnu': 16.1.6
|
||||||
'@next/swc-linux-x64-musl': 16.1.5
|
'@next/swc-linux-x64-musl': 16.1.6
|
||||||
'@next/swc-win32-arm64-msvc': 16.1.5
|
'@next/swc-win32-arm64-msvc': 16.1.6
|
||||||
'@next/swc-win32-x64-msvc': 16.1.5
|
'@next/swc-win32-x64-msvc': 16.1.6
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
|
path-browserify-esm@1.0.6: {}
|
||||||
|
|
||||||
path-parse@1.0.7: {}
|
path-parse@1.0.7: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
@@ -3597,6 +3686,8 @@ snapshots:
|
|||||||
|
|
||||||
react@19.2.4: {}
|
react@19.2.4: {}
|
||||||
|
|
||||||
|
reselect@5.1.1: {}
|
||||||
|
|
||||||
resolve@1.22.11:
|
resolve@1.22.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-core-module: 2.16.1
|
is-core-module: 2.16.1
|
||||||
@@ -3690,6 +3781,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
spark-md5@3.0.2: {}
|
||||||
|
|
||||||
string-convert@0.2.1: {}
|
string-convert@0.2.1: {}
|
||||||
|
|
||||||
styled-jsx@5.1.6(react@19.2.4):
|
styled-jsx@5.1.6(react@19.2.4):
|
||||||
@@ -3701,6 +3794,8 @@ snapshots:
|
|||||||
|
|
||||||
supports-preserve-symlinks-flag@1.0.0: {}
|
supports-preserve-symlinks-flag@1.0.0: {}
|
||||||
|
|
||||||
|
tabbable@6.4.0: {}
|
||||||
|
|
||||||
tailwind-merge@3.4.0: {}
|
tailwind-merge@3.4.0: {}
|
||||||
|
|
||||||
tailwindcss@4.1.18: {}
|
tailwindcss@4.1.18: {}
|
||||||
@@ -3734,6 +3829,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
|
||||||
valtio@2.3.0(@types/react@19.2.7)(react@19.2.4):
|
valtio@2.3.0(@types/react@19.2.7)(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-compare: 3.0.1
|
proxy-compare: 3.0.1
|
||||||
@@ -3750,7 +3849,8 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- '@types/react-dom'
|
- '@types/react-dom'
|
||||||
|
|
||||||
zustand@5.0.10(@types/react@19.2.7)(react@19.2.4):
|
zustand@5.0.11(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.7
|
'@types/react': 19.2.7
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.4)
|
||||||
|
|||||||
168
src/app/config/components/autocomplate.tsx
Normal file
168
src/app/config/components/autocomplate.tsx
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Check, ChevronsUpDown } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command"
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover"
|
||||||
|
|
||||||
|
export interface AutocomplateOption {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
tags?: readonly string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutocomplateProps {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
options: AutocomplateOption[]
|
||||||
|
placeholder?: string
|
||||||
|
searchPlaceholder?: string
|
||||||
|
emptyMessage?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Autocomplate({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
placeholder = "选择项目...",
|
||||||
|
searchPlaceholder = "搜索...",
|
||||||
|
emptyMessage = "未找到结果",
|
||||||
|
className,
|
||||||
|
}: AutocomplateProps) {
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
const [inputValue, setInputValue] = React.useState(value)
|
||||||
|
const [searchValue, setSearchValue] = React.useState("")
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setInputValue(value)
|
||||||
|
}, [value])
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value
|
||||||
|
setInputValue(newValue)
|
||||||
|
onChange(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
setInputValue(selectedValue)
|
||||||
|
onChange(selectedValue)
|
||||||
|
setSearchValue("")
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter' && searchValue) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSelect(searchValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机获取badge variant
|
||||||
|
const getRandomVariant = (index: number) => {
|
||||||
|
const variants = ['default', 'secondary', 'outline'] as const
|
||||||
|
return variants[index % variants.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={inputValue}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className={cn("flex-1 font-mono", className)}
|
||||||
|
/>
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className="w-10 px-0 shrink-0"
|
||||||
|
>
|
||||||
|
<ChevronsUpDown className="h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-125 p-0" align="end">
|
||||||
|
<Command className="**:[[cmdk-input]]:font-mono">
|
||||||
|
<CommandInput
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
value={searchValue}
|
||||||
|
onValueChange={setSearchValue}
|
||||||
|
onKeyDown={handleSearchKeyDown}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
<div className="text-muted-foreground mb-2">{emptyMessage}</div>
|
||||||
|
{searchValue && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleSelect(searchValue)}
|
||||||
|
className="text-primary hover:underline text-sm"
|
||||||
|
>
|
||||||
|
使用 "{searchValue}"
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((option) => (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
keywords={[option.value, option.label, option.description || '', ...(option.tags || [])]}
|
||||||
|
onSelect={() => handleSelect(option.value)}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
inputValue === option.value ? "opacity-100" : "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col flex-1">
|
||||||
|
<span className="font-medium font-mono">{option.label}</span>
|
||||||
|
{option.description && (
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{option.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{option.tags && option.tags.length > 0 && (
|
||||||
|
<div className="flex gap-1 flex-wrap mt-1">
|
||||||
|
{option.tags.map((tag, index) => (
|
||||||
|
<Badge
|
||||||
|
key={tag}
|
||||||
|
variant={getRandomVariant(index)}
|
||||||
|
className="text-[10px] px-1.5 py-0 h-4"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
324
src/app/config/env/page.tsx
vendored
Normal file
324
src/app/config/env/page.tsx
vendored
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { configEnvList } from '@kevisual/use-config/env-config.ts';
|
||||||
|
import { useConfigStore } from '../store/config';
|
||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card';
|
||||||
|
import { Trash2, Plus, Save, Upload, Download } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Autocomplate, type AutocomplateOption } from '@/app/config/components/autocomplate';
|
||||||
|
|
||||||
|
type EnvItem = {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function EnvPage() {
|
||||||
|
const { getEnv, updateEnv, envData } = useConfigStore();
|
||||||
|
const [envItems, setEnvItems] = useState<EnvItem[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadEnvData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (envData && typeof envData === 'object') {
|
||||||
|
const data = envData?.data || {};
|
||||||
|
const items = Object.entries(data).map(([key, value], index) => ({
|
||||||
|
key,
|
||||||
|
value: String(value || ''),
|
||||||
|
id: `env-${index}-${Date.now()}`,
|
||||||
|
}));
|
||||||
|
setEnvItems(items);
|
||||||
|
}
|
||||||
|
}, [envData]);
|
||||||
|
|
||||||
|
const loadEnvData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await getEnv();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEnvItem = () => {
|
||||||
|
const newItem: EnvItem = {
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
id: `env-new-${Date.now()}`,
|
||||||
|
};
|
||||||
|
setEnvItems([...envItems, newItem]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEnvItem = (id: string) => {
|
||||||
|
setEnvItems(envItems.filter((item) => item.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateEnvItem = (id: string, field: 'key' | 'value', newValue: string) => {
|
||||||
|
setEnvItems(
|
||||||
|
envItems.map((item) => (item.id === id ? { ...item, [field]: newValue } : item))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// 验证是否有空的 key
|
||||||
|
const hasEmptyKey = envItems.some((item) => !item.key.trim());
|
||||||
|
if (hasEmptyKey) {
|
||||||
|
toast.error('请填写所有环境变量的键名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证是否有重复的 key
|
||||||
|
const keys = envItems.map((item) => item.key);
|
||||||
|
const uniqueKeys = new Set(keys);
|
||||||
|
if (keys.length !== uniqueKeys.size) {
|
||||||
|
toast.error('存在重复的环境变量键名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为对象
|
||||||
|
const envObject = envItems.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
if (item.key.trim()) {
|
||||||
|
acc[item.key] = item.value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await updateEnv({ ...envData, data: envObject });
|
||||||
|
await loadEnvData();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnvDescription = (key: string): string => {
|
||||||
|
const config = configEnvList.find((item) => item.title === key);
|
||||||
|
return config?.description || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnvTags = (key: string): readonly string[] => {
|
||||||
|
const config = configEnvList.find((item) => item.title === key);
|
||||||
|
return config?.tags || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
// 只导出第一级的 key-value
|
||||||
|
const envObject = envItems.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
if (item.key.trim()) {
|
||||||
|
acc[item.key] = item.value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = JSON.stringify(envObject, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'env-config.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
toast.success('导出成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'application/json';
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(event.target?.result as string);
|
||||||
|
|
||||||
|
// 只读取第一级的 key-value
|
||||||
|
const items: EnvItem[] = [];
|
||||||
|
Object.entries(json).forEach(([key, value]) => {
|
||||||
|
// 只处理第一级,忽略嵌套对象
|
||||||
|
if (typeof value !== 'object' || value === null) {
|
||||||
|
items.push({
|
||||||
|
key,
|
||||||
|
value: String(value || ''),
|
||||||
|
id: `env-${items.length}-${Date.now()}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果是对象,只取第一级的值
|
||||||
|
items.push({
|
||||||
|
key,
|
||||||
|
value: JSON.stringify(value),
|
||||||
|
id: `env-${items.length}-${Date.now()}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setEnvItems(items);
|
||||||
|
toast.success('导入成功');
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('导入失败,请检查文件格式');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const comboboxOptions = useMemo<AutocomplateOption[]>(() => {
|
||||||
|
return configEnvList.map((config) => ({
|
||||||
|
value: config.title,
|
||||||
|
label: config.title,
|
||||||
|
description: config.description,
|
||||||
|
tags: config.tags,
|
||||||
|
})).sort((a, b) => {
|
||||||
|
// 优先排序:有"常用"标签的排在前面
|
||||||
|
const aHasCommon = a.tags?.some(tag => tag === '常用') ?? false;
|
||||||
|
const bHasCommon = b.tags?.some(tag => tag === '常用') ?? false;
|
||||||
|
|
||||||
|
if (aHasCommon && !bHasCommon) return -1;
|
||||||
|
if (!aHasCommon && bHasCommon) return 1;
|
||||||
|
|
||||||
|
// 其次按字母顺序排序
|
||||||
|
return a.label.localeCompare(b.label);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading && envItems.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
<div className="text-lg">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col">
|
||||||
|
<div className="flex-none border-b bg-background">
|
||||||
|
<div className="container mx-auto p-6 max-w-5xl">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">环境变量配置</h1>
|
||||||
|
<p className="text-muted-foreground mt-2">管理应用的环境变量配置</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button onClick={handleImport} variant="outline">
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
导入
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleExport} variant="outline">
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
<Button onClick={addEnvItem} variant="outline">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加变量
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={isLoading}>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
{isLoading ? '保存中...' : '保存配置'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="container mx-auto p-6 max-w-5xl">
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{envItems.map((item) => (
|
||||||
|
<Card key={item.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start gap-4">
|
||||||
|
<div className="flex-1 space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`key-${item.id}`}>环境变量键</Label>
|
||||||
|
<Autocomplate
|
||||||
|
value={item.key}
|
||||||
|
onChange={(value) => updateEnvItem(item.id, 'key', value)}
|
||||||
|
options={comboboxOptions}
|
||||||
|
placeholder="输入或选择环境变量键"
|
||||||
|
searchPlaceholder="搜索环境变量..."
|
||||||
|
emptyMessage="未找到匹配的环境变量"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.key && getEnvDescription(item.key) && (
|
||||||
|
<CardDescription>{getEnvDescription(item.key)}</CardDescription>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item.key && getEnvTags(item.key).length > 0 && (
|
||||||
|
<div className="flex gap-2 flex-wrap">
|
||||||
|
{getEnvTags(item.key).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-xs px-2 py-1 rounded-full bg-secondary text-secondary-foreground"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`value-${item.id}`}>环境变量值</Label>
|
||||||
|
<Input
|
||||||
|
id={`value-${item.id}`}
|
||||||
|
value={item.value}
|
||||||
|
onChange={(e) => updateEnvItem(item.id, 'value', e.target.value)}
|
||||||
|
placeholder="输入环境变量的值"
|
||||||
|
type={
|
||||||
|
item.key.toLowerCase().includes('password') ||
|
||||||
|
item.key.toLowerCase().includes('secret') ||
|
||||||
|
item.key.toLowerCase().includes('key')
|
||||||
|
? 'password'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeEnvItem(item.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{envItems.length === 0 && (
|
||||||
|
<Card className="p-12">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<p className="text-lg mb-4">暂无环境变量</p>
|
||||||
|
<Button onClick={addEnvItem} variant="outline">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加第一个环境变量
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { query } from '@/modules/query';
|
import { query } from '@/modules/query';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { QueryConfig } from '@kevisual/api/config';
|
import { QueryConfig, Config } from '@kevisual/api/config';
|
||||||
|
|
||||||
export const queryConfig = new QueryConfig({ query: query as any });
|
export const queryConfig = new QueryConfig({ query: query as any });
|
||||||
|
|
||||||
@@ -16,6 +16,10 @@ interface ConfigStore {
|
|||||||
deleteConfig: (id: string) => Promise<void>;
|
deleteConfig: (id: string) => Promise<void>;
|
||||||
detectConfig: () => Promise<void>;
|
detectConfig: () => Promise<void>;
|
||||||
onOpenKey: (key: string) => Promise<void>;
|
onOpenKey: (key: string) => Promise<void>;
|
||||||
|
getEnv: () => Promise<void>;
|
||||||
|
updateEnv: (data: Config) => Promise<void>;
|
||||||
|
envData: Config;
|
||||||
|
setEnvData: (envData: Config) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useConfigStore = create<ConfigStore>((set, get) => ({
|
export const useConfigStore = create<ConfigStore>((set, get) => ({
|
||||||
@@ -75,4 +79,26 @@ export const useConfigStore = create<ConfigStore>((set, get) => ({
|
|||||||
toast.error('获取配置失败');
|
toast.error('获取配置失败');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
getEnv: async () => {
|
||||||
|
const res = await queryConfig.getByKey('env.json');
|
||||||
|
if (res.code === 200) {
|
||||||
|
const data = res.data;
|
||||||
|
console.log(data);
|
||||||
|
set({ envData: data });
|
||||||
|
} else {
|
||||||
|
console.log(res);
|
||||||
|
toast.error('获取失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateEnv: async (data: any) => {
|
||||||
|
const res = await queryConfig.updateConfig({ key: 'env.json', ...data });
|
||||||
|
if (res.code === 200) {
|
||||||
|
toast.success('更新成功');
|
||||||
|
} else {
|
||||||
|
console.log(res);
|
||||||
|
toast.error('更新失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
envData: {},
|
||||||
|
setEnvData: (envData: any) => set({ envData }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
292
src/app/user/page.tsx
Normal file
292
src/app/user/page.tsx
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
'use client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useUserStore } from './store';
|
||||||
|
import { useLayoutStore } from '@/modules/layout/store';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card';
|
||||||
|
import { LayoutMain } from '@/modules/layout';
|
||||||
|
import { Pencil, Key, User } from 'lucide-react';
|
||||||
|
import PandaPNG from '@/assets/panda.jpg';
|
||||||
|
|
||||||
|
const ProfileCard = () => {
|
||||||
|
const { me, getMe } = useLayoutStore();
|
||||||
|
const { setShowEdit, setShowChangePassword } = useUserStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMe();
|
||||||
|
}, [getMe]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="max-w-2xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="w-5 h-5" />
|
||||||
|
个人信息
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>管理您的个人资料和账户设置</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Avatar */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-20 h-20 rounded-full overflow-hidden border-2 border-gray-200">
|
||||||
|
{me?.avatar ? (
|
||||||
|
<img
|
||||||
|
src={me.avatar}
|
||||||
|
alt="avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={PandaPNG.src}
|
||||||
|
alt="avatar"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">{me?.username || '-'}</h3>
|
||||||
|
<p className="text-sm text-gray-500">{me?.description || '暂无描述'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* User Info Fields */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||||
|
<Label>用户名</Label>
|
||||||
|
<div className="text-gray-700">{me?.username || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||||
|
<Label>昵称</Label>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-gray-700">{me?.nickname || '-'}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowEdit(true)}>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[120px_1fr] items-start gap-4">
|
||||||
|
<Label className="mt-2">个人描述</Label>
|
||||||
|
<div className="flex items-center justify-between flex-1">
|
||||||
|
<p className="text-gray-700 text-sm">{me?.description || '暂无描述'}</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowEdit(true)}>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||||
|
<Label>用户ID</Label>
|
||||||
|
<div className="text-gray-500 text-sm">{me?.id || '-'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[120px_1fr] items-center gap-4">
|
||||||
|
<Label>密码</Label>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowChangePassword(true)}>
|
||||||
|
<Key className="w-4 h-4 mr-1" />
|
||||||
|
修改密码
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edit Profile Button */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={() => setShowEdit(true)}>
|
||||||
|
<Pencil className="w-4 h-4 mr-1" />
|
||||||
|
编辑资料
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditProfileModal = () => {
|
||||||
|
const { showEdit, setShowEdit, setFormData, updateSelf, loading } = useUserStore();
|
||||||
|
const { me, getMe } = useLayoutStore();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showEdit) {
|
||||||
|
reset({
|
||||||
|
nickname: me?.nickname || '',
|
||||||
|
description: me?.description || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [me, showEdit, reset]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
const res = await updateSelf(data);
|
||||||
|
if (res) {
|
||||||
|
setShowEdit(false);
|
||||||
|
setFormData({});
|
||||||
|
await getMe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showEdit} onOpenChange={(open) => {
|
||||||
|
setShowEdit(open);
|
||||||
|
if (!open) setFormData({});
|
||||||
|
}}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>编辑个人资料</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>昵称</Label>
|
||||||
|
<Input
|
||||||
|
{...register('nickname')}
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>个人描述</Label>
|
||||||
|
<Textarea
|
||||||
|
{...register('description')}
|
||||||
|
placeholder="请输入个人描述"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowEdit(false)}
|
||||||
|
disabled={loading}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading ? '保存中...' : '保存'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangePasswordModal = () => {
|
||||||
|
const { showChangePassword, setShowChangePassword, loading } = useUserStore();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
const { updateSelf } = useUserStore.getState();
|
||||||
|
const res = await updateSelf({
|
||||||
|
password: data.newPassword,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
setShowChangePassword(false);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={showChangePassword} onOpenChange={setShowChangePassword}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>修改密码</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="p-4">
|
||||||
|
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>新密码</Label>
|
||||||
|
<Input
|
||||||
|
{...register('newPassword', {
|
||||||
|
required: '请输入新密码',
|
||||||
|
minLength: { value: 6, message: '密码长度至少6位' },
|
||||||
|
})}
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入新密码"
|
||||||
|
className={errors.newPassword ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{errors.newPassword && (
|
||||||
|
<span className="text-xs text-red-500">{errors.newPassword.message as string}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label>确认新密码</Label>
|
||||||
|
<Input
|
||||||
|
{...register('confirmPassword', {
|
||||||
|
required: '请确认新密码',
|
||||||
|
validate: (value, formValues) =>
|
||||||
|
value === formValues.newPassword || '两次输入的密码不一致',
|
||||||
|
})}
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入新密码"
|
||||||
|
className={errors.confirmPassword ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<span className="text-xs text-red-500">{errors.confirmPassword.message as string}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowChangePassword(false)}
|
||||||
|
disabled={loading}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={loading}>
|
||||||
|
{loading ? '保存中...' : '确认修改'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProfile = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-6 w-full h-full overflow-auto">
|
||||||
|
<ProfileCard />
|
||||||
|
<EditProfileModal />
|
||||||
|
<ChangePasswordModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return <LayoutMain title="个人信息"><UserProfile /></LayoutMain>;
|
||||||
|
}
|
||||||
57
src/app/user/store/index.ts
Normal file
57
src/app/user/store/index.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { query, queryLogin } from '@/modules/query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type UserInfo = {
|
||||||
|
avatar: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
needChangePassword: boolean;
|
||||||
|
nickname: string | null;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
type UserStore = {
|
||||||
|
showEdit: boolean;
|
||||||
|
setShowEdit: (showEdit: boolean) => void;
|
||||||
|
showNameEdit: boolean;
|
||||||
|
setShowNameEdit: (showNameEdit: boolean) => void;
|
||||||
|
showCheckUserExist: boolean;
|
||||||
|
setShowCheckUserExist: (showCheckUserExist: boolean) => void;
|
||||||
|
formData: any;
|
||||||
|
setFormData: (formData: any) => void;
|
||||||
|
loading: boolean;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
updateSelf: (data: any) => Promise<any>;
|
||||||
|
showChangePassword: boolean;
|
||||||
|
setShowChangePassword: (showChangePassword: boolean) => void;
|
||||||
|
};
|
||||||
|
export const useUserStore = create<UserStore>((set, get) => {
|
||||||
|
return {
|
||||||
|
showEdit: false,
|
||||||
|
setShowEdit: (showEdit) => set({ showEdit }),
|
||||||
|
showNameEdit: false,
|
||||||
|
setShowNameEdit: (showNameEdit) => set({ showNameEdit }),
|
||||||
|
showCheckUserExist: false,
|
||||||
|
setShowCheckUserExist: (showCheckUserExist) => set({ showCheckUserExist }),
|
||||||
|
formData: {},
|
||||||
|
setFormData: (formData) => set({ formData }),
|
||||||
|
loading: false,
|
||||||
|
setLoading: (loading) => set({ loading }),
|
||||||
|
updateSelf: async (data) => {
|
||||||
|
const res = await query.post({
|
||||||
|
path: 'user',
|
||||||
|
key: 'updateSelf',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
toast.success('Success');
|
||||||
|
set({ formData: res.data });
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Request failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showChangePassword: false,
|
||||||
|
setShowChangePassword: (showChangePassword) => set({ showChangePassword }),
|
||||||
|
};
|
||||||
|
});
|
||||||
90
src/app/user/store/login.ts
Normal file
90
src/app/user/store/login.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { query, queryLogin } from '@/modules/query';
|
||||||
|
import { basename } from '@/modules/basename';
|
||||||
|
import { toast as message } from 'sonner';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
// 如果自己是在iframe中登录,需要调用这个方法
|
||||||
|
export const postLoginInIframe = (token: string) => {
|
||||||
|
console.log('window.parent !== window', window.parent !== window);
|
||||||
|
if (window.parent === window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 获取父窗口的来源
|
||||||
|
const parentOrigin = window.location.ancestorOrigins ? window.location.ancestorOrigins[0] : document.referrer;
|
||||||
|
|
||||||
|
// 检查父窗口的来源是否合法
|
||||||
|
const allowedOrigins = ['http://localhost', /^https?:\/\/(.+\.)?on-ai\.ai$/, /^https?:\/\/(.+\.)?xiongxiao\.me$/];
|
||||||
|
|
||||||
|
let targetOrigin: string | null = null;
|
||||||
|
|
||||||
|
// 根据来源动态选择 targetOrigin
|
||||||
|
if (allowedOrigins.some((origin) => (typeof origin === 'string' ? parentOrigin.includes(origin) : origin.test(parentOrigin)))) {
|
||||||
|
targetOrigin = parentOrigin; // 使用合法来源作为 targetOrigin
|
||||||
|
}
|
||||||
|
// 如果找到合法的 targetOrigin,则发送消息
|
||||||
|
if (targetOrigin) {
|
||||||
|
const message = { type: 'login-from-iframe', data: { token } };
|
||||||
|
parent.postMessage(message, targetOrigin);
|
||||||
|
} else {
|
||||||
|
console.warn('Parent origin is not allowed:', parentOrigin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginStore = {
|
||||||
|
loading: boolean;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
formData: any;
|
||||||
|
setFormData: (formData: any) => void;
|
||||||
|
login: () => Promise<void>;
|
||||||
|
register: () => Promise<void>;
|
||||||
|
isLogin: boolean;
|
||||||
|
setIsLogin: (isLogin: boolean) => void;
|
||||||
|
};
|
||||||
|
export const useLoginStore = create<LoginStore>((set, get) => {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
setLoading: (loading) => set({ loading }),
|
||||||
|
formData: {},
|
||||||
|
setFormData: (formData) => set({ formData }),
|
||||||
|
login: async () => {
|
||||||
|
const { formData } = get();
|
||||||
|
const { username, password } = formData;
|
||||||
|
if (!username || !password) {
|
||||||
|
message.error('Please input username and password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set({ loading: true });
|
||||||
|
const res = await queryLogin.login({ username, password });
|
||||||
|
if (res.code === 200) {
|
||||||
|
message.success('Success');
|
||||||
|
set({ isLogin: true });
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
if (window.parent !== window) {
|
||||||
|
postLoginInIframe(res.data?.accessToken || '');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
}
|
||||||
|
const search = new URLSearchParams(window.location.search);
|
||||||
|
const redirect = search.get('redirect');
|
||||||
|
if (redirect) {
|
||||||
|
window.location.href = redirect;
|
||||||
|
} else {
|
||||||
|
window.location.href = basename ? basename + '/' : '/';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.message || 'Request failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
register: async () => {
|
||||||
|
set({ loading: true });
|
||||||
|
const res = await query.post({ path: 'user', key: 'register' });
|
||||||
|
if (res.code === 200) {
|
||||||
|
message.success('Success');
|
||||||
|
// 跳到某一个页面
|
||||||
|
} else {
|
||||||
|
message.error(res.message || 'Request failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isLogin: false,
|
||||||
|
setIsLogin: (isLogin) => set({ isLogin }),
|
||||||
|
};
|
||||||
|
});
|
||||||
48
src/components/ui/badge.tsx
Normal file
48
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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 border-transparent 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: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"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:
|
||||||
|
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
310
src/components/ui/combobox.tsx
Normal file
310
src/components/ui/combobox.tsx
Normal 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,
|
||||||
|
}
|
||||||
170
src/components/ui/input-group.tsx
Normal file
170
src/components/ui/input-group.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
@@ -47,9 +47,10 @@ export const usePlatformStore = create<PlatfromStore>((set) => {
|
|||||||
type Me = {
|
type Me = {
|
||||||
id?: string;
|
id?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
nickname?: string | null;
|
||||||
needChangePassword?: boolean;
|
needChangePassword?: boolean;
|
||||||
role?: string;
|
role?: string;
|
||||||
description?: string;
|
description?: string | null;
|
||||||
type?: 'user' | 'org';
|
type?: 'user' | 'org';
|
||||||
orgs?: string[];
|
orgs?: string[];
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user