Compare commits
14 Commits
develop-gr
...
c861a3ba7a
| Author | SHA1 | Date | |
|---|---|---|---|
| c861a3ba7a | |||
| 476f40a529 | |||
| dbc8b1a82a | |||
| ab9b4f229d | |||
| b435638774 | |||
| 8515498ade | |||
| 43702e8bf7 | |||
| cc3f62a6a1 | |||
| 15b69773f0 | |||
| a80fa755d4 | |||
| 6a1d8b1aee | |||
| 1f8fdad2da | |||
| 5350f421f9 | |||
| 698abcec22 |
79
.deploy/stream.ui-production.yaml
Normal file
79
.deploy/stream.ui-production.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: stream.ui-config
|
||||
namespace: stream-production
|
||||
labels:
|
||||
app: stream.ui
|
||||
data:
|
||||
STREAM_API_GRPC_ADDR: "stream.api-svc:9000"
|
||||
GOOGLE_AUTH_FINALIZE_PATH: "/auth/google/finalize"
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: stream.ui-svc
|
||||
namespace: stream-production
|
||||
labels:
|
||||
app: stream.ui
|
||||
spec:
|
||||
selector:
|
||||
app: stream.ui
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
type: NodePort
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: stream.ui-dep
|
||||
namespace: stream-production
|
||||
labels:
|
||||
app: stream.ui
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: stream.ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: stream.ui
|
||||
spec:
|
||||
imagePullSecrets:
|
||||
- name: registry-production-secret
|
||||
containers:
|
||||
- name: stream.ui
|
||||
image: registry.awing.vn/stream-production/stream.ui:$BUILD_NUMBER
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: STREAM_API_GRPC_ADDR
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: stream.ui-config
|
||||
key: STREAM_API_GRPC_ADDR
|
||||
- name: GOOGLE_AUTH_FINALIZE_PATH
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: stream.ui-config
|
||||
key: GOOGLE_AUTH_FINALIZE_PATH
|
||||
- name: STREAM_INTERNAL_AUTH_MARKER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stream.ui-secret
|
||||
key: STREAM_INTERNAL_AUTH_MARKER
|
||||
- name: STREAM_UI_JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stream.ui-secret
|
||||
key: STREAM_UI_JWT_SECRET
|
||||
- name: STREAM_UI_REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: stream.ui-secret
|
||||
key: STREAM_UI_REDIS_URL
|
||||
|
||||
19
buf.gen.yaml
19
buf.gen.yaml
@@ -1,19 +0,0 @@
|
||||
version: v2
|
||||
plugins:
|
||||
# - remote: buf.build/protocolbuffers/go
|
||||
# out: internal/gen/proto
|
||||
# opt:
|
||||
# - paths=source_relative
|
||||
# - remote: buf.build/grpc/go
|
||||
# out: internal/gen/proto
|
||||
# opt:
|
||||
# - paths=source_relative
|
||||
- remote: buf.build/community/stephenh-ts-proto
|
||||
out: ./src/server/utils/proto
|
||||
opt:
|
||||
- env=node
|
||||
- esModuleInterop=true
|
||||
- outputServices=grpc-js
|
||||
- useOptionals=all
|
||||
- forceLong=number
|
||||
- useDate=string
|
||||
9
buf.yaml
9
buf.yaml
@@ -1,9 +0,0 @@
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
323
bun.lock
323
bun.lock
@@ -10,36 +10,37 @@
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hiogawa/utils": "^1.7.0",
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"@hono/node-server": "^1.19.12",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@pinia/colada": "^1.0.0",
|
||||
"@pinia/colada": "^1.1.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.1.12",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"hono": "^4.12.7",
|
||||
"i18next": "^25.8.18",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"hono": "^4.12.9",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-http-backend": "^3.0.4",
|
||||
"i18next-vue": "^5.4.0",
|
||||
"is-mobile": "^5.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"superjson": "^2.2.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.10",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||
"estree-walker": "2.0.2",
|
||||
"unocss": "^66.6.6",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"estree-walker": "3.0.3",
|
||||
"unocss": "^66.6.7",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^8.0.0-beta.16",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.3",
|
||||
"vite-ssr-components": "^0.5.2",
|
||||
},
|
||||
},
|
||||
@@ -83,9 +84,9 @@
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
|
||||
|
||||
@@ -93,7 +94,7 @@
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
|
||||
@@ -103,63 +104,11 @@
|
||||
|
||||
"@bufbuild/protobuf": ["@bufbuild/protobuf@2.11.0", "", {}, "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
|
||||
|
||||
@@ -179,7 +128,7 @@
|
||||
|
||||
"@hiogawa/utils": ["@hiogawa/utils@1.7.0", "", {}, "sha512-ghiEFWBR1NENoHn+lSuW7liicTIzVPN+8Srm5UedCTw43gus0mlse6Wp2lz6GmbOXJ/CalMPp/0Tz2X8tajkAg=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
"@hono/node-server": ["@hono/node-server@1.19.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
|
||||
|
||||
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
|
||||
|
||||
@@ -243,11 +192,9 @@
|
||||
|
||||
"@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.115.0", "", { "os": "win32", "cpu": "x64" }, "sha512-oxUl82N+fIO9jIaXPph8SPPHQXrA08BHokBBJW8ct9F/x6o6bZE6eUAhUtWajbtvFhL8UYcCWRMba+kww6MBlA=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||
|
||||
"@pinia/colada": ["@pinia/colada@1.0.0", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-YKSybA6wusFK4CAUPzItoSgPCfScVnnnO2MSlmaaisE/L7luE77GxFyhTzipM8IbvbXh4zkCy97OE7w9WX34wA=="],
|
||||
"@pinia/colada": ["@pinia/colada@1.1.0", "", { "peerDependencies": { "pinia": "^2.2.6 || ^3.0.0", "vue": "^3.5.17" } }, "sha512-GoTOlaDuQuF+9Lj5MbY2oOOZeJSP/I0U9/+Ugo5nfCymlWSyjFFjE7QcHageNL7tQr+tfepb13ks8+qO6uSV/A=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
@@ -273,31 +220,35 @@
|
||||
|
||||
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.6", "", { "os": "android", "cpu": "arm64" }, "sha512-kvjTSWGcrv+BaR2vge57rsKiYdVR8V8CoS0vgKrc570qRBfty4bT+1X0z3j2TaVV+kAYzA0PjeB9+mdZyqUZlg=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+tJhD21KvGNtUrpLXrZQlT+j5HZKiEwR2qtcZb3vNOUpvoT9QjEykr75ZW/Kr0W89gose/HVXU6351uVZD8Qvw=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-DKNhjMk38FAWaHwUt1dFR3rA/qRAvn2NUvSG2UGvxvlMxSmN/qqww/j4ABAbXhNRXtGQNmrAINMXRuwHl16ZHg=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-8TThsRkCPAnfyMBShxrGdtoOE6h36QepqRQI97iFaQSCRbHFWHcDHppcojZnzXoruuhPnjMEygzaykvPVJsMRg=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm" }, "sha512-ZfmFoOwPUZCWtGOVC9/qbQzfc0249FrRUOzV2XabSMUV60Crp211OWLQN1zmQAsRIVWRcEwhJ46Z1mXGo/L/nQ=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZsGzbNETxPodGlLTYHaCSGVhNN/rvkMDCJYHdT7PZr5jFJRmBfmDi2awhF64Dt2vxrJqY6VeeYSgOzEbHRsb7Q=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-elPpdevtCdUOqziemR86C4CSCr/5sUxalzDrf/CJdMT+kZt2C556as++qHikNOz0vuFf52h+GJNXZM08eWgGPQ=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.6", "", { "os": "linux", "cpu": "x64" }, "sha512-IBwXsf56o3xhzAyaZxdM1CX8UFiBEUFCjiVUgny67Q8vPIqkjzJj0YKhd3TbBHanuxThgBa59f6Pgutg2OGk5A=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.6", "", { "os": "linux", "cpu": "x64" }, "sha512-vOk7G8V9Zm+8a6PL6JTpCea61q491oYlGtO6CvnsbhNLlKdf0bbCPytFzGQhYmCKZDKkEbmnkcIprTEGCURnwg=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.6", "", { "os": "none", "cpu": "arm64" }, "sha512-ASjEDI4MRv7XCQb2JVaBzfEYO98JKCGrAgoW6M03fJzH/ilCnC43Mb3ptB9q/lzsaahoJyIBoAGKAYEjUvpyvQ=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-mYa1+h2l6Zc0LvmwUh0oXKKYihnw/1WC73vTqw+IgtfEtv47A+rWzzcWwVDkW73+UDr0d/Ie/HRXoaOY22pQDw=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-e2ABskbNH3MRUBMjgxaMjYIw11DSwjLJxBII3UgpF6WClGLIh8A20kamc+FKH5vIaFVnYQInmcLYSUVpqMPLow=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.6", "", { "os": "win32", "cpu": "x64" }, "sha512-dJVc3ifhaRXxIEh1xowLohzFrlQXkJ66LepHm+CmSprTWgVrPa8Fx3OL57xwIqDEH9hufcKkDX2v65rS3NZyRA=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
|
||||
|
||||
@@ -307,61 +258,61 @@
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@unhead/vue": ["@unhead/vue@2.1.12", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.12" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-zEWqg0nZM8acpuTZE40wkeUl8AhIe0tU0OkilVi1D4fmVjACrwoh5HP6aNqJ8kUnKsoy6D+R3Vi/O+fmdNGO7g=="],
|
||||
|
||||
"@unocss/cli": ["@unocss/cli@66.6.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.6", "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6", "@unocss/preset-wind4": "66.6.6", "@unocss/transformer-directives": "66.6.6", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-78SY8j4hAVelK+vP/adsDGaSjEITasYLFECJLHWxUJSzK+G9UIc5wtL/u4jA+zKvwVkHcDvbkcO5K6wwwpAixg=="],
|
||||
"@unocss/cli": ["@unocss/cli@66.6.7", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.7", "@unocss/core": "66.6.7", "@unocss/preset-wind3": "66.6.7", "@unocss/preset-wind4": "66.6.7", "@unocss/transformer-directives": "66.6.7", "cac": "^6.7.14", "chokidar": "^5.0.0", "colorette": "^2.0.20", "consola": "^3.4.2", "magic-string": "^0.30.21", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "bin": { "unocss": "bin/unocss.mjs" } }, "sha512-m/yW5HMVyxfAOeyO4OyA4JB9dY+/gTsk25ucI8xVCFVDEENPEGr+vEqTDOA+vfe6pdURtyDYS7OrhikIRU1WNA=="],
|
||||
|
||||
"@unocss/config": ["@unocss/config@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-menlnkqAFX/4wR2aandY8hSqrt01JE+rOzvtQxWaBt8kf1du62b0sS72FE5Z40n6HlEsEbF91N9FCfhnzG6i6g=="],
|
||||
"@unocss/config": ["@unocss/config@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "colorette": "^2.0.20", "consola": "^3.4.2", "unconfig": "^7.5.0" } }, "sha512-1uleyRLyJc6PNNc2L3hEaKL89zXwvQAtP36oFySgL47RAxZHPZ4vfqFpbwR0eEN4iSqTS24ZFr7CTRWCaEGjzQ=="],
|
||||
|
||||
"@unocss/core": ["@unocss/core@66.6.6", "", {}, "sha512-Sbbx0ZQqmV8K2lg8E+z9MJzWb1MgRtJnvqzxDIrNuBjXasKhbcFt5wEMBtEZJOr63Z4ck0xThhZK53HmYT2jmg=="],
|
||||
"@unocss/core": ["@unocss/core@66.6.7", "", {}, "sha512-Q8456iWFtdwrUNYKVOQY8ygRggjZOVtLc6Jc8KIkxig7OiNlUWOgXJTfCh4I8g6jBYzC5eHaHFDLgJOmOrxBsg=="],
|
||||
|
||||
"@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-uMzekF2miZRUwSZGvy3yYQiBAcSAs9LiXK8e3NjldxEw8xcRDWgTErxgStRoBeAD6UyzDcg/Cvwtf2guMbtR+g=="],
|
||||
"@unocss/extractor-arbitrary-variants": ["@unocss/extractor-arbitrary-variants@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7" } }, "sha512-PQiBHK0yUJ0BR+3GYnTPU6va6HVSRPV+O+s1zZmt23TWbyIeucoKCNR47TDtv+Z1xuksY8krIjtDYtufdrVWKw=="],
|
||||
|
||||
"@unocss/inspector": ["@unocss/inspector@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-CpXIsqHwxCXJtUjUz6S29diHCIA+EJ1u5WML/6m2YPI4ObgWAVKrExy09inSg2icS52lFkWWdWQSeqc9kl5W6Q=="],
|
||||
"@unocss/inspector": ["@unocss/inspector@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/rule-utils": "66.6.7", "colorette": "^2.0.20", "gzip-size": "^6.0.0", "sirv": "^3.0.2" } }, "sha512-4lA70A/wy9dfSDm7rJ5Uq5fKz+/Szm2rUcHjdbLCVNEc6vv2YXeI7aFvP5qDjTp4ClBSF2AMPnF1mtoMQOfDvA=="],
|
||||
|
||||
"@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-3H12UI1rBt60PQy+S4IEeFYWu1/WQFuc2yhJ5mu/RCvX5/qwlIGanBpuh+xzTPXU1fWBlZN68yyO9uWOQgTqZQ=="],
|
||||
"@unocss/preset-attributify": ["@unocss/preset-attributify@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7" } }, "sha512-thtoLQb53+Acy2QJYT6n+YhgNJ5ilhS8k9bqi+UzflbsuK4TJqOuQQjC9fRkULP5QjtNxgqN3d5Up7ms8tBPDA=="],
|
||||
|
||||
"@unocss/preset-icons": ["@unocss/preset-icons@66.6.6", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.6", "ofetch": "^1.5.1" } }, "sha512-HfIEEqf3jyKexOB2Sux556n0NkPoUftb2H4+Cf7prJvKHopMkZ/OUkXjwvUlxt1e5UpAEaIa0A2Ir7+ApxXoGA=="],
|
||||
"@unocss/preset-icons": ["@unocss/preset-icons@66.6.7", "", { "dependencies": { "@iconify/utils": "^3.1.0", "@unocss/core": "66.6.7", "ofetch": "^1.5.1" } }, "sha512-mGAOyI/qz1pZUV1BcOtWAMm5czdFCjhFCYcDk0KY+Jw37pKRVSQRFeh4gpHuYKmehGv36caLyVrWXpTAwRBdFQ=="],
|
||||
|
||||
"@unocss/preset-mini": ["@unocss/preset-mini@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/extractor-arbitrary-variants": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-k+/95PKMPOK57cJcSmz34VkIFem8BlujRRx6/L0Yusw7vLJMh98k0rPhC5s+NomZ/d9ZPgbNylskLhItJlak3w=="],
|
||||
"@unocss/preset-mini": ["@unocss/preset-mini@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/extractor-arbitrary-variants": "66.6.7", "@unocss/rule-utils": "66.6.7" } }, "sha512-tf0mqiSEhPQ49WZOqjNhxlbZbNakiBLzCoxfLSzqfIGglOPYShP8mxsdp9Jv0n+Ntn0rHcBiX5KTLfax1/Bd9g=="],
|
||||
|
||||
"@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-KgBXYPYS0g4TVC3NLiIB78YIqUlvDLanz1EHIDo34rOTUfMgY8Uf5VuDJAzMu4Sc0LiwwBJbk6nIG9/Zm7ufWg=="],
|
||||
"@unocss/preset-tagify": ["@unocss/preset-tagify@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7" } }, "sha512-0WeQf+Dx9Ztv3aewkBKEnAfOauSjvWBlfkpsgLpXcCkyGMnCqq87UrAq3+b76TDJvQc8i2ADlvVGK7V1z0JZQg=="],
|
||||
|
||||
"@unocss/preset-typography": ["@unocss/preset-typography@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-SM1km5nqt15z4sTabfOobSC633I5Ol5nnme6JFTra4wiyCUNs+Cg31nJ6jnopWDUT4SEAXqfUH7jKSSoCnI6ZA=="],
|
||||
"@unocss/preset-typography": ["@unocss/preset-typography@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/rule-utils": "66.6.7" } }, "sha512-RA7MwPDD5N9xGrbWnguVm5tP+F4/n/9X1rJsq2nBjvvK2dbtIRJZjRFM1vBDsR0GIhtvbHMoTchZaSZed5I+Hw=="],
|
||||
|
||||
"@unocss/preset-uno": ["@unocss/preset-uno@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6" } }, "sha512-40PcBDtlhW7QP7e/WOxC684IhN5T1dXvj1dgx9ZzK+8lEDGjcX7bN2noW4aSenzSrHymeSsMrL/0ltL4ED/5Zw=="],
|
||||
"@unocss/preset-uno": ["@unocss/preset-uno@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/preset-wind3": "66.6.7" } }, "sha512-imGCe6Yv2XgrJxP77gV8WZCz0xL99MsGov5rYn64lh2/tcsHF2rUIhTj/Urgxt0kwk8rLFtGbR1JuwPMNL5EDw=="],
|
||||
|
||||
"@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "ofetch": "^1.5.1" } }, "sha512-5ikwgrJB8VPzKd0bqgGNgYUGix90KFnVtKJPjWTP5qsv3+ZtZnea1rRbAFl8i2t52hg35msNBsQo+40IC3xB6A=="],
|
||||
"@unocss/preset-web-fonts": ["@unocss/preset-web-fonts@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "ofetch": "^1.5.1" } }, "sha512-GLjUoSL/kYt1Yw2zpzixKnxvpHgLHAg0JXiPglct4PZ9YmUzCPbvJ/vVn+0AnB8Fxr29Z8NAFSNoX625ZaRonQ=="],
|
||||
|
||||
"@unocss/preset-wind": ["@unocss/preset-wind@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-wind3": "66.6.6" } }, "sha512-TMy3lZ35FP/4QqDHOLWZmV+RoOGWUDqnDEOTjOKI1CQARGta0ppUmq+IZMuI1ZJLuOa4OZ9V6SfnwMXwRLgXmw=="],
|
||||
"@unocss/preset-wind": ["@unocss/preset-wind@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/preset-wind3": "66.6.7" } }, "sha512-jxtAN96jljd+KglbhPv6Y/ujceI5rVdrLQimj4KUTPoYBPEiWadzsGKN3o8Q07hlPRg+hBlO0r4tGSUWl+/EZQ=="],
|
||||
|
||||
"@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/preset-mini": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-rk6gPPIQ7z2DVucOqp7XZ4vGpKAuzBV1vtUDvDh5WscxzO/QlqaeTfTALk5YgGpmLaF4+ns6FrTgLjV+wHgHuQ=="],
|
||||
"@unocss/preset-wind3": ["@unocss/preset-wind3@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/preset-mini": "66.6.7", "@unocss/rule-utils": "66.6.7" } }, "sha512-PKyqeRzlIMd3Irdt6fCKMm73zgwweiXESk5edUK8dVWndvPIcZCOqrEq7yg6Pr/Q8tHdq26viYSkVY3a3t8RSg=="],
|
||||
|
||||
"@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/extractor-arbitrary-variants": "66.6.6", "@unocss/rule-utils": "66.6.6" } }, "sha512-caTDM9rZSlp4tyPWWAnwMvQr2PXq53LsEYwd3N8zj0ou2hcsqptJvF+mFvyhvGF66x26wWJr/FwuUEhh7qycaw=="],
|
||||
"@unocss/preset-wind4": ["@unocss/preset-wind4@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/extractor-arbitrary-variants": "66.6.7", "@unocss/rule-utils": "66.6.7" } }, "sha512-9grhWeBsFzpv8iER9AFATRaxLyXMCwGQ5HzeI4XZh2ZZ9O6vC7nYfGhns4/I+F/RpFglzU1bjqMWRS/DS8OpGQ=="],
|
||||
|
||||
"@unocss/rule-utils": ["@unocss/rule-utils@66.6.6", "", { "dependencies": { "@unocss/core": "^66.6.6", "magic-string": "^0.30.21" } }, "sha512-krWtQKGshOaqQMuxeGq1NOA8NL35VdpYlmQEWOe39BY6TACT51bgQFu40MRfsAIMZZtoGS2YYTrnHojgR92omw=="],
|
||||
"@unocss/rule-utils": ["@unocss/rule-utils@66.6.7", "", { "dependencies": { "@unocss/core": "^66.6.7", "magic-string": "^0.30.21" } }, "sha512-4PT/s8yKIShSqP9XPSw4EjbZopcu3wlIB9i3kbGbzQwF91H+0Yy10guK3kHDGtkmWVN6Np6VvaGIj2UcbmaivA=="],
|
||||
|
||||
"@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "oxc-parser": "^0.115.0", "oxc-walker": "^0.7.0" } }, "sha512-NnDchmN2EeFLy4lfVqDgNe9j1+w2RLL2L9zKECXs5g6rDVfeeEK6FNgxSq3XnPcKltjNCy1pF4MaDOROG7r8yA=="],
|
||||
"@unocss/transformer-attributify-jsx": ["@unocss/transformer-attributify-jsx@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "oxc-parser": "^0.115.0", "oxc-walker": "^0.7.0" } }, "sha512-r5bsnaPVe4iySLK5G5rA/QPSKmpPjYT9lixEv+KElvZcqZ+cPpkGoo+E+rnTcapu9KDMOVJItH/4Zy9m4AQ1ZQ=="],
|
||||
|
||||
"@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-KKssJxU8fZ9x84yznIirbtta2sB0LN/3lm0bp+Wl1298HITaNiVeG2n26iStQ3N7r240xRN2RarxncSVCMFwWw=="],
|
||||
"@unocss/transformer-compile-class": ["@unocss/transformer-compile-class@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7" } }, "sha512-4uz4jCyq8VUaSPveXhelUWUNaTnetPFvEmXzmbYJ5BygAlUlipNynffUlUusDQmBBRrfZhJNB5J1Zif2Q6oUiA=="],
|
||||
|
||||
"@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6", "@unocss/rule-utils": "66.6.6", "css-tree": "^3.1.0" } }, "sha512-CReFTcBfMtKkRvzIqxL20VptWt5C1Om27dwoKzyVFBXv0jzViWysbu0y0AQg3bsgD4cFqndFyAGyeL84j0nbKg=="],
|
||||
"@unocss/transformer-directives": ["@unocss/transformer-directives@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7", "@unocss/rule-utils": "66.6.7", "css-tree": "^3.1.0" } }, "sha512-z3gi8/cD2P0I+c6jOPZUtsPXknHwVNlMIitSh7LhyM6W3EqbqvDcYH2gFeGhdhoYcN2r5OpTBujq34iz4IdUxA=="],
|
||||
|
||||
"@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.6", "", { "dependencies": { "@unocss/core": "66.6.6" } }, "sha512-j4L/0Tw6AdMVB2dDnuBlDbevyL1/0CAk88a77VF/VjgEIBwB9VXsCCUsxz+2Dohcl7N2GMm7+kpaWA6qt2PSaA=="],
|
||||
"@unocss/transformer-variant-group": ["@unocss/transformer-variant-group@66.6.7", "", { "dependencies": { "@unocss/core": "66.6.7" } }, "sha512-XouJuQCjYJpvR3sY4QDXnGXxtyJ4qgWFG+S9bAB01TTslhQLvNPE9o2+4gZlltnJLqxiPQWuLeJA1KdPD6ciww=="],
|
||||
|
||||
"@unocss/vite": ["@unocss/vite@66.6.6", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.6", "@unocss/core": "66.6.6", "@unocss/inspector": "66.6.6", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-DgG7KcUUMtoDhPOlFf2l4dR+66xZ23SdZvTYpikk5nZfLCzZd62vedutD7x0bTR6VpK2YRq39B+F+Z6TktNY/w=="],
|
||||
"@unocss/vite": ["@unocss/vite@66.6.7", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@unocss/config": "66.6.7", "@unocss/core": "66.6.7", "@unocss/inspector": "66.6.7", "chokidar": "^5.0.0", "magic-string": "^0.30.21", "pathe": "^2.0.3", "tinyglobby": "^0.2.15", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0-0" } }, "sha512-8AHrVzAecnQaPLJv3/mpyFt5j2iL3gEwkZcZ8HzjH5ttK2XON1YE9vgujN5NS/yvZwlJxCMNPxn0S410/Ek61A=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.5", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.2.25" } }, "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-syntax-typescript": "^7.28.6", "@babel/plugin-transform-typescript": "^7.28.6", "@rolldown/pluginutils": "^1.0.0-rc.2", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.0.0" } }, "sha512-70LmoVk9riR7qc4W2CpjsbNMWTPnuZb9dpFKX1emru0yP57nsc9k8nhLA6U93ngQapv5VDIUq2JatNfLbBIkrA=="],
|
||||
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.5", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-syntax-typescript": "^7.28.6", "@babel/plugin-transform-typescript": "^7.28.6", "@rolldown/pluginutils": "^1.0.0-rc.2", "@vue/babel-plugin-jsx": "^2.0.1" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", "vue": "^3.0.0" } }, "sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA=="],
|
||||
|
||||
"@vue-macros/common": ["@vue-macros/common@3.1.2", "", { "dependencies": { "@vue/compiler-sfc": "^3.5.22", "ast-kit": "^2.1.2", "local-pkg": "^1.1.2", "magic-string-ast": "^1.0.2", "unplugin-utils": "^0.3.0" }, "peerDependencies": { "vue": "^2.7.0 || ^3.2.25" }, "optionalPeers": ["vue"] }, "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng=="],
|
||||
|
||||
@@ -371,13 +322,13 @@
|
||||
|
||||
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@2.0.1", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/parser": "^7.28.4", "@vue/compiler-sfc": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/shared": "3.5.31", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
|
||||
|
||||
@@ -385,15 +336,15 @@
|
||||
|
||||
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.30", "", { "dependencies": { "@vue/shared": "3.5.30" } }, "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q=="],
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg=="],
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.30", "", { "dependencies": { "@vue/reactivity": "3.5.30", "@vue/runtime-core": "3.5.30", "@vue/shared": "3.5.30", "csstype": "^3.2.3" } }, "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw=="],
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/runtime-core": "3.5.31", "@vue/shared": "3.5.31", "csstype": "^3.2.3" } }, "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.30", "", { "dependencies": { "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "vue": "3.5.30" } }, "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ=="],
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.31", "", { "dependencies": { "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "vue": "3.5.31" } }, "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
||||
"@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
|
||||
|
||||
@@ -417,22 +368,24 @@
|
||||
|
||||
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.10", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ=="],
|
||||
|
||||
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
"bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
@@ -451,9 +404,9 @@
|
||||
|
||||
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
|
||||
|
||||
"cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="],
|
||||
"cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="],
|
||||
|
||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||
|
||||
@@ -467,19 +420,17 @@
|
||||
|
||||
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.323", "", {}, "sha512-oQm+FxbazvN2WICCbvJgj3IYPKV8awip57+W5VP+Aatk4kFU4pDYCPHZOX22Z27zpw8uttBehEqgK+VTJAYrVw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
@@ -497,13 +448,13 @@
|
||||
|
||||
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||
|
||||
"hono": ["hono@4.12.7", "", {}, "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw=="],
|
||||
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
|
||||
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
|
||||
"hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="],
|
||||
|
||||
"i18next": ["i18next@25.8.18", "", { "dependencies": { "@babel/runtime": "^7.28.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA=="],
|
||||
"i18next": ["i18next@26.0.3", "", { "dependencies": { "@babel/runtime": "^7.29.2" }, "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg=="],
|
||||
|
||||
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
|
||||
"i18next-http-backend": ["i18next-http-backend@3.0.4", "", { "dependencies": { "cross-fetch": "4.1.0" } }, "sha512-udwrBIE6cNpqn1gRAqRULq3+7MzIIuaiKRWrz++dVz5SqWW2VwXmPJtAgkI0JtMLFaADC9qNmnZAxWAhsxXx2g=="],
|
||||
|
||||
"i18next-vue": ["i18next-vue@5.4.0", "", { "peerDependencies": { "i18next": ">=23", "vue": "^3.4.38" } }, "sha512-GDj0Xvmis5Xgcvo9gMBJMgJCtewYMLZP6gAEPDDGCMjA+QeB4uS4qUf1MK79mkz/FukhaJdC+nlj0y1qk6NO2Q=="],
|
||||
|
||||
@@ -521,29 +472,29 @@
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||
|
||||
@@ -559,7 +510,7 @@
|
||||
|
||||
"magic-string-ast": ["magic-string-ast@1.0.3", "", { "dependencies": { "magic-string": "^0.30.19" } }, "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA=="],
|
||||
|
||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
@@ -567,7 +518,7 @@
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
@@ -581,7 +532,7 @@
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
@@ -599,13 +550,13 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
|
||||
|
||||
"pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
@@ -619,7 +570,7 @@
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-rc.6", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.6" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-arm64": "1.0.0-rc.6", "@rolldown/binding-darwin-x64": "1.0.0-rc.6", "@rolldown/binding-freebsd-x64": "1.0.0-rc.6", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.6", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.6", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.6", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.6", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.6", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.6", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.6" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-B8vFPV1ADyegoYfhg+E7RAucYKv0xdVlwYYsIJgfPNeiSxZGWNxts9RqhyGzC11ULK/VaeXyKezGCwpMiH8Ktw=="],
|
||||
"rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
|
||||
|
||||
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||
|
||||
@@ -643,7 +594,7 @@
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
@@ -667,9 +618,9 @@
|
||||
|
||||
"unhead": ["unhead@2.1.12", "", { "dependencies": { "hookable": "^6.0.1" } }, "sha512-iTHdWD9ztTunOErtfUFk6Wr11BxvzumcYJ0CzaSCBUOEtg+DUZ9+gnE99i8QkLFT2q1rZD48BYYGXpOZVDLYkA=="],
|
||||
|
||||
"unimport": ["unimport@5.6.0", "", { "dependencies": { "acorn": "^8.15.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A=="],
|
||||
"unimport": ["unimport@5.7.0", "", { "dependencies": { "acorn": "^8.16.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "pkg-types": "^2.3.0", "scule": "^1.3.0", "strip-literal": "^3.1.0", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" } }, "sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q=="],
|
||||
|
||||
"unocss": ["unocss@66.6.6", "", { "dependencies": { "@unocss/cli": "66.6.6", "@unocss/core": "66.6.6", "@unocss/preset-attributify": "66.6.6", "@unocss/preset-icons": "66.6.6", "@unocss/preset-mini": "66.6.6", "@unocss/preset-tagify": "66.6.6", "@unocss/preset-typography": "66.6.6", "@unocss/preset-uno": "66.6.6", "@unocss/preset-web-fonts": "66.6.6", "@unocss/preset-wind": "66.6.6", "@unocss/preset-wind3": "66.6.6", "@unocss/preset-wind4": "66.6.6", "@unocss/transformer-attributify-jsx": "66.6.6", "@unocss/transformer-compile-class": "66.6.6", "@unocss/transformer-directives": "66.6.6", "@unocss/transformer-variant-group": "66.6.6", "@unocss/vite": "66.6.6" }, "peerDependencies": { "@unocss/astro": "66.6.6", "@unocss/postcss": "66.6.6", "@unocss/webpack": "66.6.6" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-PRKK945e2oZKHV664MA5Z9CDHbvY/V79IvTOUWKZ514jpl3UsJU3sS+skgxmKJSmwrWvXE5OVcmPthJrD/7vxg=="],
|
||||
"unocss": ["unocss@66.6.7", "", { "dependencies": { "@unocss/cli": "66.6.7", "@unocss/core": "66.6.7", "@unocss/preset-attributify": "66.6.7", "@unocss/preset-icons": "66.6.7", "@unocss/preset-mini": "66.6.7", "@unocss/preset-tagify": "66.6.7", "@unocss/preset-typography": "66.6.7", "@unocss/preset-uno": "66.6.7", "@unocss/preset-web-fonts": "66.6.7", "@unocss/preset-wind": "66.6.7", "@unocss/preset-wind3": "66.6.7", "@unocss/preset-wind4": "66.6.7", "@unocss/transformer-attributify-jsx": "66.6.7", "@unocss/transformer-compile-class": "66.6.7", "@unocss/transformer-directives": "66.6.7", "@unocss/transformer-variant-group": "66.6.7", "@unocss/vite": "66.6.7" }, "peerDependencies": { "@unocss/astro": "66.6.7", "@unocss/postcss": "66.6.7", "@unocss/webpack": "66.6.7" }, "optionalPeers": ["@unocss/astro", "@unocss/postcss", "@unocss/webpack"] }, "sha512-TdZ/JnKhrqkknrMvLl0KOwrGzFThEspFIyYiylFYJki2JkMN/5EJIr+vIZEGRX69hFTjTLi6utIpbipueqzNbw=="],
|
||||
|
||||
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
|
||||
|
||||
@@ -677,19 +628,19 @@
|
||||
|
||||
"unplugin-utils": ["unplugin-utils@0.3.1", "", { "dependencies": { "pathe": "^2.0.3", "picomatch": "^4.0.3" } }, "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog=="],
|
||||
|
||||
"unplugin-vue-components": ["unplugin-vue-components@31.0.0", "", { "dependencies": { "chokidar": "^5.0.0", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "obug": "^2.1.1", "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit"] }, "sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q=="],
|
||||
"unplugin-vue-components": ["unplugin-vue-components@32.0.0", "", { "dependencies": { "chokidar": "^5.0.0", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.2", "obug": "^2.1.1", "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit"] }, "sha512-uLdccgS7mf3pv1bCCP20y/hm+u1eOjAmygVkh+Oa70MPkzgl1eQv1L0CwdHNM3gscO8/GDMGIET98Ja47CBbZg=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||
|
||||
"urlpattern-polyfill": ["urlpattern-polyfill@10.1.0", "", {}, "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw=="],
|
||||
|
||||
"vite": ["vite@8.0.0-beta.16", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.6", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-c0t7hYkxsjws89HH+BUFh/sL3BpPNhNsL9CJrTpMxBmwKQBRSa5OJ5w4o9O0bQVI/H/vx7UpUUIevvXa37NS/Q=="],
|
||||
"vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
|
||||
|
||||
"vite-ssr-components": ["vite-ssr-components@0.5.2", "", { "dependencies": { "@babel/parser": "^7.27.2", "@babel/traverse": "^7.27.1", "picomatch": "^4.0.2" } }, "sha512-1a8YThRwyyu1gGjc1Ral9Q4uS+n0D4GydhbkVd9c1SA1YNgXyrOizttped87C1ItEznQzhiCyQjaOcYnXa0zMA=="],
|
||||
|
||||
"vue": ["vue@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/compiler-sfc": "3.5.30", "@vue/runtime-dom": "3.5.30", "@vue/server-renderer": "3.5.30", "@vue/shared": "3.5.30" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg=="],
|
||||
"vue": ["vue@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/compiler-sfc": "3.5.31", "@vue/runtime-dom": "3.5.31", "@vue/server-renderer": "3.5.31", "@vue/shared": "3.5.31" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q=="],
|
||||
|
||||
"vue-router": ["vue-router@5.0.3", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-nG1c7aAFac7NYj8Hluo68WyWfc41xkEjaR0ViLHCa3oDvTQ/nIuLJlXJX1NUPw/DXzx/8+OKMng045HHQKQKWw=="],
|
||||
"vue-router": ["vue-router@5.0.4", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.6", "ast-walker-scope": "^0.8.3", "chokidar": "^5.0.0", "json5": "^2.2.3", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "muggle-string": "^0.4.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "scule": "^1.3.0", "tinyglobby": "^0.2.15", "unplugin": "^3.0.0", "unplugin-utils": "^0.3.1", "yaml": "^2.8.2" }, "peerDependencies": { "@pinia/colada": ">=0.21.2", "@vue/compiler-sfc": "^3.5.17", "pinia": "^3.0.4", "vue": "^3.5.0" }, "optionalPeers": ["@pinia/colada", "@vue/compiler-sfc", "pinia"] }, "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
@@ -703,7 +654,7 @@
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
@@ -713,25 +664,27 @@
|
||||
|
||||
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
|
||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
|
||||
|
||||
"@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||
"@vue/babel-plugin-jsx/@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.29", "@vue/compiler-dom": "3.5.29", "@vue/compiler-ssr": "3.5.29", "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA=="],
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.30", "@vue/compiler-dom": "3.5.30", "@vue/compiler-ssr": "3.5.30", "@vue/shared": "3.5.30", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A=="],
|
||||
|
||||
"@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"magic-regexp/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.6", "", {}, "sha512-Y0+JT8Mi1mmW08K6HieG315XNRu4L0rkfCpA364HtytjgiqYnMYRdFPcxRl+BQQqNXzecL2S9nii+RUpO93XIA=="],
|
||||
"oxc-parser/@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
@@ -739,33 +692,37 @@
|
||||
|
||||
"unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||
|
||||
"unimport/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
"unplugin-vue-components/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
|
||||
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.0.6", "", { "dependencies": { "@vue/devtools-kit": "^8.0.6" } }, "sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA=="],
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@8.1.1", "", { "dependencies": { "@vue/devtools-kit": "^8.1.1" } }, "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw=="],
|
||||
|
||||
"vue-router/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
|
||||
"@vue-macros/common/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.29", "", { "dependencies": { "@vue/compiler-core": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg=="],
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.30", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.30", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.30", "", { "dependencies": { "@vue/compiler-core": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.29", "", {}, "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg=="],
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.30", "", { "dependencies": { "@vue/compiler-dom": "3.5.30", "@vue/shared": "3.5.30" } }, "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.30", "", {}, "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.0.6", "", { "dependencies": { "@vue/devtools-shared": "^8.0.6", "birpc": "^2.6.1", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^2.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw=="],
|
||||
"vue-router/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@8.1.1", "", { "dependencies": { "@vue/devtools-shared": "^8.1.1", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg=="],
|
||||
|
||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.0.6", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg=="],
|
||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.1.1", "", {}, "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ=="],
|
||||
|
||||
"vue-router/@vue/devtools-api/@vue/devtools-kit/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
}
|
||||
|
||||
22
components.d.ts
vendored
22
components.d.ts
vendored
@@ -30,7 +30,8 @@ declare module 'vue' {
|
||||
AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||
BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||
BellDot: typeof import('./src/components/icons/BellDot.vue')['default']
|
||||
BellOff: typeof import('./src/components/icons/BellOff.vue')['default']
|
||||
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
@@ -49,16 +50,21 @@ declare module 'vue' {
|
||||
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||
Globe: typeof import('./src/components/icons/Globe.vue')['default']
|
||||
GlobeIcon: typeof import('./src/components/icons/GlobeIcon.vue')['default']
|
||||
HardDrive: typeof import('./src/components/icons/hard-drive.vue')['default']
|
||||
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||
HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||
Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||
ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||
Inbox: typeof import('./src/components/icons/Inbox.vue')['default']
|
||||
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||
LanguageIcon: typeof import('./src/components/icons/LanguageIcon.vue')['default']
|
||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
ListIcon: typeof import('./src/components/icons/ListIcon.vue')['default']
|
||||
LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
||||
MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||
MoneyCheck: typeof import('./src/components/icons/MoneyCheck.vue')['default']
|
||||
MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||
OfflineOverlay: typeof import('./src/components/OfflineOverlay.vue')['default']
|
||||
@@ -68,12 +74,14 @@ declare module 'vue' {
|
||||
PlayIcon: typeof import('./src/components/icons/PlayIcon.vue')['default']
|
||||
PlusIcon: typeof import('./src/components/icons/PlusIcon.vue')['default']
|
||||
PlusSquareIcon: typeof import('./src/components/icons/PlusSquareIcon.vue')['default']
|
||||
PopupAdsRuntime: typeof import('./src/components/PopupAdsRuntime.vue')['default']
|
||||
RepeatIcon: typeof import('./src/components/icons/RepeatIcon.vue')['default']
|
||||
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||
ShieldUser: typeof import('./src/components/icons/shield-user.vue')['default']
|
||||
SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||
TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
||||
@@ -90,6 +98,7 @@ declare module 'vue' {
|
||||
VolumeOffIcon: typeof import('./src/components/icons/VolumeOffIcon.vue')['default']
|
||||
VueHead: typeof import('./src/components/VueHead.tsx')['default']
|
||||
WifiIcon: typeof import('./src/components/icons/WifiIcon.vue')['default']
|
||||
Windows: typeof import('./src/components/icons/windows.vue')['default']
|
||||
XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
|
||||
XIcon: typeof import('./src/components/icons/XIcon.vue')['default']
|
||||
}
|
||||
@@ -115,7 +124,8 @@ declare global {
|
||||
const AsyncSelect: typeof import('./src/components/ui/AsyncSelect.vue')['default']
|
||||
const BaseTable: typeof import('./src/components/ui/BaseTable.vue')['default']
|
||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
const BellIcon: typeof import('./src/components/icons/BellIcon.vue')['default']
|
||||
const BellDot: typeof import('./src/components/icons/BellDot.vue')['default']
|
||||
const BellOff: typeof import('./src/components/icons/BellOff.vue')['default']
|
||||
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
@@ -134,16 +144,21 @@ declare global {
|
||||
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||
const Globe: typeof import('./src/components/icons/Globe.vue')['default']
|
||||
const GlobeIcon: typeof import('./src/components/icons/GlobeIcon.vue')['default']
|
||||
const HardDrive: typeof import('./src/components/icons/hard-drive.vue')['default']
|
||||
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||
const HeartIcon: typeof import('./src/components/icons/HeartIcon.vue')['default']
|
||||
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||
const ImageIcon: typeof import('./src/components/icons/ImageIcon.vue')['default']
|
||||
const Inbox: typeof import('./src/components/icons/Inbox.vue')['default']
|
||||
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||
const LanguageIcon: typeof import('./src/components/icons/LanguageIcon.vue')['default']
|
||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
const LayoutDashboard: typeof import('./src/components/icons/LayoutDashboard.vue')['default']
|
||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
const ListIcon: typeof import('./src/components/icons/ListIcon.vue')['default']
|
||||
const LockIcon: typeof import('./src/components/icons/LockIcon.vue')['default']
|
||||
const MailIcon: typeof import('./src/components/icons/MailIcon.vue')['default']
|
||||
const MoneyCheck: typeof import('./src/components/icons/MoneyCheck.vue')['default']
|
||||
const MonitorIcon: typeof import('./src/components/icons/MonitorIcon.vue')['default']
|
||||
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||
const OfflineOverlay: typeof import('./src/components/OfflineOverlay.vue')['default']
|
||||
@@ -153,12 +168,14 @@ declare global {
|
||||
const PlayIcon: typeof import('./src/components/icons/PlayIcon.vue')['default']
|
||||
const PlusIcon: typeof import('./src/components/icons/PlusIcon.vue')['default']
|
||||
const PlusSquareIcon: typeof import('./src/components/icons/PlusSquareIcon.vue')['default']
|
||||
const PopupAdsRuntime: typeof import('./src/components/PopupAdsRuntime.vue')['default']
|
||||
const RepeatIcon: typeof import('./src/components/icons/RepeatIcon.vue')['default']
|
||||
const RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||
const RouterView: typeof import('vue-router')['RouterView']
|
||||
const SendIcon: typeof import('./src/components/icons/SendIcon.vue')['default']
|
||||
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||
const ShieldUser: typeof import('./src/components/icons/shield-user.vue')['default']
|
||||
const SlidersIcon: typeof import('./src/components/icons/SlidersIcon.vue')['default']
|
||||
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||
const TelegramIcon: typeof import('./src/components/icons/TelegramIcon.vue')['default']
|
||||
@@ -175,6 +192,7 @@ declare global {
|
||||
const VolumeOffIcon: typeof import('./src/components/icons/VolumeOffIcon.vue')['default']
|
||||
const VueHead: typeof import('./src/components/VueHead.tsx')['default']
|
||||
const WifiIcon: typeof import('./src/components/icons/WifiIcon.vue')['default']
|
||||
const Windows: typeof import('./src/components/icons/windows.vue')['default']
|
||||
const XCircleIcon: typeof import('./src/components/icons/XCircleIcon.vue')['default']
|
||||
const XIcon: typeof import('./src/components/icons/XIcon.vue')['default']
|
||||
}
|
||||
35
package.json
35
package.json
@@ -2,9 +2,9 @@
|
||||
"name": "holistream",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "bunx --bun vite",
|
||||
"build": "bunx --bun vite build",
|
||||
"preview": "bunx --bun vite preview"
|
||||
"dev": "bun x --bun vite",
|
||||
"build": "bun x --bun vite build",
|
||||
"preview": "bun x --bun vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
@@ -12,36 +12,37 @@
|
||||
"@hattip/adapter-node": "^0.0.49",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hiogawa/utils": "^1.7.0",
|
||||
"@hono/node-server": "^1.19.11",
|
||||
"@hono/node-server": "^1.19.12",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@pinia/colada": "^1.0.0",
|
||||
"@pinia/colada": "^1.1.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.1.12",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"aws4fetch": "^1.0.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"hono": "^4.12.7",
|
||||
"i18next": "^25.8.18",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"hono": "^4.12.9",
|
||||
"i18next": "^26.0.3",
|
||||
"i18next-http-backend": "^3.0.4",
|
||||
"i18next-vue": "^5.4.0",
|
||||
"is-mobile": "^5.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"superjson": "^2.2.6",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue": "^3.5.31",
|
||||
"vue-router": "^5.0.4",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.10",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.4",
|
||||
"estree-walker": "2.0.2",
|
||||
"unocss": "^66.6.6",
|
||||
"@types/bun": "^1.3.11",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"estree-walker": "3.0.3",
|
||||
"unocss": "^66.6.7",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^8.0.0-beta.16",
|
||||
"unplugin-vue-components": "^32.0.0",
|
||||
"vite": "^8.0.3",
|
||||
"vite-ssr-components": "^0.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package stream.common.v1;
|
||||
|
||||
option go_package = "stream/proto/gen/go/common/v1;commonv1";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
message RequestContext {
|
||||
string user_id = 1;
|
||||
string email = 2;
|
||||
string role = 3;
|
||||
string request_id = 4;
|
||||
string source = 5;
|
||||
}
|
||||
|
||||
message PaginationRequest {
|
||||
int32 page = 1;
|
||||
int32 page_size = 2;
|
||||
}
|
||||
|
||||
message PaginationResponse {
|
||||
int32 page = 1;
|
||||
int32 page_size = 2;
|
||||
int64 total = 3;
|
||||
}
|
||||
|
||||
message Money {
|
||||
double amount = 1;
|
||||
string currency = 2;
|
||||
}
|
||||
|
||||
message Empty {}
|
||||
|
||||
message IdRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteResponse {
|
||||
string message = 1;
|
||||
}
|
||||
|
||||
message TimestampRange {
|
||||
google.protobuf.Timestamp from = 1;
|
||||
google.protobuf.Timestamp to = 2;
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package stream.User.v1;
|
||||
|
||||
option go_package = "stream/proto/gen/go/User/v1;Userv1";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
service UserService {
|
||||
// User CRUD
|
||||
rpc GetUser(GetUserRequest) returns (GetUserResponse);
|
||||
rpc GetUserByEmail(GetUserByEmailRequest) returns (GetUserResponse);
|
||||
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
|
||||
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
|
||||
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
|
||||
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
|
||||
rpc UpdateUserPassword(UpdateUserPasswordRequest) returns (google.protobuf.Empty);
|
||||
|
||||
// Preferences
|
||||
rpc GetPreferences(GetPreferencesRequest) returns (GetPreferencesResponse);
|
||||
rpc UpsertPreferences(UpsertPreferencesRequest) returns (UpsertPreferencesResponse);
|
||||
}
|
||||
|
||||
// ─── User Messages ───────────────────────────────────────────────────────────
|
||||
message UpdateUserPasswordRequest {
|
||||
string id = 1;
|
||||
string new_password = 2;
|
||||
}
|
||||
message GetUserRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message GetUserByEmailRequest {
|
||||
string email = 1;
|
||||
}
|
||||
|
||||
message GetUserResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
message ListUsersRequest {
|
||||
int32 page = 1;
|
||||
int32 page_size = 2;
|
||||
string role = 3; // optional filter
|
||||
}
|
||||
|
||||
message ListUsersResponse {
|
||||
repeated User users = 1;
|
||||
int32 total = 2;
|
||||
int32 page = 3;
|
||||
int32 page_size = 4;
|
||||
}
|
||||
|
||||
message CreateUserRequest {
|
||||
string email = 1;
|
||||
optional string username = 2;
|
||||
optional string password = 3;
|
||||
}
|
||||
|
||||
message CreateUserResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
message UpdateUserRequest {
|
||||
string id = 1;
|
||||
optional string username = 2;
|
||||
optional string avatar = 3;
|
||||
optional string role = 4;
|
||||
optional string plan_id = 5;
|
||||
}
|
||||
|
||||
message UpdateUserResponse {
|
||||
User user = 1;
|
||||
}
|
||||
|
||||
message DeleteUserRequest {
|
||||
string id = 1;
|
||||
}
|
||||
|
||||
message DeleteUserResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
// ─── Preferences Messages ────────────────────────────────────────────────────
|
||||
|
||||
message GetPreferencesRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
|
||||
message GetPreferencesResponse {
|
||||
Preferences preferences = 1;
|
||||
}
|
||||
|
||||
message UpsertPreferencesRequest {
|
||||
Preferences preferences = 1;
|
||||
}
|
||||
|
||||
message UpsertPreferencesResponse {
|
||||
Preferences preferences = 1;
|
||||
}
|
||||
|
||||
// ─── Core Models ─────────────────────────────────────────────────────────────
|
||||
|
||||
message User {
|
||||
string id = 1;
|
||||
string email = 2;
|
||||
string password = 3;
|
||||
optional string username = 4;
|
||||
optional string avatar = 5;
|
||||
optional string role = 6;
|
||||
optional string google_id = 7;
|
||||
int64 storage_used = 8;
|
||||
optional string plan_id = 9;
|
||||
optional google.protobuf.Timestamp created_at = 10;
|
||||
google.protobuf.Timestamp updated_at = 11;
|
||||
}
|
||||
|
||||
message Preferences {
|
||||
string user_id = 1;
|
||||
optional string language = 2;
|
||||
optional string locale = 3;
|
||||
optional bool email_notifications = 4;
|
||||
optional bool push_notifications = 5;
|
||||
optional bool marketing_notifications = 6;
|
||||
optional bool telegram_notifications = 7;
|
||||
optional bool autoplay = 8;
|
||||
optional bool loop = 9;
|
||||
optional bool muted = 10;
|
||||
optional bool show_controls = 11;
|
||||
optional bool pip = 12;
|
||||
optional bool airplay = 13;
|
||||
optional bool chromecast = 14;
|
||||
optional bool encrytion_m3u8 = 15;
|
||||
}
|
||||
@@ -11,7 +11,12 @@
|
||||
"status": "Status",
|
||||
"videos": "Videos",
|
||||
"selected": "{{count}} selected",
|
||||
"copy": "Copy"
|
||||
"copy": "Copy",
|
||||
"rowsPerPage": "Rows per page",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"page": "Page {{current}} of {{total}}",
|
||||
"records": "records"
|
||||
},
|
||||
"app": {
|
||||
"name": "EcoStream"
|
||||
@@ -116,7 +121,8 @@
|
||||
"playerConfigs": "Player Configs",
|
||||
"domains": "Allowed Domains",
|
||||
"ads": "Ads & VAST",
|
||||
"danger": "Danger Zone"
|
||||
"danger": "Danger Zone",
|
||||
"popupAds": "Popup Ads"
|
||||
},
|
||||
"content": {
|
||||
"fallbackTitle": "Settings",
|
||||
@@ -152,6 +158,10 @@
|
||||
"danger": {
|
||||
"title": "Danger Zone",
|
||||
"subtitle": "Irreversible and destructive actions. Be careful!"
|
||||
},
|
||||
"popupAds": {
|
||||
"title": "Popup Ads",
|
||||
"subtitle": "Manage your popup ad settings and preferences."
|
||||
}
|
||||
},
|
||||
"notificationSettings": {
|
||||
@@ -389,7 +399,7 @@
|
||||
"create": "Create"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Are you sure you want to delete \"{name}\"?",
|
||||
"deleteMessage": "Are you sure you want to delete \"{{name}}\"?",
|
||||
"deleteHeader": "Delete Config",
|
||||
"deleteAccept": "Delete",
|
||||
"deleteReject": "Cancel"
|
||||
@@ -404,14 +414,14 @@
|
||||
"enabledSummary": "Config enabled",
|
||||
"disabledSummary": "Config disabled",
|
||||
"defaultUpdatedSummary": "Default updated",
|
||||
"defaultUpdatedDetail": "{name} is now the default config for new videos.",
|
||||
"defaultUpdatedDetail": "{{name}} is now the default config for new videos.",
|
||||
"upgradeRequiredSummary": "Config limit reached",
|
||||
"upgradeRequiredDetail": "Free accounts can only have 1 player config.",
|
||||
"limitSummary": "Config limit reached",
|
||||
"limitDetail": "Free accounts can only have 1 player config.",
|
||||
"reconciliationSummary": "Delete extra configs",
|
||||
"reconciliationDetail": "Delete extra player configs until only 1 remains to continue managing them on the free plan.",
|
||||
"toggleDetail": "{name} has been {state}.",
|
||||
"toggleDetail": "{{name}} has been {state}.",
|
||||
"deletedSummary": "Config deleted",
|
||||
"deletedDetail": "Player config has been removed.",
|
||||
"failedSummary": "Action failed",
|
||||
@@ -463,7 +473,7 @@
|
||||
"create": "Create"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Are you sure you want to delete \"{name}\"?",
|
||||
"deleteMessage": "Are you sure you want to delete \"{{name}}\"?",
|
||||
"deleteHeader": "Delete Template",
|
||||
"deleteAccept": "Delete",
|
||||
"deleteReject": "Cancel"
|
||||
@@ -484,10 +494,10 @@
|
||||
"enabledSummary": "Template Enabled",
|
||||
"disabledSummary": "Template Disabled",
|
||||
"defaultUpdatedSummary": "Default Updated",
|
||||
"defaultUpdatedDetail": "{name} is now the default template for new videos.",
|
||||
"defaultUpdatedDetail": "{{name}} is now the default template for new videos.",
|
||||
"upgradeRequiredSummary": "Upgrade required",
|
||||
"upgradeRequiredDetail": "Upgrade your plan to manage Ads & VAST.",
|
||||
"toggleDetail": "{name} has been {state}.",
|
||||
"toggleDetail": "{{name}} has been {state}.",
|
||||
"deletedSummary": "Template Deleted",
|
||||
"deletedDetail": "VAST template has been removed.",
|
||||
"copiedSummary": "Copied",
|
||||
@@ -496,6 +506,68 @@
|
||||
"failedDetail": "Failed to load or update VAST templates."
|
||||
}
|
||||
},
|
||||
"popupAds": {
|
||||
"createItem": "Add Popup Ad",
|
||||
"maxTriggersLabel": "Highest URL trigger limit per session ({{count}})",
|
||||
"emptyTitle": "No popup ads yet",
|
||||
"emptySubtitle": "Create a popup ad to start opening URLs or injecting scripts.",
|
||||
"types": {
|
||||
"url": "URL",
|
||||
"script": "Script"
|
||||
},
|
||||
"table": {
|
||||
"label": "Label",
|
||||
"type": "Type",
|
||||
"target": "Target",
|
||||
"maxTriggersPerSession": "Max triggers/session"
|
||||
},
|
||||
"dialog": {
|
||||
"createTitle": "Create Popup Ad",
|
||||
"editTitle": "Edit Popup Ad",
|
||||
"type": "Type",
|
||||
"label": "Label",
|
||||
"labelPlaceholder": "e.g., Ad Network 1",
|
||||
"url": "Destination URL",
|
||||
"urlPlaceholder": "https://example.com/landing-page",
|
||||
"script": "Script snippet",
|
||||
"scriptPlaceholder": "<script async src=\"//example.com/ad.js\"></script>",
|
||||
"maxTriggersPerSession": "Max popup triggers per session",
|
||||
"activeTitle": "Item status",
|
||||
"activeDescription": "Disable an item to keep it in the table without serving it.",
|
||||
"update": "Update",
|
||||
"create": "Create"
|
||||
},
|
||||
"info": {
|
||||
"urlTitle": "URL:",
|
||||
"urlDescription": "Opens in a new tab when viewer clicks.",
|
||||
"scriptTitle": "Script:",
|
||||
"scriptDescription": "Injects the script tag into the page (popunder networks, etc)."
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Are you sure you want to delete \"{{name}}\"?",
|
||||
"deleteHeader": "Delete Popup Ad",
|
||||
"deleteAccept": "Delete",
|
||||
"deleteReject": "Cancel"
|
||||
},
|
||||
"toast": {
|
||||
"labelRequiredSummary": "Label required",
|
||||
"labelRequiredDetail": "Please enter a label for this popup ad.",
|
||||
"valueRequiredSummary": "Value required",
|
||||
"valueRequiredDetail": "Please enter a URL or script snippet.",
|
||||
"maxTriggersRequiredSummary": "Trigger limit required",
|
||||
"maxTriggersRequiredDetail": "Please enter a max trigger count greater than 0 for URL popup ads.",
|
||||
"invalidUrlSummary": "Invalid URL",
|
||||
"invalidUrlDetail": "Please enter a valid URL.",
|
||||
"createdSummary": "Popup ad created",
|
||||
"createdDetail": "The popup ad has been added.",
|
||||
"updatedSummary": "Popup ad updated",
|
||||
"updatedDetail": "The popup ad has been updated.",
|
||||
"deletedSummary": "Popup ad deleted",
|
||||
"deletedDetail": "The popup ad has been removed.",
|
||||
"failedSummary": "Action failed",
|
||||
"failedDetail": "Failed to load or update popup ads."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile Information",
|
||||
"subtitle": "Manage your personal information and account details.",
|
||||
@@ -503,7 +575,7 @@
|
||||
"username": "Username",
|
||||
"email": "Email Address",
|
||||
"storageUsage": "Storage Usage",
|
||||
"storageUsedOfLimit": "{{used}} of {{limit}} used",
|
||||
"storageUsedOfLimit": "{{used}} used",
|
||||
"editProfile": "Edit Profile",
|
||||
"changePassword": "Change Password"
|
||||
},
|
||||
@@ -534,9 +606,9 @@
|
||||
"processing": "Processing...",
|
||||
"upgrade": "Upgrade",
|
||||
"storage": "Storage",
|
||||
"storageUsedOfLimit": "{{used}} of {{limit}} used",
|
||||
"storageUsedOfLimit": "{{used}} used",
|
||||
"totalVideos": "Total videos",
|
||||
"totalVideosUsedOfLimit": "{{used}} of {{limit}} videos",
|
||||
"totalVideosUsedOfLimit": "{{used}} videos",
|
||||
"paymentHistory": "Payment History",
|
||||
"paymentHistorySubtitle": "Your past payments and invoices",
|
||||
"noPaymentHistory": "No payment history found.",
|
||||
@@ -552,9 +624,9 @@
|
||||
},
|
||||
"subscription": {
|
||||
"activeTitle": "Plan active",
|
||||
"activeDescription": " {{plan}} is active until {{date}}",
|
||||
"activeDescription": " Active until {{date}}",
|
||||
"expiringTitle": "Expiring soon",
|
||||
"expiringDescription": " {{plan}} expires on {{date}}",
|
||||
"expiringDescription": " Expires on {{date}}",
|
||||
"expiredTitle": "Plan expired",
|
||||
"expiredDescription": "Your last subscription ended on {{date}}",
|
||||
"freeTitle": "Free access",
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
"status": "Trạng thái",
|
||||
"videos": "Video",
|
||||
"selected": "{{count}} mục đã chọn",
|
||||
"copy": "Sao chép"
|
||||
"copy": "Sao chép",
|
||||
"rowsPerPage": "Số hàng mỗi trang",
|
||||
"next": "Tiếp",
|
||||
"previous": "Trước",
|
||||
"page": "Trang {{current}} trên {{total}}",
|
||||
"records": "bản ghi"
|
||||
},
|
||||
"app": {
|
||||
"name": "EcoStream"
|
||||
@@ -116,7 +121,8 @@
|
||||
"playerConfigs": "Cấu hình trình phát",
|
||||
"domains": "Tên miền được phép",
|
||||
"ads": "Quảng cáo & VAST",
|
||||
"danger": "Vùng nguy hiểm"
|
||||
"danger": "Vùng nguy hiểm",
|
||||
"popupAds": "Popup Ads"
|
||||
},
|
||||
"content": {
|
||||
"fallbackTitle": "Cài đặt",
|
||||
@@ -152,6 +158,10 @@
|
||||
"danger": {
|
||||
"title": "Vùng nguy hiểm",
|
||||
"subtitle": "Hành động không thể hoàn tác và có tính phá hủy. Hãy cẩn thận!"
|
||||
},
|
||||
"popupAds": {
|
||||
"title": "Popup Ads",
|
||||
"subtitle": "Quản lý cài đặt và tùy chọn quảng cáo popup của bạn."
|
||||
}
|
||||
},
|
||||
"notificationSettings": {
|
||||
@@ -389,7 +399,7 @@
|
||||
"create": "Tạo"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{name}\"?",
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{{name}}\"?",
|
||||
"deleteHeader": "Xóa cấu hình",
|
||||
"deleteAccept": "Xóa",
|
||||
"deleteReject": "Hủy"
|
||||
@@ -404,14 +414,14 @@
|
||||
"enabledSummary": "Đã bật cấu hình",
|
||||
"disabledSummary": "Đã tắt cấu hình",
|
||||
"defaultUpdatedSummary": "Đã cập nhật mặc định",
|
||||
"defaultUpdatedDetail": "{name} hiện là cấu hình mặc định cho video mới.",
|
||||
"defaultUpdatedDetail": "{{name}} hiện là cấu hình mặc định cho video mới.",
|
||||
"upgradeRequiredSummary": "Đã đạt giới hạn cấu hình",
|
||||
"upgradeRequiredDetail": "Tài khoản free chỉ có thể có 1 player config.",
|
||||
"limitSummary": "Đã đạt giới hạn cấu hình",
|
||||
"limitDetail": "Tài khoản free chỉ có thể có 1 player config.",
|
||||
"reconciliationSummary": "Hãy xóa bớt config",
|
||||
"reconciliationDetail": "Hãy xóa các player config dư cho đến khi chỉ còn 1 config để tiếp tục quản lý trên gói free.",
|
||||
"toggleDetail": "{name} đã được {state}.",
|
||||
"toggleDetail": "{{name}} đã được {state}.",
|
||||
"deletedSummary": "Đã xóa cấu hình",
|
||||
"deletedDetail": "Cấu hình trình phát đã được gỡ bỏ.",
|
||||
"failedSummary": "Thao tác thất bại",
|
||||
@@ -463,7 +473,7 @@
|
||||
"create": "Tạo"
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{name}\"?",
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{{name}}\"?",
|
||||
"deleteHeader": "Xóa mẫu",
|
||||
"deleteAccept": "Xóa",
|
||||
"deleteReject": "Hủy"
|
||||
@@ -484,10 +494,10 @@
|
||||
"enabledSummary": "Đã bật mẫu",
|
||||
"disabledSummary": "Đã tắt mẫu",
|
||||
"defaultUpdatedSummary": "Đã cập nhật mặc định",
|
||||
"defaultUpdatedDetail": "{name} hiện là mẫu mặc định cho video mới.",
|
||||
"defaultUpdatedDetail": "{{name}} hiện là mẫu mặc định cho video mới.",
|
||||
"upgradeRequiredSummary": "Cần nâng cấp gói",
|
||||
"upgradeRequiredDetail": "Hãy nâng cấp gói để quản lý Ads & VAST.",
|
||||
"toggleDetail": "{name} đã được {state}.",
|
||||
"toggleDetail": "{{name}} đã được {state}.",
|
||||
"deletedSummary": "Đã xóa mẫu",
|
||||
"deletedDetail": "Mẫu VAST đã được gỡ bỏ.",
|
||||
"copiedSummary": "Đã sao chép",
|
||||
@@ -496,6 +506,68 @@
|
||||
"failedDetail": "Không thể tải hoặc cập nhật mẫu VAST."
|
||||
}
|
||||
},
|
||||
"popupAds": {
|
||||
"createItem": "Thêm popup ad",
|
||||
"maxTriggersLabel": "Giới hạn trigger URL cao nhất mỗi phiên ({{count}})",
|
||||
"emptyTitle": "Chưa có popup ad",
|
||||
"emptySubtitle": "Tạo popup ad để bắt đầu mở URL hoặc inject script.",
|
||||
"types": {
|
||||
"url": "URL",
|
||||
"script": "Script"
|
||||
},
|
||||
"table": {
|
||||
"label": "Nhãn",
|
||||
"type": "Loại",
|
||||
"target": "Đích",
|
||||
"maxTriggersPerSession": "Số trigger tối đa/phiên"
|
||||
},
|
||||
"dialog": {
|
||||
"createTitle": "Tạo popup ad",
|
||||
"editTitle": "Sửa popup ad",
|
||||
"type": "Loại",
|
||||
"label": "Nhãn",
|
||||
"labelPlaceholder": "ví dụ: Ad Network 1",
|
||||
"url": "URL đích",
|
||||
"urlPlaceholder": "https://example.com/landing-page",
|
||||
"script": "Đoạn script",
|
||||
"scriptPlaceholder": "<script async src=\"//example.com/ad.js\"></script>",
|
||||
"maxTriggersPerSession": "Số lần popup tối đa mỗi phiên",
|
||||
"activeTitle": "Trạng thái mục",
|
||||
"activeDescription": "Tắt một mục để giữ nó trong bảng mà không phân phối nó.",
|
||||
"update": "Cập nhật",
|
||||
"create": "Tạo"
|
||||
},
|
||||
"info": {
|
||||
"urlTitle": "URL:",
|
||||
"urlDescription": "Mở tab mới khi người xem nhấp.",
|
||||
"scriptTitle": "Script:",
|
||||
"scriptDescription": "Inject script tag vào trang cho các mạng popup/popunder."
|
||||
},
|
||||
"confirm": {
|
||||
"deleteMessage": "Bạn có chắc muốn xóa \"{{name}}\"?",
|
||||
"deleteHeader": "Xóa popup ad",
|
||||
"deleteAccept": "Xóa",
|
||||
"deleteReject": "Hủy"
|
||||
},
|
||||
"toast": {
|
||||
"labelRequiredSummary": "Thiếu nhãn",
|
||||
"labelRequiredDetail": "Vui lòng nhập nhãn cho popup ad này.",
|
||||
"valueRequiredSummary": "Thiếu giá trị",
|
||||
"valueRequiredDetail": "Vui lòng nhập URL hoặc đoạn script.",
|
||||
"maxTriggersRequiredSummary": "Thiếu giới hạn trigger",
|
||||
"maxTriggersRequiredDetail": "Vui lòng nhập số trigger lớn hơn 0 cho popup ad loại URL.",
|
||||
"invalidUrlSummary": "URL không hợp lệ",
|
||||
"invalidUrlDetail": "Vui lòng nhập URL hợp lệ.",
|
||||
"createdSummary": "Đã tạo popup ad",
|
||||
"createdDetail": "Popup ad đã được thêm.",
|
||||
"updatedSummary": "Đã cập nhật popup ad",
|
||||
"updatedDetail": "Popup ad đã được cập nhật.",
|
||||
"deletedSummary": "Đã xóa popup ad",
|
||||
"deletedDetail": "Popup ad đã được gỡ bỏ.",
|
||||
"failedSummary": "Thao tác thất bại",
|
||||
"failedDetail": "Không thể tải hoặc cập nhật popup ads."
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Thông tin hồ sơ",
|
||||
"subtitle": "Quản lý thông tin cá nhân và chi tiết tài khoản của bạn.",
|
||||
@@ -503,7 +575,7 @@
|
||||
"username": "Tên người dùng",
|
||||
"email": "Địa chỉ email",
|
||||
"storageUsage": "Dung lượng sử dụng",
|
||||
"storageUsedOfLimit": "Đã dùng {{used}} trên {{limit}}",
|
||||
"storageUsedOfLimit": "Đã dùng {{used}}",
|
||||
"editProfile": "Chỉnh sửa hồ sơ",
|
||||
"changePassword": "Đổi mật khẩu"
|
||||
},
|
||||
@@ -534,9 +606,9 @@
|
||||
"processing": "Đang xử lý...",
|
||||
"upgrade": "Nâng cấp",
|
||||
"storage": "Dung lượng",
|
||||
"storageUsedOfLimit": "Đã dùng {{used}} trên {{limit}}",
|
||||
"storageUsedOfLimit": "Đã dùng {{used}}",
|
||||
"totalVideos": "Tổng video",
|
||||
"totalVideosUsedOfLimit": "{{used}} trên {{limit}} video",
|
||||
"totalVideosUsedOfLimit": "{{used}} video",
|
||||
"paymentHistory": "Lịch sử thanh toán",
|
||||
"paymentHistorySubtitle": "Các khoản thanh toán và hóa đơn trước đây của bạn",
|
||||
"noPaymentHistory": "Không tìm thấy lịch sử thanh toán.",
|
||||
@@ -551,9 +623,9 @@
|
||||
},
|
||||
"subscription": {
|
||||
"activeTitle": "Gói đang hoạt động",
|
||||
"activeDescription": " {{plan}} có hiệu lực đến {{date}}",
|
||||
"activeDescription": " Hiệu lực đến {{date}}",
|
||||
"expiringTitle": "Sắp hết hạn",
|
||||
"expiringDescription": " {{plan}} sẽ hết hạn vào {{date}}",
|
||||
"expiringDescription": "Hết hạn vào {{date}}",
|
||||
"expiredTitle": "Gói đã hết hạn",
|
||||
"expiredDescription": "Gói gần nhất của bạn đã kết thúc vào {{date}}",
|
||||
"freeTitle": "Gói miễn phí",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// return () => null;
|
||||
// });
|
||||
|
||||
import { ref, onMounted } from "vue";
|
||||
import { defineComponent, onMounted, ref } from "vue";
|
||||
const ClientOnly = defineComponent({
|
||||
name: "ClientOnly",
|
||||
setup(_p, { slots }) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Upload from "@/routes/upload/Upload.vue";
|
||||
import DashboardNav from "./DashboardNav.vue";
|
||||
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
||||
import PopupAdsRuntime from "./PopupAdsRuntime.vue";
|
||||
|
||||
</script>
|
||||
|
||||
@@ -22,5 +23,6 @@ import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
|
||||
</div>
|
||||
<GlobalUploadIndicator />
|
||||
<Upload />
|
||||
<PopupAdsRuntime />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import Bell from "@/components/icons/Bell.vue";
|
||||
import Home from "@/components/icons/Home.vue";
|
||||
import LayoutDashboard from "@/components/icons/LayoutDashboard.vue";
|
||||
import SettingsIcon from "@/components/icons/SettingsIcon.vue";
|
||||
import Video from "@/components/icons/Video.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useNotifications } from "@/composables/useNotifications";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed, createStaticVNode, ref } from "vue";
|
||||
import { computed, createStaticVNode, h, ref } from "vue";
|
||||
import NotificationDrawer from "./NotificationDrawer.vue";
|
||||
import Chart from "./icons/Chart.vue";
|
||||
|
||||
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
|
||||
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
|
||||
const notificationPopover = ref<InstanceType<typeof NotificationDrawer>>();
|
||||
const isNotificationOpen = ref(false);
|
||||
const { t } = useTranslation();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const isAdmin = computed(() => String(auth.user?.role || "").toLowerCase() === "admin");
|
||||
const notificationStore = useNotifications();
|
||||
const unreadCount = computed(() => notificationStore.unreadCount.value);
|
||||
|
||||
const handleNotificationClick = (event: Event) => {
|
||||
notificationPopover.value?.toggle(event);
|
||||
@@ -25,18 +25,41 @@ const handleNotificationClick = (event: Event) => {
|
||||
|
||||
const links = computed<Record<string, any>>(() => {
|
||||
const baseLinks = [
|
||||
{ href: "/#home", label: "app", icon: homeHoist, action: () => {}, className },
|
||||
{ href: "/", label: t("nav.overview"), icon: Home, action: null, className },
|
||||
{ href: "/videos", label: t("nav.videos"), icon: Video, action: null, className },
|
||||
{
|
||||
id: "home",
|
||||
href: "/#home", label: "app", icon: homeHoist, action: () => { }, className
|
||||
},
|
||||
{
|
||||
id: "overview",
|
||||
href: "/", label: t("nav.overview"), icon: Home, action: null, className
|
||||
},
|
||||
{
|
||||
id: "videos",
|
||||
href: "/videos", label: t("nav.videos"), icon: Video, action: null, className
|
||||
},
|
||||
{
|
||||
id: "analytics",
|
||||
href: "/analytics", label: t("nav.analytics"), icon: Chart, action: null, className
|
||||
},
|
||||
{
|
||||
id: "notification",
|
||||
href: "/notification",
|
||||
label: t("nav.notification"),
|
||||
icon: Bell,
|
||||
className,
|
||||
className: cn(
|
||||
className,
|
||||
isNotificationOpen.value && "bg-primary/15",
|
||||
),
|
||||
action: handleNotificationClick,
|
||||
isActive: isNotificationOpen,
|
||||
expandComponent: unreadCount.value > 0 ? () => h('span', {
|
||||
class: 'absolute -top-2 -right-2 min-w-4 h-4 text-xs font-bold text-white bg-red rounded-full flex items-center justify-center'
|
||||
}, [unreadCount.value > 9 ? '9+' : unreadCount.value]) : undefined
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
href: "/settings", label: t("nav.settings"), icon: SettingsIcon, action: null, className
|
||||
},
|
||||
{ href: "/settings", label: t("nav.settings"), icon: SettingsIcon, action: null, className },
|
||||
] as const;
|
||||
return baseLinks;
|
||||
});
|
||||
@@ -44,24 +67,18 @@ const links = computed<Record<string, any>>(() => {
|
||||
|
||||
<template>
|
||||
<header
|
||||
class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-header transition-all duration-300 ease-in-out w-18 items-center border-r border-border text-foreground/60"
|
||||
>
|
||||
class=":uno: fixed left-0 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen bg-header transition-all duration-300 ease-in-out w-18 items-center border-r border-border text-foreground/60">
|
||||
<template v-for="i in links" :key="i.href">
|
||||
<component
|
||||
:name="i.label"
|
||||
:is="i.action ? 'div' : 'router-link'"
|
||||
v-bind="i.action ? {} : { to: i.href }"
|
||||
@click="i.action && i.action($event)"
|
||||
:class="cn(
|
||||
<component :name="i.label" :is="i.action ? 'div' : 'router-link'" v-bind="i.action ? {} : { to: i.href }"
|
||||
@click="i.action && i.action($event)" :class="cn(
|
||||
i.className,
|
||||
($route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value) && 'bg-primary/15',
|
||||
)"
|
||||
>
|
||||
<component
|
||||
:is="i.icon"
|
||||
class="w-6 h-6 shrink-0"
|
||||
:filled="$route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value"
|
||||
/>
|
||||
($route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value) && 'bg-primary/15 text-primary',
|
||||
)">
|
||||
<div class="relative">
|
||||
<component :is="i.icon" class="w-6 h-6 shrink-0"
|
||||
:filled="$route.path === i.href || $route.path.startsWith(i.href + '/') || i.isActive?.value" />
|
||||
<component v-if="i.expandComponent" :is="i.expandComponent" />
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { useUploadQueue } from '@/composables/useUploadQueue';
|
||||
import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue';
|
||||
import { useUIState } from '@/stores/uiState';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNotifications } from '@/composables/useNotifications';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import BellOff from './icons/BellOff.vue';
|
||||
|
||||
const isMounted = ref(false);
|
||||
onMounted(() => {
|
||||
@@ -98,7 +99,7 @@ defineExpose({ toggle });
|
||||
</template>
|
||||
|
||||
<div v-else class="py-12 text-center">
|
||||
<span class="i-lucide-bell-off w-12 h-12 text-gray-300 mx-auto block mb-3"></span>
|
||||
<BellOff class="w-12 h-12 text-gray-300 mx-auto block mb-3" />
|
||||
<p class="text-gray-500 text-sm">{{ t('notification.empty.title') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
75
src/components/PopupAdsRuntime.vue
Normal file
75
src/components/PopupAdsRuntime.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import ClientOnly from '@/components/ClientOnly';
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
let activeItem: any | null = null;
|
||||
let clickHandler: ((event: MouseEvent) => void) | null = null;
|
||||
let scriptNode: HTMLScriptElement | null = null;
|
||||
let triggerCount = 0;
|
||||
|
||||
const triggerKey = (id: string) => `popup_ad_triggers:${id}`;
|
||||
|
||||
const cleanupScript = () => {
|
||||
if (scriptNode?.parentNode) {
|
||||
scriptNode.parentNode.removeChild(scriptNode);
|
||||
}
|
||||
scriptNode = null;
|
||||
};
|
||||
|
||||
const attachUrlHandler = () => {
|
||||
if (!activeItem?.id || typeof window === 'undefined') return;
|
||||
const maxTriggers = Number(activeItem.maxTriggersPerSession || 1);
|
||||
triggerCount = Number(sessionStorage.getItem(triggerKey(activeItem.id)) || '0');
|
||||
|
||||
clickHandler = () => {
|
||||
if (!activeItem?.value || triggerCount >= maxTriggers) return;
|
||||
triggerCount += 1;
|
||||
sessionStorage.setItem(triggerKey(activeItem.id), String(triggerCount));
|
||||
window.open(activeItem.value, '_blank', 'noopener,noreferrer');
|
||||
};
|
||||
|
||||
window.addEventListener('click', clickHandler, { capture: true });
|
||||
};
|
||||
|
||||
const attachScript = () => {
|
||||
if (!activeItem?.value || typeof document === 'undefined') return;
|
||||
cleanupScript();
|
||||
scriptNode = document.createElement('script');
|
||||
scriptNode.async = true;
|
||||
scriptNode.text = activeItem.value;
|
||||
document.body.appendChild(scriptNode);
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const response = await rpcClient.getActivePopupAd();
|
||||
activeItem = response.item || null;
|
||||
if (!activeItem?.isActive) return;
|
||||
|
||||
if (activeItem.type === 'script') {
|
||||
attachScript();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeItem.type === 'url') {
|
||||
attachUrlHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (clickHandler && typeof window !== 'undefined') {
|
||||
window.removeEventListener('click', clickHandler, { capture: true } as EventListenerOptions);
|
||||
}
|
||||
cleanupScript();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<span class="hidden" />
|
||||
</ClientOnly>
|
||||
</template>
|
||||
@@ -60,7 +60,7 @@ const getButtonClass = (variant?: string) => {
|
||||
<!-- Title & Actions -->
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 v-if="typeof props.title == 'string'" class="text-3xl font-bold text-gray-900 mb-1">{{ title }}</h1>
|
||||
<h1 v-if="typeof props.title == 'string'" class="text-2xl font-bold text-gray-900 mb-1">{{ title }}</h1>
|
||||
<component v-else :is="title" />
|
||||
<p v-if="description" class="text-gray-600">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="currentColor"/></svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 518"><path d="M234 124v256c58 3 113 25 156 63l47 41c9 8 23 10 34 5 12-5 19-16 19-29V44c0-13-7-24-19-29-11-5-25-3-34 5l-47 41c-43 38-98 60-156 63z" fill="var(--fill1)"/><path d="M138 124c-71 0-128 57-128 128s57 128 128 128v96c0 18 14 32 32 32h32c18 0 32-14 32-32V124h-96z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -242 500 516"><path d="M448-194v404l-26-24c-50-47-114-75-182-81V-89c68-6 132-34 182-81l26-24zM240 137c60 6 116 31 160 72l34 32c5 4 12 7 19 7 15 0 27-12 27-27v-425c0-16-12-28-27-28-7 0-14 3-19 8l-34 31c-50 47-116 73-185 73h-87C57-120 0-63 0 8c0 60 41 110 96 124v84c0 27 22 48 48 48h48c27 0 48-21 48-48v-79zm-40-1h8v80c0 9-7 16-16 16h-48c-9 0-16-7-16-16v-80h72zm0-224h8v192h-80c-53 0-96-43-96-96s43-96 96-96h72z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -5,9 +5,6 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 524"><path d="M10 448c0 36 30 66 67 66h427c36 0 66-30 66-66 0-12-3-23-8-33L353 47c-13-23-37-37-63-37s-50 14-63 37L19 415c-6 10-9 21-9 33zm301-46c0 12-9 21-21 21s-21-9-21-21 9-21 21-21 21 9 21 21zm-35-238c0-8 6-14 14-14s14 6 14 14v168c0 8-6 14-14 14s-14-6-14-14V164z" fill="color-mix(in srgb, currentColor 40%, transparent)"/><path d="M290 423c-12 0-21-9-21-21s9-21 21-21 21 9 21 21-9 21-21 21zm14-91c0 8-6 14-14 14s-14-6-14-14V164c0-8 6-14 14-14s14 6 14 14v168z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="531" height="488" viewBox="-13 -224 531 488"><path d="M253-106c9 0 18 8 18 18V56c0 10-9 18-18 18-10 0-18-8-18-18V-88c0-10 8-18 18-18zm0 279c14 0 27-12 27-27s-13-27-27-27c-15 0-27 12-27 27s12 27 27 27zm-63-350c12-23 36-37 63-37 26 0 50 14 62 37l180 324c13 22 13 50 0 72s-37 35-62 35H73c-26 0-50-13-63-35s-13-50 0-72l180-324zm63-1c-14 0-26 7-32 19L41 165c-6 11-6 24 0 35 7 11 19 18 32 18h360c12 0 24-7 31-18 6-11 6-24 0-35L284-159c-6-12-18-19-31-19z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 532"><path d="M10 391c0 19 16 35 36 35h376c20 0 36-16 36-35 0-9-3-16-8-23l-10-12c-30-37-46-84-46-132v-22c0-77-55-142-128-157v-3c0-18-14-32-32-32s-32 14-32 32v3C129 60 74 125 74 202v22c0 48-16 95-46 132l-10 12c-5 7-8 14-8 23z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M172 474c7 28 32 48 62 48s55-20 62-48H172z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 532"><path d="M10 391c0 19 16 35 36 35h376c20 0 36-16 36-35 0-9-3-16-8-23l-10-12c-30-37-46-84-46-132v-22c0-77-55-142-128-157v-3c0-18-14-32-32-32s-32 14-32 32v3C129 60 74 125 74 202v22c0 48-16 95-46 132l-10 12c-5 7-8 14-8 23z" fill="var(--fill1)"/><path d="M172 474c7 28 32 48 62 48s55-20 62-48H172z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -258 468 532">
|
||||
<path
|
||||
d="M224-248c-13 0-24 11-24 24v10C119-203 56-133 56-48v15C56 4 46 41 27 74L5 111c-3 6-5 13-5 19 0 21 17 38 38 38h372c21 0 38-17 38-38 0-6-2-13-5-19l-22-37c-19-33-29-70-29-108v-14c0-85-63-155-144-166v-10c0-13-11-24-24-24zm168 368H56l12-22c24-40 36-85 36-131v-15c0-66 54-120 120-120s120 54 120 120v15c0 46 12 91 36 131l12 22zm-236 96c10 28 37 48 68 48s58-20 68-48H156z"
|
||||
|
||||
7
src/components/icons/BellDot.vue
Normal file
7
src/components/icons/BellDot.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 660 535"><path d="M106 394c0 19 16 35 36 35h376c20 0 36-16 36-35 0-9-3-16-8-23l-10-12c-30-37-46-84-46-132v-22c0-77-55-142-128-157v-3c0-18-14-32-32-32s-32 14-32 32v3c-73 15-128 80-128 157v22c0 48-16 95-46 132l-10 12c-5 7-8 14-8 23z" fill="var(--fill1)"/><path d="M616 28c5 12 0 26-12 31l-56 24c-13 5-27 0-32-13-5-12 0-26 13-31l56-24c12-5 26 0 31 13zM10 197c0-13 11-24 24-24h64c13 0 24 11 24 24s-11 24-24 24H34c-13 0-24-11-24-24zm258 280h124c-7 28-32 48-62 48s-55-20-62-48zm294-304h64c13 0 24 11 24 24s-11 24-24 24h-64c-13 0-24-11-24-24s11-24 24-24zM57 59c-13-5-18-19-13-31 5-13 19-18 32-13l56 24c12 5 17 19 12 31-5 13-19 18-31 13L57 59z" fill="var(--fill4)"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else width="660" height="535" viewBox="-10 -261 660 535"><path d="M606-233c-5-13-19-18-31-13l-56 24c-13 5-18 19-13 31 5 13 19 18 31 13l56-24c13-5 18-19 13-31zm-286-15c-13 0-24 11-24 24v10c-81 11-144 81-144 166v15c0 37-10 74-29 107l-22 37c-3 6-5 13-5 19 0 21 17 38 38 38h372c21 0 38-17 38-38 0-6-2-13-5-19l-22-37c-19-33-29-70-29-108v-14c0-85-63-155-144-166v-10c0-13-11-24-24-24zm168 368H152l12-22c24-40 36-85 36-131v-15c0-66 54-120 120-120s120 54 120 120v15c0 46 12 91 36 131l12 22zm-236 96c10 28 37 48 68 48s58-20 68-48H252zM0-64c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24H24C11-88 0-77 0-64zm552-24c-13 0-24 11-24 24s11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64zM47-202l56 24c12 5 26 0 31-12 5-13 0-27-13-32l-56-24c-12-5-26 0-31 13-5 12 0 26 13 31z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
filled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
</template>
|
||||
7
src/components/icons/BellOff.vue
Normal file
7
src/components/icons/BellOff.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600"><path d="M76 425c0 19 16 35 36 35h246L140 242v16c0 48-16 94-46 132l-10 12c-5 7-8 14-8 22zm0 0zm162 83c7 28 32 48 62 48s55-20 62-48H238z" fill="var(--fill1)"/><path d="M19 19c9-9 25-9 34 0l120 120c23-30 56-52 95-60v-3c0-18 14-32 32-32s32 14 32 32v3c73 15 128 80 128 157v22c0 48 16 95 46 132l10 12c5 7 8 14 8 23 0 17-13 32-30 35l87 87c9 10 9 25 0 34s-25 9-34 0L19 53c-9-9-9-25 0-34z" fill="var(--fill4)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="600" height="601" viewBox="-12 -292 600 601"><path d="M41-273c-9-9-25-9-34 0s-9 25 0 34l528 528c9 10 25 10 34 0 9-9 9-24 0-34l-88-88c18-3 31-18 31-37 0-6-2-13-5-19l-22-37c-19-33-29-70-29-108v-14c0-85-63-155-144-166v-10c0-13-11-24-24-24s-24 11-24 24v10c-42 6-79 27-105 59L41-273zm152 152c22-29 56-47 95-47 66 0 120 54 120 120v15c0 46 12 91 36 131l12 22h-22L193-121zM133 98c19-33 31-71 34-109l-47-47v25c0 37-10 74-29 107l-22 37c-3 6-5 13-5 19 0 21 17 38 38 38h244l-48-48H120l13-22zm87 118c10 28 37 48 68 48s58-20 68-48H220z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 580 524"><path d="M10 234v112c0 46 38 84 84 84s84-38 84-84V234c0-46-38-84-84-84s-84 38-84 84zM206 94v252c0 46 38 84 84 84s84-38 84-84V94c0-46-38-84-84-84s-84 38-84 84zm196 56v196c0 46 38 84 84 84s84-38 84-84V150c0-46-38-84-84-84s-84 38-84 84z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M10 500c0-8 6-14 14-14h532c8 0 14 6 14 14s-6 14-14 14H24c-8 0-14-6-14-14z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 532 468"><path d="M272-184c9 0 16 7 16 16v352c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-352c0-9 7-16 16-16h32zm-32-32c-26 0-48 22-48 48v352c0 27 22 48 48 48h32c27 0 48-21 48-48v-352c0-26-21-48-48-48h-32zM80 8c9 0 16 7 16 16v160c0 9-7 16-16 16H48c-9 0-16-7-16-16V24c0-9 7-16 16-16h32zM48-24C22-24 0-2 0 24v160c0 27 22 48 48 48h32c27 0 48-21 48-48V24c0-26-21-48-48-48H48zm384-96h32c9 0 16 7 16 16v288c0 9-7 16-16 16h-32c-9 0-16-7-16-16v-288c0-9 7-16 16-16zm-48 16v288c0 27 22 48 48 48h32c27 0 48-21 48-48v-288c0-26-21-48-48-48h-32c-26 0-48 22-48 48z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 600 564"><path d="M21 254c11 14 31 16 45 5l141-112 108 81c12 8 28 8 39-1l140-112v39c0 18 14 32 32 32s32-14 32-32V42c0-18-14-32-32-32H414c-18 0-32 14-32 32s14 32 32 32h29l-110 88-108-82c-11-8-28-8-39 1L26 209c-14 11-16 31-5 45zm25 108v96c0 18 14 32 32 32s32-14 32-32v-96c0-18-14-32-32-32s-32 14-32 32zm128-96v192c0 18 14 32 32 32s32-14 32-32V266c0-18-14-32-32-32s-32 14-32 32z" fill="var(--fill1)"/><path d="M446 554c80 0 144-64 144-144s-64-144-144-144-144 64-144 144 64 144 144 144zm0-240c9 0 16 7 16 16v8h16c9 0 16 7 16 16s-7 16-16 16h-46c-5 0-10 5-10 10s4 9 8 10l45 8c20 4 35 22 35 42 0 23-19 42-42 42h-6v8c0 9-7 16-16 16s-16-7-16-16v-8h-16c-9 0-16-7-16-16s7-16 16-16h54c5 0 10-4 10-10 0-5-4-9-8-10l-45-8c-20-4-35-21-35-42 0-22 18-41 40-42v-8c0-9 7-16 16-16z" fill="var(--fill4)"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="0 0 599 564"><path d="M421 10c-13 0-24 11-24 24s11 24 24 24h53L333 178 221 80c-8-7-20-8-29-2L24 190c-11 7-14 22-7 33s22 14 33 7l153-102 114 100c9 8 23 8 32 0L509 91v55c0 13 11 24 24 24s24-11 24-24V34c0-13-11-24-24-24H421zM205 234c-13 0-24 11-24 24v208c0 13 11 24 24 24s24-11 24-24V258c0-13-11-24-24-24zM69 330c-13 0-24 11-24 24v112c0 13 11 24 24 24s24-11 24-24V354c0-13-11-24-24-24zm376 224c80 0 144-64 144-144s-64-144-144-144-144 64-144 144 64 144 144 144zm0-240c9 0 16 7 16 16v8h16c9 0 16 7 16 16s-7 16-16 16h-46c-5 0-10 5-10 10s4 9 8 10l45 8c20 4 35 22 35 42 0 23-19 42-42 42h-6v8c0 9-7 16-16 16s-16-7-16-16v-8h-16c-9 0-16-7-16-16s7-16 16-16h54c5 0 10-4 10-10 0-5-4-9-8-10l-45-8c-20-4-35-21-35-42 0-22 18-41 40-42v-8c0-9 7-16 16-16z" fill="var(--fill4)"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
|
||||
@@ -5,6 +5,6 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 532"><path d="M26 427c0-121 70-194 157-273h134c87 79 157 152 157 273 0 44-35 79-79 79H105c-44 0-79-35-79-79zM138 42c0-9 7-16 16-16h192c9 0 16 7 16 16 0 3 0 5-2 8l-46 88H187l-47-88c-1-3-2-5-2-8zm56 267c0 21 15 38 36 42l38 6c13 2 22 13 22 26 0 15-12 27-27 27h-53c-4 0-8 4-8 8s4 8 8 8h32v16c0 4 4 8 8 8s8-4 8-8v-16h5c24 0 43-19 43-43 0-20-15-38-36-42l-38-6c-12-2-22-13-22-26 0-15 12-27 27-27h45c4 0 8-3 8-8 0-4-4-8-8-8h-24v-16c0-4-4-8-8-8s-8 4-8 8v16h-5c-24 0-43 19-43 43z" fill="#a6acb9"/><path d="M346 26c9 0 16 7 16 16 0 3-1 5-2 8l-46 88H187l-47-88c-1-3-2-5-2-8 0-9 7-16 16-16h192zM126 57l45 86C85 222 10 299 10 427c0 52 43 95 95 95h290c52 0 95-43 95-95 0-128-75-205-161-284l45-86c3-5 4-10 4-15 0-18-14-32-32-32H154c-18 0-32 14-32 32 0 5 1 10 4 15zM26 427c0-121 70-194 157-273h134c87 79 157 152 157 273 0 44-35 79-79 79H105c-44 0-79-35-79-79zm224-185c-4 0-8 4-8 8v16h-5c-24 0-43 19-43 43 0 20 15 38 36 42l38 6c13 2 22 13 22 26 0 15-12 27-27 27h-53c-4 0-8 4-8 8s4 8 8 8h32v16c0 4 4 8 8 8s8-4 8-8v-16h5c24 0 43-19 43-43 0-20-15-38-36-42l-38-6c-12-2-22-13-22-26 0-15 12-27 27-27h45c4 0 8-3 8-8 0-4-4-8-8-8h-24v-16c0-4-4-8-8-8z" fill="currentColor"/></svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 532"><path d="M10 435c0 48 39 87 88 87h305c48 0 87-39 87-87 0-87-38-169-105-224l-48-41H164l-49 41C48 266 10 348 10 435zM138 36c0 4 1 8 3 12l37 74h144l37-74c2-4 3-8 3-12 0-14-12-26-26-26H164c-14 0-26 12-26 26zm44 275c0-29 23-53 52-53v-4c0-11 9-20 20-20s20 9 20 20v4h8c11 0 20 9 20 20s-9 20-20 20h-47c-7 0-13 6-13 13 0 6 4 11 10 12l42 7c25 4 44 26 44 52s-19 47-44 51v5c0 11-9 20-20 20s-20-9-20-20v-4h-24c-11 0-20-9-20-20s9-20 20-20h56c6 0 12-5 12-12 0-6-4-12-10-13l-42-7c-25-4-44-26-44-51z" fill="var(--fill1)"/><path d="M162 122c-13 0-24 11-24 24s11 24 24 24h176c13 0 24-11 24-24s-11-24-24-24H162zm92 112c-11 0-20 9-20 20v4c-29 0-52 24-52 53 0 25 19 47 44 51l42 7c6 1 10 7 10 13 0 7-6 12-12 12h-56c-11 0-20 9-20 20s9 20 20 20h24v4c0 11 9 20 20 20s20-9 20-20v-5c25-4 44-25 44-51s-18-48-44-52l-42-7c-6-1-10-6-10-13 0-6 6-12 13-12h47c11 0 20-9 20-20s-9-20-20-20h-8v-4c0-11-9-20-20-20z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="532" viewBox="6 -258 500 532"><path d="m379-191-46 81c84 77 163 154 163 279 0 52-43 95-95 95H111c-52 0-95-43-95-95C16 44 96-33 179-110l-46-81c-3-6-5-12-5-19 0-21 17-38 38-38h180c21 0 38 17 38 38 0 7-2 13-5 19zM227-88l-1 1C134-4 64 61 64 169c0 26 21 47 47 47h290c26 0 47-21 47-47C448 61 378-4 286-87l-1-1h-58zm-7-48h72l37-64H183l37 64zm40 96c11 0 20 9 20 20v4h8c11 0 20 9 20 20s-9 20-20 20h-47c-7 0-13 6-13 13 0 6 4 11 10 12l42 7c25 4 44 26 44 52s-19 47-44 51v5c0 11-9 20-20 20s-20-9-20-20v-4h-24c-11 0-20-9-20-20s9-20 20-20h56c6 0 12-5 12-12 0-6-4-12-10-13l-42-7c-25-4-44-26-44-51 0-29 23-53 52-53v-4c0-11 9-20 20-20z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M10 106h512v64H10zm0 0z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v32H10V74zm0 96h512v160c0 35-29 64-64 64H74c-35 0-64-29-64-64V170zm64 136c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24H98c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="var(--fill1)"/><path d="M10 106h512v64H10zm0 0z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -194 532 404"><path d="M448-136c9 0 16 7 16 16v32H48v-32c0-9 7-16 16-16h384zm16 112v160c0 9-7 16-16 16H64c-9 0-16-7-16-16V-24h416zM64-184c-35 0-64 29-64 64v256c0 35 29 64 64 64h384c35 0 64-29 64-64v-256c0-35-29-64-64-64H64zM80 96c0 13 11 24 24 24h48c13 0 24-11 24-24s-11-24-24-24h-48c-13 0-24 11-24 24zm144 0c0 13 11 24 24 24h64c13 0 24-11 24-24s-11-24-24-24h-64c-13 0-24 11-24 24z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -5,5 +5,6 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="524" height="524" viewBox="-10 -242 524 524"><path d="M252-232C113-232 0-119 0 20s113 252 252 252S504 159 504 20 391-232 252-232zM37 2c7-92 73-168 161-191-42 55-68 122-71 191H37zm0 36h89c4 69 30 136 71 191-87-23-153-98-160-191zm213 198c-50-52-83-125-87-198h179c-5 73-37 146-88 198h-4zM378 38h89c-7 92-73 168-161 191 42-55 68-122 71-191zm0 0zm0-36c-4-69-30-136-71-191 87 23 153 99 160 191h-89zM254-196c51 53 83 125 87 198H163c4-73 36-145 87-198h4z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 627 563"><path d="M10 241h112c5-88 35-169 71-222C94 49 20 135 10 241zm0 48c10 106 84 193 183 222-36-52-66-134-71-222H10zm160-48h190c-4-62-22-121-45-165-13-25-27-44-38-56-6-5-10-8-12-10-2 2-6 5-12 10-11 12-25 31-38 56-23 44-41 103-45 165zm0 48c4 62 22 121 45 166 13 25 27 43 38 55 6 5 10 8 12 10 2-2 6-5 12-10 6-6 13-15 20-25-10-23-16-49-16-76 0-45 16-87 42-120H170zM337 19c34 50 64 126 70 210 21-8 43-12 66-12 15 0 30 2 44 5-16-97-87-175-180-203z" fill="var(--fill1)"/><path d="M473 553c80 0 144-64 144-144s-64-144-144-144-144 64-144 144 64 144 144 144zm87-145c-19-28-51-47-87-47s-68 19-87 47l-25-19c24-36 65-60 112-60s88 24 113 60l-26 19zm-23 17-26 19c-8-11-22-19-38-19s-30 8-38 19l-26-19c15-19 38-32 64-32s49 13 64 32zm-84 48c0-11 9-20 20-20s20 9 20 20-9 20-20 20-20-9-20-20z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -258 628 564"><path d="M288 232c-1 0-7-1-18-12-12-11-24-28-36-49-22-39-39-92-41-147h160c17-19 38-35 62-46-7-76-37-145-70-187 81 22 144 87 162 169 12 1 23 3 34 5-21-121-126-213-253-213C147-248 32-133 32 8s115 256 256 256c17 0 33-2 49-5-10-14-18-30-23-47-3 3-5 6-7 8-12 11-18 12-19 12zM384-8H192c3-55 20-107 42-147 12-21 24-38 35-49 12-10 18-12 19-12s7 2 18 12c12 11 24 28 36 49 22 40 39 92 41 147zm0 0zM160-8H65c6-97 75-177 166-201-35 44-67 120-71 201zM65 24h95c4 82 36 157 71 201C140 201 71 121 65 24zm431 16c62 0 112 50 112 112s-50 112-112 112-112-50-112-112S434 40 496 40zm0 256c80 0 144-64 144-144S576 8 496 8 352 72 352 152s64 144 144 144zm96-165c-24-26-58-43-96-43s-72 17-96 43l25 21c17-20 43-32 71-32s54 12 71 32l25-21zm-96 13c-21 0-41 8-55 22l25 21c8-7 19-11 30-11 12 0 22 4 30 11l25-21c-14-14-34-22-55-22zm0 92c11 0 20-9 20-20s-9-20-20-20-20 9-20 20 9 20 20 20z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 539 535">
|
||||
<path d="M61 281c2-1 4-3 6-5L269 89l202 187c2 2 4 4 6 5v180c0 35-29 64-64 64H125c-35 0-64-29-64-64V281z"
|
||||
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||
fill="var(--fill1)" />
|
||||
<path
|
||||
d="M247 22c13-12 32-12 44 0l224 208c13 12 13 32 1 45s-32 14-45 2L269 89 67 276c-13 12-33 12-45-1s-12-33 1-45L247 22z"
|
||||
fill="var(--colors-primary-DEFAULT)" />
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-11 -259 535 533">
|
||||
<path
|
||||
|
||||
7
src/components/icons/Inbox.vue
Normal file
7
src/components/icons/Inbox.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 468"><path d="M10 268v126c0 35 29 64 64 64h384c35 0 64-29 64-64V268c0-3 0-6-1-9L494 65c-5-32-32-55-64-55H102c-32 0-59 23-64 55L11 259c-1 3-1 6-1 9zm64-2 28-192h328l28 192h-60c-12 0-23 7-29 18l-14 28c-6 11-17 18-29 18H206c-12 0-23-7-29-18l-14-28c-5-11-17-18-29-18H74z" fill="var(--fill1)"/><path d="M249 291c9 9 25 9 34 0l64-64c9-9 9-25 0-34s-25-9-34 0l-23 23v-86c0-13-11-24-24-24s-24 11-24 24v86l-23-23c-9-9-25-9-34 0-9 10-9 25 0 34l64 64z" fill="var(--fill4)"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else width="532" height="468" viewBox="-10 -226 532 468"><path d="M98-184c-16 0-30 12-32 27L35 56h100c11 0 21 5 27 14l23 34h142l23-34c6-9 16-14 27-14h100l-31-213c-2-15-16-27-31-27H98zM32 168c0 18 14 32 32 32h384c18 0 32-14 32-32V88H377l-23 34c-6 9-16 14-26 14H185c-11 0-21-5-27-14l-23-34H32v80zm2-329c5-32 32-55 64-55h317c31 0 58 23 63 55l33 227c1 3 1 6 1 10v92c0 35-29 64-64 64H64c-35 0-64-29-64-64V76c0-4 0-7 1-10l33-227zM339-21l-72 72c-6 7-16 7-22 0l-72-72c-6-6-6-16 0-22s16-6 22 0l45 44v-121c0-9 7-16 16-16s16 7 16 16V1l45-44c6-6 16-6 22 0 7 6 7 16 0 22z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
7
src/components/icons/LanguageIcon.vue
Normal file
7
src/components/icons/LanguageIcon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 536"><path d="M269 477c58-131 80-180 128-288 5-12 16-19 29-19s24 7 29 19l128 288c7 16 0 35-16 42s-35 0-42-16l-20-45H347l-20 45c-7 16-26 23-42 16s-23-26-16-42zm107-83h100l-50-113-50 113z" fill="var(--fill1)"/><path d="M170 10c18 0 32 14 32 32v32h128c18 0 32 14 32 32s-14 32-32 32h-10l-8 23c-16 45-41 87-72 122 14 9 29 17 44 24l51 22-26 59-51-23c-23-10-45-22-66-36-21 17-44 32-69 44l-35 18c-15 8-35 1-43-15-7-15-1-35 15-43l34-17c17-8 32-18 47-28-14-13-27-27-39-41l-21-24c-11-14-9-34 5-46 13-11 33-9 45 5l20 24c11 14 24 27 37 39 28-31 50-66 64-106v-1H42c-18 0-32-14-32-32s14-32 32-32h96V42c0-18 14-32 32-32z" fill="var(--fill3)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 599 535"><path d="M178 10c13 0 24 11 24 24v56h136c13 0 24 11 24 24s-11 24-24 24h-16l-17 38c-18 44-45 83-78 116 14 10 29 18 44 25l61 28 72-161c4-8 13-14 22-14 10 0 18 6 22 14l136 304c5 12 0 27-12 32s-26 0-32-12l-29-66H341l-29 66c-5 12-20 17-32 12s-17-20-12-32l45-99-61-29c-22-9-42-21-61-35-18 14-37 26-57 36l-57 30c-12 7-26 2-32-10-6-11-2-26 10-32l57-30c14-7 27-16 40-25-27-26-51-55-70-88-6-11-3-26 9-33 11-6 26-2 33 9 17 30 39 58 65 81 31-30 55-66 72-106l9-19H34c-13 0-24-11-24-24s11-24 24-24h120V34c0-13 11-24 24-24zm311 384-63-141-63 141h126z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
7
src/components/icons/ListIcon.vue
Normal file
7
src/components/icons/ListIcon.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 468"><path d="M10 170v224c0 35 29 64 64 64h320c35 0 64-29 64-64V170H10zm96 88c0-13 11-24 24-24h208c13 0 24 11 24 24s-11 24-24 24H130c-13 0-24-11-24-24zm0 112c0-13 11-24 24-24h208c13 0 24 11 24 24s-11 24-24 24H130c-13 0-24-11-24-24z" fill="var(--fill1)"/><path d="M74 10c-35 0-64 29-64 64v96h448V74c0-35-29-64-64-64H74zm240 48h64c7 0 12 4 15 10 2 6 1 13-4 17l-32 32c-6 7-16 7-22 0l-32-32c-5-4-6-11-4-17 3-6 9-10 15-10z" fill="currentColor"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="-10 -226 468 468"><path d="M64-184h320c18 0 32 14 32 32v64H32v-64c0-18 14-32 32-32zM32-56h384v224c0 18-14 32-32 32H64c-18 0-32-14-32-32V-56zM0-152v320c0 35 29 64 64 64h320c35 0 64-29 64-64v-320c0-35-29-64-64-64H64c-35 0-64 29-64 64zM112 8c-9 0-16 7-16 16s7 16 16 16h224c9 0 16-7 16-16s-7-16-16-16H112zm0 96c-9 0-16 7-16 16s7 16 16 16h224c9 0 16-7 16-16s-7-16-16-16H112zm200-260c-5 0-9 3-11 7-2 5-1 10 3 14l24 24c4 4 12 4 17 0l24-24c3-4 4-9 2-14-2-4-6-7-11-7h-48z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -5,7 +5,8 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 532 404"><path d="M10 58c0 15 7 29 19 38l208 156c17 13 41 13 58 0L503 96c12-9 19-23 19-38v272c0 35-29 64-64 64H74c-35 0-64-29-64-64V58z" fill="var(--fill1)"/><path d="M58 10c-26 0-48 22-48 48 0 15 7 29 19 38l208 156c17 13 41 13 58 0L503 96c12-9 19-23 19-38 0-26-21-48-48-48H58z" fill="var(--fill4)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
|
||||
10
src/components/icons/MoneyCheck.vue
Normal file
10
src/components/icons/MoneyCheck.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
filled?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 610 500"><path d="M10 74c0-35 29-64 64-64h384c35 0 64 29 64 64v73c-21 3-41 13-58 29l-58 58H306c-13 0-24 11-24 24s11 24 24 24h52l-64 64c-13 14-23 30-28 48H74c-35 0-64-29-64-64V74zm76 93c0 25 19 47 44 51l42 7c6 1 10 7 10 13 0 7-5 12-12 12h-56c-11 0-20 9-20 20s9 20 20 20h24v4c0 11 9 20 20 20s20-9 20-20v-5c25-4 44-25 44-51s-18-48-44-52l-41-7c-6-1-11-6-11-12 0-7 6-13 13-13h47c11 0 20-9 20-20s-9-20-20-20h-8v-4c0-11-9-20-20-20s-20 9-20 20v4c-29 0-52 24-52 53zm196-21c0 13 11 24 24 24h128c13 0 24-11 24-24s-11-24-24-24H306c-13 0-24 11-24 24z" fill="var(--fill1)"/><path d="m298 473 12-60c3-12 9-24 18-33l119-119 80 80-119 119c-9 9-20 15-33 18l-59 12h-3c-8 0-15-7-15-15v-3zm0 0zm251-154-80-80 29-29c22-22 58-22 80 0s22 58 0 80l-29 29z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 607 500"><path d="M74 42h384c18 0 32 14 32 32v81c11-3 22-5 32-5V74c0-35-28-64-64-64H74c-35 0-64 29-64 64v256c0 35 29 64 64 64h189l1-4c1-9 4-19 8-28H74c-17 0-32-14-32-32V74c0-18 15-32 32-32zm240 192c-8 0-16 7-16 16s8 16 16 16h44l32-32h-76zm-16-80c0 9 8 16 16 16h96c9 0 16-7 16-16s-7-16-16-16h-96c-8 0-16 7-16 16zM170 98c-8 0-16 7-16 16v8h-1c-26 0-47 21-47 46 0 23 17 42 39 46l45 8c7 1 12 7 12 14 0 8-6 14-14 14h-58c-8 0-16 7-16 16s8 16 16 16h24v8c0 9 8 16 16 16 9 0 16-7 16-16v-8h2c26 0 46-21 46-46 0-23-16-42-38-46l-46-8c-6-1-12-7-12-14 0-8 7-14 15-14h49c9 0 16-7 16-16s-7-16-16-16h-16v-8c0-9-7-16-16-16zm182 288 102-102 50 51-102 102c-4 5-11 8-17 9l-51 8 9-51c1-6 4-12 8-17zm0 0zm124-125 21-20c14-14 37-14 51 0s14 36 0 50l-21 21-51-51zM311 398l-12 75c0 1-1 1-1 2 0 8 7 15 15 15h3l74-13c13-2 26-8 35-17l145-146c27-26 27-69 0-96-26-26-69-26-96 0L329 364c-9 9-16 21-18 34z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"
|
||||
stroke="none">
|
||||
<path
|
||||
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
|
||||
</svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 534 533"><path d="m303 93 104 104 34 34 62-62c14-13 21-32 21-51s-7-38-21-51l-36-36c-13-13-32-21-51-21s-38 8-51 21l-62 62z" fill="#a6acb9"/><path d="m95 377-24 87 86-25c7-1 13-5 18-10l-71-69c-4 5-8 10-9 17zm174-250 34-34 104 104 34 34-34 34-198 198c-11 11-24 19-39 23L42 521c-8 2-17 0-23-6s-9-15-6-23l35-128c5-15 12-28 23-39l198-198z" fill="#1e3050"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="534" height="533" viewBox="-12 -258 534 533"><path d="M404-216c-11 0-21 4-28 12l-59 58 93 93 58-58c8-8 12-18 12-29s-4-21-12-28l-35-36c-8-8-18-12-29-12zM93 78l93 93L387-30l-93-93L93 78zm-21 25c-2 3-4 7-5 11L36 229l114-32c4-1 8-3 11-5l-89-89zm281-330c13-13 32-21 51-21s38 8 51 21l36 36c13 13 21 32 21 51s-8 38-21 51L197 205c-11 11-24 19-39 23L30 263c-8 2-17 0-23-6s-9-15-6-23l35-128c5-15 12-28 23-39l294-294z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -5,7 +5,8 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 628 628"><path d="M286 343 618 10 394 618 286 343z" fill="var(--fill1)"/><path d="M618 10 10 234l276 109L618 10z" fill="var(--fill4)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m22 2-7 20-4-9-9-4Z" />
|
||||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 580"><path d="M18 190c-8 14-6 32 5 43l37 36v42l-37 36c-11 12-13 29-5 43l46 80c8 14 24 21 40 17l50-14c11 8 23 15 36 21l13 50c4 15 18 26 34 26h93c16 0 30-11 34-26l13-50c13-6 25-13 36-21l50 14c15 4 32-3 40-17l46-80c8-14 6-31-6-43l-37-36c1-7 1-14 1-21s0-14-1-21l37-36c12-11 14-29 6-43l-46-80c-8-14-24-21-40-17l-50 14c-11-8-23-15-36-21l-13-50c-4-15-18-26-34-26h-93c-16 0-30 11-34 26l-13 50c-13 6-25 13-36 21l-50-13c-16-5-32 2-40 16l-46 80zm377 100c1 41-20 79-55 99-35 21-79 21-114 0-35-20-56-58-54-99-2-41 19-79 54-99 35-21 79-21 114 0 35 20 56 58 55 99zm-195 0c-2 31 14 59 40 75 27 15 59 15 86 0 26-16 42-44 41-75 1-31-15-59-41-75-27-15-59-15-86 0-26 16-42 44-40 75z" fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)"/><path d="M283 206c46 0 84 37 84 84 0 46-37 84-83 84-47 0-85-37-85-84 0-46 37-84 84-84zm1 196c61 0 111-51 111-112 0-62-51-112-112-112-62 0-112 51-112 112 0 62 51 112 113 112z" fill="var(--colors-primary-DEFAULT)"/></svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 567 580"><path d="M18 190c-8 14-6 32 5 43l37 36v42l-37 36c-11 12-13 29-5 43l46 80c8 14 24 21 40 17l50-14c11 8 23 15 36 21l13 50c4 15 18 26 34 26h93c16 0 30-11 34-26l13-50c13-6 25-13 36-21l50 14c15 4 32-3 40-17l46-80c8-14 6-31-6-43l-37-36c1-7 1-14 1-21s0-14-1-21l37-36c12-11 14-29 6-43l-46-80c-8-14-24-21-40-17l-50 14c-11-8-23-15-36-21l-13-50c-4-15-18-26-34-26h-93c-16 0-30 11-34 26l-13 50c-13 6-25 13-36 21l-50-13c-16-5-32 2-40 16l-46 80zm377 100c1 41-20 79-55 99-35 21-79 21-114 0-35-20-56-58-54-99-2-41 19-79 54-99 35-21 79-21 114 0 35 20 56 58 55 99zm-195 0c-2 31 14 59 40 75 27 15 59 15 86 0 26-16 42-44 41-75 1-31-15-59-41-75-27-15-59-15-86 0-26 16-42 44-40 75z" fill="var(--fill1)"/><path d="M283 206c46 0 84 37 84 84 0 46-37 84-83 84-47 0-85-37-85-84 0-46 37-84 84-84zm1 196c61 0 111-51 111-112 0-62-51-112-112-112-62 0-112 51-112 112 0 62 51 112 113 112z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"
|
||||
stroke="none">
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||
</svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 548"><path d="M42 122v352c0 35 29 64 64 64h256c35 0 64-29 64-64V122H42zm64 88c0-13 11-24 24-24s24 11 24 24v240c0 13-11 24-24 24s-24-11-24-24V210zm104 0c0-13 11-24 24-24s24 11 24 24v240c0 13-11 24-24 24s-24-11-24-24V210zm104 0c0-13 11-24 24-24s24 11 24 24v240c0 13-11 24-24 24s-24-11-24-24V210z" fill="color-mix(in srgb, var(--colors-danger-DEFAULT) 40%, transparent)"/><path d="M177 10c-14 0-26 9-30 22l-9 26H42c-18 0-32 14-32 32s14 32 32 32h384c18 0 32-14 32-32s-14-32-32-32h-96l-9-26c-4-13-16-22-30-22H177z" fill="var(--colors-danger-DEFAULT)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" class="min-w-[28px]" viewBox="0 0 596 468">
|
||||
<path
|
||||
d="M10 314c0-63 41-117 98-136-1-8-2-16-2-24 0-79 65-144 144-144 55 0 104 31 128 77 14-8 30-13 48-13 53 0 96 43 96 96 0 16-4 31-10 44 44 20 74 64 74 116 0 71-57 128-128 128H154c-79 0-144-64-144-144zm199-73c-9 9-9 25 0 34s25 9 34 0l31-31v102c0 13 11 24 24 24s24-11 24-24V244l31 31c9 9 25 9 34 0s9-25 0-34l-72-72c-10-9-25-9-34 0l-72 72z"
|
||||
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||
fill="var(--fill1)" />
|
||||
<path
|
||||
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
|
||||
fill="var(--colors-primary-DEFAULT)" />
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else class="min-w-[28px]" viewBox="-10 -226 596 468">
|
||||
<path
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-if="filled" viewBox="0 0 532 404">
|
||||
<path d="M10 74v256c0 35 29 64 64 64h256c35 0 64-29 64-64V74c0-35-29-64-64-64H74c-35 0-64 29-64 64z"
|
||||
fill="color-mix(in srgb, var(--colors-primary-DEFAULT) 40%, transparent)" />
|
||||
fill="var(--fill1)" />
|
||||
<path d="M394 135v134l90 72c4 3 9 5 14 5 13 0 24-11 24-24V82c0-13-11-24-24-24-5 0-10 2-14 5l-90 72z"
|
||||
fill="var(--colors-primary-DEFAULT)" />
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" v-else viewBox="22 -194 564 404">
|
||||
<path
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M42 170h241c-40 35-65 87-65 144 0 17 2 33 6 48H74c-18 0-32-14-32-32V170z" fill="#a6acb9"/><path d="M458 42H345l-96 96h84c-18 8-35 19-50 32H42v160c0 18 14 32 32 32h150c3 11 7 22 11 32H74c-35 0-64-29-64-64V74c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3V74c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM43 138l96-96H74c-18 0-32 14-32 32v64h1zm46 0h114l96-96H185l-96 96zm321 288c62 0 112-50 112-112s-50-112-112-112-112 50-112 112 50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144 64-144 144-144zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V258c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 564 468"><path d="M10 74v256c0 35 29 64 64 64h161c-11-24-17-51-17-80 0-106 86-192 192-192 42 0 81 13 112 36V74c0-15-5-29-14-40l-46 46c-16-4-34-6-52-6h-10l64-64h-92l-1 1-95 95h-68l96-96h-92l-1 1-95 95H48l96-96H74c-35 0-64 29-64 64z" fill="var(--fill1)"/><path d="M266 314c0-80 64-144 144-144s144 64 144 144-64 144-144 144-144-64-144-144zm104-62c-5 3-8 8-8 14v96c0 6 3 11 8 14s11 3 16 0l80-48c5-3 8-8 8-14s-3-11-8-14l-80-48c-5-3-11-3-16 0z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="564" height="468" viewBox="22 -194 564 468"><path d="M480-152H367l-96 96h84c-18 8-35 19-50 32H64v160c0 18 14 32 32 32h150c3 11 7 22 11 32H96c-35 0-64-29-64-64v-256c0-35 29-64 64-64h384c35 0 64 29 64 64v84c-11-8-23-15-35-20h3v-64c0-5-1-10-3-14l-63 63c-5-1-9-1-14-1-11 0-23 1-33 3l82-83h-1zM65-56l96-96H96c-18 0-32 14-32 32v64h1zm46 0h114l96-96H207l-96 96zm321 288c62 0 112-50 112-112S494 8 432 8 320 58 320 120s50 112 112 112zm0-256c80 0 144 64 144 144s-64 144-144 144-144-64-144-144S352-24 432-24zm-40 74c5-3 11-3 16 0l94 56c4 3 7 8 7 14s-3 11-7 14l-94 56c-5 3-11 3-16 0s-8-8-8-14V64c0-6 3-11 8-14zm24 98 46-28-46-28v56z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
||||
8
src/components/icons/hard-drive.vue
Normal file
8
src/components/icons/hard-drive.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 468 468"><path d="M10 74c0-35 29-64 64-64h320c35 0 64 29 64 64v256c0-35-29-64-64-64H74c-35 0-64 29-64 64V74zm288 288c0 18-14 32-32 32s-32-14-32-32 14-32 32-32 32 14 32 32z" fill="var(--fill1)"/><path d="M10 330c0-35 29-64 64-64h320c35 0 64 29 64 64v64c0 35-29 64-64 64H74c-35 0-64-29-64-64v-64zm288 32c0-18-14-32-32-32s-32 14-32 32 14 32 32 32 32-14 32-32zm64 32c18 0 32-14 32-32s-14-32-32-32-32 14-32 32 14 32 32 32z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="-10 -226 468 468"><path d="M64-184c-18 0-32 14-32 32V17c9-6 20-9 32-9h320c12 0 23 3 32 9v-169c0-18-14-32-32-32H64zM32 72v96c0 18 14 32 32 32h320c18 0 32-14 32-32V72c0-18-14-32-32-32H64c-18 0-32 14-32 32zM0 72v-224c0-35 29-64 64-64h320c35 0 64 29 64 64v320c0 35-29 64-64 64H64c-35 0-64-29-64-64V72zm256 24c13 0 24 11 24 24s-11 24-24 24-24-11-24-24 11-24 24-24zm96 0c13 0 24 11 24 24s-11 24-24 24-24-11-24-24 11-24 24-24z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -1,42 +0,0 @@
|
||||
import { createStaticVNode } from "vue";
|
||||
|
||||
export const Home = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M4.6 22.73A107 107 0 0 0 11 23h2.22c2.43-.04 4.6-.16 6.18-.27A3.9 3.9 0 0 0 23 18.8v-8.46a4 4 0 0 0-1.34-3L14.4.93a3.63 3.63 0 0 0-4.82 0L2.34 7.36A4 4 0 0 0 1 10.35v8.46a3.9 3.9 0 0 0 3.6 3.92M13.08 2.4l7.25 6.44a2 2 0 0 1 .67 1.5v8.46a1.9 1.9 0 0 1-1.74 1.92q-1.39.11-3.26.19V16a4 4 0 0 0-8 0v4.92q-1.87-.08-3.26-.19A1.9 1.9 0 0 1 3 18.81v-8.46a2 2 0 0 1 .67-1.5l7.25-6.44a1.63 1.63 0 0 1 2.16 0M13.12 21h-2.24a1 1 0 0 1-.88-1v-4a2 2 0 1 1 4 0v4a1 1 0 0 1-.88 1">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const HomeFilled = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M9.59.92a3.63 3.63 0 0 1 4.82 0l7.25 6.44A4 4 0 0 1 23 10.35v8.46a3.9 3.9 0 0 1-3.6 3.92 106 106 0 0 1-14.8 0A3.9 3.9 0 0 1 1 18.8v-8.46a4 4 0 0 1 1.34-3zM12 16a5 5 0 0 1-3.05-1.04l-1.23 1.58a7 7 0 0 0 8.56 0l-1.23-1.58A5 5 0 0 1 12 16">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const Dashboard = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M23 5a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4zm-10 6V3h6a2 2 0 0 1 2 2v6zm8 8a2 2 0 0 1-2 2h-6v-8h8zM5 3h6v18H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const DashboardFilled = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M11 23H5a4 4 0 0 1-4-4V5a4 4 0 0 1 4-4h6zm12-4a4 4 0 0 1-4 4h-6V13h10zM19 1a4 4 0 0 1 4 4v6H13V1z">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const Add = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M11 11H6v2h5v5h2v-5h5v-2h-5V6h-2zM5 1a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V5a4 4 0 0 0-4-4zm16 4v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const AddFilled = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M1 5a4 4 0 0 1 4-4h14a4 4 0 0 1 4 4v14a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4zm10 6H6v2h5v5h2v-5h5v-2h-5V6h-2z">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const Bell = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M16 19h8v-2h-.34a3.15 3.15 0 0 1-3.12-2.76l-.8-6.41a7.8 7.8 0 0 0-15.48 0l-.8 6.41A3.15 3.15 0 0 1 .34 17H0v2h8v1h.02a3.4 3.4 0 0 0 3.38 3h1.2a3.4 3.4 0 0 0 3.38-3H16zm1.75-10.92.8 6.4c.12.95.5 1.81 1.04 2.52H4.4c.55-.7.92-1.57 1.04-2.51l.8-6.41a5.8 5.8 0 0 1 11.5 0M13.4 19c.33 0 .6.27.6.6 0 .77-.63 1.4-1.4 1.4h-1.2a1.4 1.4 0 0 1-1.4-1.4c0-.33.27-.6.6-.6z">
|
||||
</path>
|
||||
</svg>`, 1);
|
||||
export const BellFilled = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24">
|
||||
<path
|
||||
d="M20.54 14.24A3.15 3.15 0 0 0 23.66 17H24v2h-8v1h-.02a3.4 3.4 0 0 1-3.38 3h-1.2a3.4 3.4 0 0 1-3.38-3H8v-1H0v-2h.34a3.15 3.15 0 0 0 3.12-2.76l.8-6.41a7.8 7.8 0 0 1 15.48 0zM10 19.6c0 .77.63 1.4 1.4 1.4h1.2c.77 0 1.4-.63 1.4-1.4a.6.6 0 0 0-.6-.6h-2.8a.6.6 0 0 0-.6.6" ></path>
|
||||
</svg>`, 1);
|
||||
export const Search = createStaticVNode(`<svg aria-hidden="true" aria-label="" class="v-mid m-a" height="24" role="img" viewBox="0 0 24 24" width="24"><path d="M17.33 18.74a10 10 0 1 1 1.41-1.41l4.47 4.47-1.41 1.41zM11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16"></path></svg>`, 1);
|
||||
7
src/components/icons/shield-user.vue
Normal file
7
src/components/icons/shield-user.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 531"><path d="M10 150c1 99 41 281 214 363 16 8 36 8 52 0 173-82 214-264 214-363 0-26-16-48-38-57L263 13c-4-2-9-3-13-3s-8 1-12 3L48 93c-22 9-38 31-38 57zm128 212c0-44 36-80 80-80h64c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16zm168-176c0 31-25 56-56 56s-56-25-56-56 25-56 56-56 56 25 56 56z" fill="var(--fill1)"/><path d="M282 282c44 0 80 36 80 80 0 9-7 16-16 16H154c-9 0-16-7-16-16 0-44 36-80 80-80h64zm-32-40c-31 0-56-25-56-56s25-56 56-56 56 25 56 56-25 56-56 56z" fill="currentColor"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="500" height="529" viewBox="6 -257 500 529"><path d="M231-240c16-7 34-7 50 0l177 75 4 2c18 9 32 28 34 50v24c-6 102-52 267-214 344l-5 2c-11 5-24 5-35 2l-6-1-6-3C68 178 22 14 17-88l-1-20c0-25 14-45 34-55l4-2 177-75zm38 29c-7-3-15-3-22-1l-3 1-177 75c-11 5-19 16-19 28l1 18c5 96 48 246 194 316l4 2c7 2 15 2 22-2l14-7c144-78 181-236 181-327v-3c-1-10-7-19-17-24l-2-1-176-75zm19 235c44 0 80 36 80 80 0 9-7 16-16 16s-16-7-16-16c0-26-21-48-48-48h-64c-26 0-48 22-48 48 0 9-7 16-16 16s-16-7-16-16c0-44 36-80 80-80h64zM256-8c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64zm0-96c-18 0-32 14-32 32s14 32 32 32 32-14 32-32-14-32-32-32z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
7
src/components/icons/windows.vue
Normal file
7
src/components/icons/windows.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg v-if="filled" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596 468"><path d="M170 74v64h64V74h288v192h-96v64h96c35 0 64-29 64-64V74c0-35-29-64-64-64H234c-35 0-64 29-64 64z" fill="var(--fill1)"/><path d="M74 138c-35 0-64 29-64 64v192c0 35 29 64 64 64h288c35 0 64-29 64-64V202c0-35-29-64-64-64H74zm24 80h240c13 0 24 11 24 24s-11 24-24 24H98c-13 0-24-11-24-24s11-24 24-24z" fill="var(--fill4)"/></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="596" height="468" viewBox="-10 -226 596 468"><path d="M512-184H224c-18 0-32 14-32 32v16h-32v-16c0-35 29-64 64-64h288c35 0 64 29 64 64V40c0 35-29 64-64 64h-48V72h48c18 0 32-14 32-32v-192c0-18-14-32-32-32zM352-56H64c-18 0-32 14-32 32V8h352v-32c0-18-14-32-32-32zm32 96H32v128c0 18 14 32 32 32h288c18 0 32-14 32-32V40zM64-88h288c35 0 64 29 64 64v192c0 35-29 64-64 64H64c-35 0-64-29-64-64V-24c0-35 29-64 64-64z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ filled?: boolean }>();
|
||||
</script>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ButtonHTMLAttributes, computed } from 'vue';
|
||||
type UiButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
||||
type UiButtonSize = 'sm' | 'md' | 'lg';
|
||||
type UiButtonSize = 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -12,6 +13,7 @@ const props = withDefaults(
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
onClick?: ButtonHTMLAttributes['onClick'];
|
||||
}>(),
|
||||
{
|
||||
variant: 'primary',
|
||||
@@ -24,32 +26,40 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const isDisabled = computed(() => props.disabled || props.loading);
|
||||
const buttonVariants = cva(":uno: inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap shadow-[0_1px_0_rgba(27,31,36,0.04),0_1px_3px_rgba(27,31,36,0.12)] outline-none transition-[transform,box-shadow,background-color,border-color,color,opacity] duration-150 ease-out active:translate-y-[0.5px] hover:shadow-[0_2px_0_rgba(27,31,36,0.06)] disabled:cursor-not-allowed disabled:opacity-60 focus-visible:ring-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'border-transparent bg-primary text-white hover:bg-primaryHover focus-visible:ring-primary/25',
|
||||
secondary: 'border-border bg-white text-text hover:bg-header focus-visible:ring-#0969da/20',
|
||||
ghost: 'border-transparent bg-transparent text-text hover:bg-header focus-visible:ring-#0969da/20 shadow-none',
|
||||
danger: 'border-transparent bg-danger text-white hover:opacity-92 focus-visible:ring-danger/20',
|
||||
},
|
||||
size: {
|
||||
sm: 'min-h-[28px] px-3 text-[12px] leading-[20px]',
|
||||
md: 'min-h-[32px] px-3 text-[14px] leading-[20px]',
|
||||
lg: 'min-h-[36px] px-4 text-[14px] leading-[20px]',
|
||||
icon: 'min-h-0 p-2',
|
||||
'icon-sm': 'min-h-0 p-1',
|
||||
'icon-lg': 'min-h-0 p-3',
|
||||
},
|
||||
block: {
|
||||
true: 'w-full',
|
||||
false: '',
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: props.variant,
|
||||
size: props.size,
|
||||
block: props.block,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const classes = computed(() => {
|
||||
const variants: Record<UiButtonVariant, string> = {
|
||||
primary: 'border-transparent bg-primary text-white hover:bg-primaryHover focus-visible:ring-primary/25',
|
||||
secondary: 'border-border bg-white text-text hover:bg-header focus-visible:ring-#0969da/20',
|
||||
ghost: 'border-transparent bg-transparent text-text hover:bg-header focus-visible:ring-#0969da/20 shadow-none',
|
||||
danger: 'border-transparent bg-danger text-white hover:opacity-92 focus-visible:ring-danger/20',
|
||||
};
|
||||
|
||||
const sizes: Record<UiButtonSize, string> = {
|
||||
sm: 'min-h-[28px] px-3 text-[12px] leading-[20px]',
|
||||
md: 'min-h-[32px] px-3 text-[14px] leading-[20px]',
|
||||
lg: 'min-h-[36px] px-4 text-[14px] leading-[20px]',
|
||||
};
|
||||
|
||||
return [
|
||||
'inline-flex items-center justify-center gap-2 rounded-md border font-medium whitespace-nowrap shadow-primer outline-none transition-[transform,box-shadow,background-color,border-color,color,opacity] duration-150 ease-out active:translate-y-[0.5px] hover:shadow-[0_2px_0_rgba(27,31,36,0.06)] disabled:cursor-not-allowed disabled:opacity-60 focus-visible:ring-4',
|
||||
variants[props.variant],
|
||||
sizes[props.size],
|
||||
props.block ? 'w-full' : '',
|
||||
].join(' ');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button :type="type" :disabled="isDisabled" :class="classes" :aria-busy="loading || undefined">
|
||||
<button :type="type" :disabled="isDisabled" :class="cn(buttonVariants({variant, size, block}))" v-on:click="onClick" :aria-busy="loading || undefined">
|
||||
<span
|
||||
v-if="loading"
|
||||
class="h-4 w-4 shrink-0 animate-spin rounded-full border-2 border-current border-r-transparent"
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
import XIcon from '@/components/icons/XIcon.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { onBeforeUnmount, watch } from 'vue';
|
||||
import ClientOnly from '../ClientOnly';
|
||||
|
||||
// Ensure client-side only rendering to avoid hydration mismatch
|
||||
const isMounted = ref(false);
|
||||
onMounted(() => {
|
||||
isMounted.value = true;
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
visible: boolean;
|
||||
title?: string;
|
||||
@@ -43,8 +39,14 @@ watch(
|
||||
() => props.visible,
|
||||
(v) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (v) window.addEventListener('keydown', onKeydown);
|
||||
else window.removeEventListener('keydown', onKeydown);
|
||||
if (v) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
window.addEventListener('keydown', onKeydown);
|
||||
}
|
||||
else {
|
||||
document.body.style.overflow = 'unset';
|
||||
window.removeEventListener('keydown', onKeydown);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
@@ -56,58 +58,60 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="isMounted" to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-all duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="visible" class="fixed inset-0 z-[9999]">
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/30"
|
||||
@click="closable && close()"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-all duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="visible" class="fixed inset-0 z-[9999]">
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40"
|
||||
@click="closable && close()"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<!-- Panel -->
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div :class="cn('w-full bg-header border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
|
||||
<!-- Header slot -->
|
||||
<div v-if="$slots.header" class="px-5 py-4 border-b border-border">
|
||||
<slot name="header" :close="close" />
|
||||
</div>
|
||||
<!-- Default title -->
|
||||
<div v-else-if="title" class="flex items-center justify-between gap-3 px-5 py-4 border-b border-border">
|
||||
<h3 class="text-sm font-semibold text-foreground">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="p-1 rounded-md text-foreground/60 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||
@click="close"
|
||||
:aria-label="t('common.close')"
|
||||
>
|
||||
<XIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- Panel -->
|
||||
<div class="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div :class="cn('w-full bg-white border border-border rounded-lg shadow-lg overflow-hidden', maxWidthClass)">
|
||||
<!-- Header slot -->
|
||||
<div v-if="$slots.header" class="px-5 py-4 border-b border-border">
|
||||
<slot name="header" :close="close" />
|
||||
</div>
|
||||
<!-- Default title -->
|
||||
<div v-else-if="title" class="flex items-center justify-between gap-3 px-5 py-4 border-b border-border">
|
||||
<h3 class="font-semibold text-foreground">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="p-1 rounded-md text-foreground/60 hover:text-foreground hover:bg-muted/50 transition-all"
|
||||
@click="close"
|
||||
:aria-label="t('common.close')"
|
||||
>
|
||||
<XIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-5">
|
||||
<slot />
|
||||
</div>
|
||||
<!-- Content -->
|
||||
<div class="p-5 max-h-[80vh] overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Footer slot -->
|
||||
<div v-if="$slots.footer" class="px-5 py-4 border-t border-border bg-muted/20">
|
||||
<slot name="footer" />
|
||||
<!-- Footer slot -->
|
||||
<div v-if="$slots.footer" class="px-5 py-4 border-t border-border bg-muted/20">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { computed, useSlots } from 'vue';
|
||||
// Vue macro is available at compile time; provide a safe fallback for typecheck.
|
||||
declare const defineModelModifiers: undefined | (<T>() => T);
|
||||
|
||||
|
||||
type Props = {
|
||||
as?: 'input' | 'textarea' | 'select';
|
||||
modelValue?: string | number | null;
|
||||
type?: string;
|
||||
placeholder?: string;
|
||||
@@ -21,14 +20,17 @@ type Props = {
|
||||
max?: number | string;
|
||||
step?: number | string;
|
||||
maxlength?: number;
|
||||
rows?: number;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: 'input',
|
||||
modelValue: '',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
readonly: false,
|
||||
disabled: false,
|
||||
rows: 3,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,10 +42,13 @@ const modelModifiers = (typeof defineModelModifiers === 'function'
|
||||
? defineModelModifiers<{ number?: boolean }>()
|
||||
: ({} as { number?: boolean }));
|
||||
|
||||
const isNumberLike = computed(() => props.type === 'number' || !!modelModifiers.number);
|
||||
const isNumberLike = computed(() => props.as === 'input' && (props.type === 'number' || !!modelModifiers.number));
|
||||
const hasLeadingSlot = computed(() => props.as === 'input' && !!useSlots().prefix);
|
||||
const isTextarea = computed(() => props.as === 'textarea');
|
||||
const isSelect = computed(() => props.as === 'select');
|
||||
|
||||
const onInput = (e: Event) => {
|
||||
const el = e.target as HTMLInputElement;
|
||||
const el = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
const raw = el.value;
|
||||
if (isNumberLike.value) {
|
||||
if (raw === '') {
|
||||
@@ -66,11 +71,40 @@ const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-head
|
||||
|
||||
<template>
|
||||
<div :class="cn('relative', wrapperClass)">
|
||||
<div v-if="$slots.prefix" class="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/50">
|
||||
<div v-if="hasLeadingSlot" class="absolute left-3 top-1/2 -translate-y-1/2 text-foreground/50">
|
||||
<slot name="prefix" />
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-if="isTextarea"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:value="modelValue ?? ''"
|
||||
:rows="rows"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:maxlength="maxlength"
|
||||
:class="cn(baseInputClass, inputClass)"
|
||||
@input="onInput"
|
||||
@keyup="onKeyup"
|
||||
/>
|
||||
|
||||
<select
|
||||
v-else-if="isSelect"
|
||||
:id="id"
|
||||
:name="name"
|
||||
:value="modelValue ?? ''"
|
||||
:disabled="disabled"
|
||||
:class="cn(baseInputClass, inputClass)"
|
||||
@change="onInput"
|
||||
@keyup="onKeyup"
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-else
|
||||
:id="id"
|
||||
:name="name"
|
||||
:type="type"
|
||||
@@ -83,7 +117,7 @@ const baseInputClass = 'w-full px-3 py-2 rounded-md border border-border bg-head
|
||||
:max="max"
|
||||
:step="step"
|
||||
:maxlength="maxlength"
|
||||
:class="cn(baseInputClass, $slots.prefix ? 'pl-10' : '', inputClass)"
|
||||
:class="cn(baseInputClass, hasLeadingSlot ? 'pl-10' : '', inputClass)"
|
||||
@input="onInput"
|
||||
@keyup="onKeyup"
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<script setup lang="ts" generic="TData extends Record<string, any>">
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
FlexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useVueTable,
|
||||
type ColumnMeta,
|
||||
type ColumnDef,
|
||||
type ColumnMeta,
|
||||
type Row,
|
||||
type SortingState,
|
||||
type Updater,
|
||||
} from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
type TableColumnMeta = ColumnMeta<TData, any> & {
|
||||
headerClass?: string;
|
||||
@@ -27,11 +30,36 @@ const props = withDefaults(defineProps<{
|
||||
headerRowClass?: string;
|
||||
bodyRowClass?: string | ((row: Row<TData>) => string | undefined);
|
||||
getRowId?: (originalRow: TData, index: number) => string;
|
||||
pagination?: boolean;
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
totalRecords?: number;
|
||||
rowsPerPage?: number;
|
||||
pageSizeOptions?: number[];
|
||||
canPreviousPage?: boolean;
|
||||
canNextPage?: boolean;
|
||||
skeletonRows?: number;
|
||||
}>(), {
|
||||
loading: false,
|
||||
emptyText: 'No data available.',
|
||||
pagination: false,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalRecords: 0,
|
||||
rowsPerPage: 10,
|
||||
pageSizeOptions: () => [],
|
||||
canPreviousPage: false,
|
||||
canNextPage: false,
|
||||
skeletonRows: 10,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'previous-page'): void;
|
||||
(e: 'next-page'): void;
|
||||
(e: 'page-size-change', value: number): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const sorting = ref<SortingState>([]);
|
||||
|
||||
function updateSorting(updaterOrValue: Updater<SortingState>) {
|
||||
@@ -63,6 +91,35 @@ function resolveBodyRowClass(row: Row<TData>) {
|
||||
? props.bodyRowClass(row)
|
||||
: props.bodyRowClass;
|
||||
}
|
||||
|
||||
const shouldRenderPagination = computed(() => (
|
||||
props.pagination
|
||||
&& !props.loading
|
||||
&& table.getRowModel().rows.length > 0
|
||||
));
|
||||
|
||||
const skeletonRowIndexes = computed(() =>
|
||||
Array.from({ length: Math.max(1, props.skeletonRows) }, (_, index) => index)
|
||||
);
|
||||
|
||||
const skeletonColumnIndexes = computed(() =>
|
||||
Array.from({ length: Math.max(1, props.columns.length) }, (_, index) => index)
|
||||
);
|
||||
|
||||
function previousPage() {
|
||||
if (!props.canPreviousPage) return;
|
||||
emit('previous-page');
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (!props.canNextPage) return;
|
||||
emit('next-page');
|
||||
}
|
||||
|
||||
function changePageSize(event: Event) {
|
||||
const nextValue = Number((event.target as HTMLSelectElement).value) || props.rowsPerPage;
|
||||
emit('page-size-change', nextValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -102,16 +159,44 @@ function resolveBodyRowClass(row: Row<TData>) {
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-if="loading">
|
||||
<td
|
||||
:colspan="columns.length || 1"
|
||||
class="px-4 py-10 text-center text-sm text-gray-500"
|
||||
<template v-if="loading">
|
||||
<tr v-if="$slots.loading">
|
||||
<td
|
||||
:colspan="columns.length || 1"
|
||||
class="px-4 py-10 text-center text-sm text-gray-500"
|
||||
>
|
||||
<slot name="loading" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr
|
||||
v-for="rowIndex in skeletonRowIndexes"
|
||||
v-else
|
||||
:key="`skeleton-row-${rowIndex}`"
|
||||
class="border-b border-gray-200 last:border-b-0"
|
||||
>
|
||||
<slot name="loading">
|
||||
Loading...
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
<td
|
||||
v-for="columnIndex in skeletonColumnIndexes"
|
||||
:key="`skeleton-cell-${rowIndex}-${columnIndex}`"
|
||||
class="px-4 py-3 align-middle"
|
||||
>
|
||||
<div class="animate-pulse space-y-2">
|
||||
<div
|
||||
:class="cn(
|
||||
'h-4 rounded bg-muted/50',
|
||||
columnIndex === skeletonColumnIndexes.length - 1
|
||||
? 'ml-auto w-16'
|
||||
: 'w-full max-w-[12rem]'
|
||||
)"
|
||||
/>
|
||||
<div
|
||||
v-if="columnIndex === 0"
|
||||
class="h-3 w-24 rounded bg-muted/40"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<tr v-else-if="!table.getRowModel().rows.length">
|
||||
<td
|
||||
@@ -149,5 +234,21 @@ function resolveBodyRowClass(row: Row<TData>) {
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div v-if="shouldRenderPagination" class="flex flex-col gap-3 border-t border-gray-200 bg-muted/20 px-6 py-4 text-xs text-foreground/55 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>{{ t('common.page', { current: currentPage, total: totalPages }) }} · {{ totalRecords }} {{ t('common.records') }}</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label v-if="pageSizeOptions.length" class="flex items-center gap-2">
|
||||
<span>{{ t('common.rowsPerPage') }}</span>
|
||||
<select class="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground" :value="String(rowsPerPage)" @change="changePageSize">
|
||||
<option v-for="option in pageSizeOptions" :key="option" :value="String(option)">{{ option }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 xl:justify-end">
|
||||
<AppButton size="sm" variant="secondary" :disabled="!canPreviousPage" @click="previousPage">{{ t('common.previous') }}</AppButton>
|
||||
<AppButton size="sm" variant="secondary" :disabled="!canNextPage" @click="nextPage">{{ t('common.next') }}</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +27,11 @@ type NotificationApiItem = {
|
||||
createdAt?: string;
|
||||
};
|
||||
|
||||
type IncomingNotificationEnvelope = {
|
||||
type?: string;
|
||||
payload?: NotificationApiItem;
|
||||
};
|
||||
|
||||
const notifications = ref<AppNotification[]>([]);
|
||||
const loading = ref(false);
|
||||
const loaded = ref(false);
|
||||
@@ -45,6 +50,31 @@ const normalizeType = (value?: string): NotificationType => {
|
||||
}
|
||||
};
|
||||
|
||||
const mapNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
id: item.id || '',
|
||||
type: normalizeType(item.type),
|
||||
title: item.title || '',
|
||||
message: item.message || '',
|
||||
time: '',
|
||||
read: Boolean(item.read),
|
||||
actionUrl: item.actionUrl || undefined,
|
||||
actionLabel: item.actionLabel || undefined,
|
||||
createdAt: item.createdAt,
|
||||
});
|
||||
|
||||
const upsertNotification = (item: NotificationApiItem) => {
|
||||
const mapped = mapNotification({ ...item, read: item.read ?? false });
|
||||
if (!mapped.id) return;
|
||||
|
||||
const index = notifications.value.findIndex(notification => notification.id === mapped.id);
|
||||
if (index >= 0) {
|
||||
notifications.value[index] = { ...notifications.value[index], ...mapped };
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.value = [mapped, ...notifications.value];
|
||||
};
|
||||
|
||||
export function useNotifications() {
|
||||
const { t, i18next } = useTranslation();
|
||||
|
||||
@@ -62,23 +92,16 @@ export function useNotifications() {
|
||||
return t('notification.time.daysAgo', { count: Math.max(1, days) });
|
||||
};
|
||||
|
||||
const mapNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
id: item.id || '',
|
||||
type: normalizeType(item.type),
|
||||
title: item.title || '',
|
||||
message: item.message || '',
|
||||
const hydrateNotification = (item: NotificationApiItem): AppNotification => ({
|
||||
...mapNotification(item),
|
||||
time: formatRelativeTime(item.createdAt),
|
||||
read: Boolean(item.read),
|
||||
actionUrl: item.actionUrl || undefined,
|
||||
actionLabel: item.actionLabel || undefined,
|
||||
createdAt: item.createdAt,
|
||||
});
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await rpcClient.listNotifications();
|
||||
notifications.value = (response.notifications || []).map(mapNotification);
|
||||
notifications.value = (response.notifications || []).map(hydrateNotification);
|
||||
loaded.value = true;
|
||||
return notifications.value;
|
||||
} finally {
|
||||
@@ -86,6 +109,22 @@ export function useNotifications() {
|
||||
}
|
||||
};
|
||||
|
||||
const ingestRealtimeNotification = (raw: string | IncomingNotificationEnvelope) => {
|
||||
try {
|
||||
|
||||
const envelope = typeof raw === 'string' ? JSON.parse(raw) as IncomingNotificationEnvelope : raw;
|
||||
if (envelope?.type !== 'notification.created' || !envelope.payload) return false;
|
||||
upsertNotification(envelope.payload);
|
||||
notifications.value = notifications.value.map(item => ({
|
||||
...item,
|
||||
time: formatRelativeTime(item.createdAt),
|
||||
}));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const markRead = async (id: string) => {
|
||||
if (!id) return;
|
||||
await rpcClient.markNotificationRead({ id });
|
||||
@@ -118,6 +157,7 @@ export function useNotifications() {
|
||||
unreadCount,
|
||||
locale: computed(() => i18next.resolvedLanguage),
|
||||
fetchNotifications,
|
||||
ingestRealtimeNotification,
|
||||
markRead,
|
||||
deleteNotification,
|
||||
markAllRead,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import type { Preferences } from '@/server/gen/proto/app/v1/common';
|
||||
import type { UpdatePreferencesRequest } from '@/server/gen/proto/app/v1/account';
|
||||
import type { Preferences } from '@/server/api/proto/app/v1/common';
|
||||
import type { UpdatePreferencesRequest } from '@/server/api/proto/app/v1/account';
|
||||
import { useQuery } from '@pinia/colada';
|
||||
|
||||
export const SETTINGS_PREFERENCES_QUERY_KEY = ['settings', 'preferences'] as const;
|
||||
|
||||
@@ -85,7 +85,9 @@ export class TinyMqttClient implements ITinyMqttClient {
|
||||
break;
|
||||
case 0xD0: // PINGRESP
|
||||
break;
|
||||
case 0x30: // PUBLISH
|
||||
case 0x30: // PUBLISH QoS 0
|
||||
case 0x32: // PUBLISH QoS 1
|
||||
case 0x34: // PUBLISH QoS 2
|
||||
this.parsePublish(data);
|
||||
break;
|
||||
}
|
||||
@@ -102,9 +104,32 @@ export class TinyMqttClient implements ITinyMqttClient {
|
||||
}
|
||||
|
||||
private parsePublish(data: Uint8Array): void {
|
||||
const tLen = (data[2] << 8) | data[3];
|
||||
const topic = this.decoder.decode(data.slice(4, 4 + tLen));
|
||||
const payload = this.decoder.decode(data.slice(4 + tLen));
|
||||
let multiplier = 1;
|
||||
let remainingLength = 0;
|
||||
let offset = 1;
|
||||
let encodedByte = 0;
|
||||
|
||||
do {
|
||||
encodedByte = data[offset++];
|
||||
remainingLength += (encodedByte & 127) * multiplier;
|
||||
multiplier *= 128;
|
||||
} while ((encodedByte & 128) !== 0 && offset < data.length);
|
||||
|
||||
const variableHeaderStart = offset;
|
||||
const topicLength = (data[offset] << 8) | data[offset + 1];
|
||||
offset += 2;
|
||||
|
||||
const topic = this.decoder.decode(data.slice(offset, offset + topicLength));
|
||||
offset += topicLength;
|
||||
|
||||
const qos = (data[0] >> 1) & 0x03;
|
||||
if (qos > 0) {
|
||||
offset += 2; // packet identifier
|
||||
}
|
||||
|
||||
const consumedFromVariableHeader = offset - variableHeaderStart;
|
||||
const payloadLength = Math.max(0, remainingLength - consumedFromVariableHeader);
|
||||
const payload = this.decoder.decode(data.slice(offset, offset + payloadLength));
|
||||
this.onMessage(topic, payload);
|
||||
}
|
||||
}
|
||||
|
||||
106
src/lib/utils.ts
106
src/lib/utils.ts
@@ -5,7 +5,10 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
export function debounce<Func extends (...args: any[]) => any>(func: Func, wait: number): Func {
|
||||
export function debounce<Func extends (...args: any[]) => any>(
|
||||
func: Func,
|
||||
wait: number
|
||||
): Func {
|
||||
let timeout: ReturnType<typeof setTimeout> | null;
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (timeout) clearTimeout(timeout);
|
||||
@@ -17,8 +20,8 @@ export function debounce<Func extends (...args: any[]) => any>(func: Func, wait:
|
||||
type AspectInfo = {
|
||||
width: number;
|
||||
height: number;
|
||||
ratio: string; // ví dụ: "16:9"
|
||||
float: number; // ví dụ: 1.777...
|
||||
ratio: string; // ví dụ: "16:9"
|
||||
float: number; // ví dụ: 1.777...
|
||||
};
|
||||
|
||||
function gcd(a: number, b: number): number {
|
||||
@@ -39,7 +42,7 @@ export function getImageAspectRatio(url: string): Promise<AspectInfo> {
|
||||
width: w,
|
||||
height: h,
|
||||
ratio: `${w / g}:${h / g}`,
|
||||
float: w / h
|
||||
float: w / h,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -50,9 +53,9 @@ export function getImageAspectRatio(url: string): Promise<AspectInfo> {
|
||||
}
|
||||
|
||||
export const formatBytes = (bytes?: number) => {
|
||||
if (!bytes) return '0 B';
|
||||
if (!bytes) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
|
||||
return `${value} ${sizes[i]}`;
|
||||
@@ -60,44 +63,89 @@ export const formatBytes = (bytes?: number) => {
|
||||
};
|
||||
|
||||
export const formatDuration = (seconds?: number) => {
|
||||
if (!seconds) return '0:00';
|
||||
if (!seconds) return "0:00";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
if (h > 0) {
|
||||
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
return `${h}:${m.toString().padStart(2, "0")}:${s
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
export const formatDate = (dateString: string = "", dateOnly: boolean = false) => {
|
||||
if (!dateString) return '';
|
||||
const locale = typeof document !== 'undefined'
|
||||
? document.documentElement.lang === 'vi' ? 'vi-VN' : 'en-US'
|
||||
: 'en-US';
|
||||
export const formatDate = (
|
||||
dateString: string = "",
|
||||
dateOnly: boolean = false
|
||||
) => {
|
||||
if (!dateString) return "";
|
||||
const locale =
|
||||
typeof document !== "undefined"
|
||||
? document.documentElement.lang === "vi"
|
||||
? "vi-VN"
|
||||
: "en-US"
|
||||
: "en-US";
|
||||
return new Date(dateString).toLocaleDateString(locale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
...(dateOnly ? {} : { hour: '2-digit', minute: '2-digit' })
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
...(dateOnly ? {} : { hour: "2-digit", minute: "2-digit" }),
|
||||
});
|
||||
};
|
||||
|
||||
export const getStatusSeverity = (status: string = "") => {
|
||||
type Status = "success" | "failed" | "pending" | string;
|
||||
export const getStatusSeverity = (status: Status = "") => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
case 'ready':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
case 'pending':
|
||||
return 'warn';
|
||||
case "success":
|
||||
case "ready":
|
||||
return "success";
|
||||
case "failed":
|
||||
return "danger";
|
||||
case "pending":
|
||||
return "warn";
|
||||
default:
|
||||
return 'info';
|
||||
return "info";
|
||||
}
|
||||
};
|
||||
export const getStatusStyles = (status: Status = "") => {
|
||||
switch (status) {
|
||||
case "success":
|
||||
return "bg-success/10 text-success";
|
||||
case "failed":
|
||||
return "bg-danger/10 text-danger";
|
||||
case "pending":
|
||||
return "bg-warning/10 text-warning";
|
||||
default:
|
||||
return "bg-info/10 text-info";
|
||||
}
|
||||
};
|
||||
export const isAdmin = (role: string = "") => {
|
||||
const r = String(role).toLowerCase();
|
||||
return r === "admin" || r === "superadmin";
|
||||
};
|
||||
};
|
||||
export type ApiErrorPayload = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
export const getApiErrorPayload = (error: unknown): ApiErrorPayload | null => {
|
||||
if (!error || typeof error !== "object") return null;
|
||||
const candidate = error as {
|
||||
error?: ApiErrorPayload;
|
||||
data?: ApiErrorPayload;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
if (candidate.error && typeof candidate.error === "object")
|
||||
return candidate.error;
|
||||
if (candidate.data && typeof candidate.data === "object")
|
||||
return candidate.data;
|
||||
if (candidate.message) return { message: candidate.message };
|
||||
|
||||
return null;
|
||||
};
|
||||
export const getApiErrorMessage = (error: unknown, fallback: string) => {
|
||||
const payload = getApiErrorPayload(error);
|
||||
return payload?.message || fallback;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Video as ModelVideo } from "@/server/gen/proto/app/v1/common";
|
||||
import type { Video as ModelVideo } from "@/server/api/proto/app/v1/common";
|
||||
|
||||
export const mockVideos: ModelVideo[] = [
|
||||
{
|
||||
|
||||
22
src/routes/analytics/Analytics.vue
Normal file
22
src/routes/analytics/Analytics.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader title="Analytics"
|
||||
description="Your streaming analytics will be displayed here."
|
||||
:breadcrumbs="breadcrumbs" />
|
||||
<div class="p-6 rounded-xl border border-gray-200">
|
||||
<h2 class="text-lg font-semibold mb-4">Coming Soon</h2>
|
||||
<p class="text-gray-600">We are working hard to bring you detailed analytics about your streams. Stay tuned!</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
const {t} = useTranslation()
|
||||
const breadcrumbs = computed(() => [
|
||||
{ label: t('pageHeader.dashboard'), to: '/overview' },
|
||||
{ label: t('pageHeader.settings'), to: '/settings' },
|
||||
{ label: "Analytics"}
|
||||
// ...(currentItem.value ? [{ label: currentItem.value.label + (currentItem.value.value.includes("admin") ? " (Admin)" : "") }] : []),
|
||||
]);
|
||||
</script>
|
||||
@@ -67,7 +67,7 @@ const routes: RouteData[] = [
|
||||
{
|
||||
path: "ref/:username",
|
||||
name: "referral-entry",
|
||||
beforeEnter: (to) => ({ name: "signup", query: { ref: String(to.params.username || "") } }),
|
||||
redirect: (to) => ({ name: "signup", query: { ref: String(to.params.username || "") } }),
|
||||
},
|
||||
{
|
||||
path: "forgot",
|
||||
@@ -117,6 +117,16 @@ const routes: RouteData[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "analytics",
|
||||
name: "analytics",
|
||||
component: () => import("./analytics/Analytics.vue"),
|
||||
meta: {
|
||||
head: {
|
||||
title: "Analytics - Holistream",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "notification",
|
||||
name: "notification",
|
||||
@@ -204,6 +214,16 @@ const routes: RouteData[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "popup-ads",
|
||||
name: "settings-popup-ads",
|
||||
component: () => import("./settings/PopupAds/PopupAds.vue"),
|
||||
meta: {
|
||||
head: {
|
||||
title: "Popup Ads - Holistream",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "player-configs",
|
||||
name: "settings-player-configs",
|
||||
@@ -234,6 +254,7 @@ const routes: RouteData[] = [
|
||||
{ path: "payments", name: "admin-payments", component: () => import("./settings/admin/Payments.vue") },
|
||||
{ path: "plans", name: "admin-plans", component: () => import("./settings/admin/Plans.vue") },
|
||||
{ path: "ad-templates", name: "admin-ad-templates", component: () => import("./settings/admin/AdTemplates.vue") },
|
||||
{ path: "popup-ads", name: "admin-popup-ads", component: () => import("./settings/admin/PopupAds.vue") },
|
||||
{ path: "player-configs", name: "admin-player-configs", component: () => import("./settings/admin/PlayerConfigs.vue") },
|
||||
{ path: "jobs", name: "admin-jobs", component: () => import("./settings/admin/Jobs.vue") },
|
||||
{ path: "agents", name: "admin-agents", component: () => import("./settings/admin/Agents.vue") },
|
||||
|
||||
@@ -6,6 +6,11 @@ import NotificationActions from './components/NotificationActions.vue';
|
||||
import NotificationList from './components/NotificationList.vue';
|
||||
import NotificationTabs from './components/NotificationTabs.vue';
|
||||
import { useNotifications } from '@/composables/useNotifications';
|
||||
import Inbox from '@/components/icons/Inbox.vue';
|
||||
import Video from '@/components/icons/Video.vue';
|
||||
import Credit from '@/components/icons/Credit.vue';
|
||||
import BellOff from '@/components/icons/BellOff.vue';
|
||||
import BellDot from '@/components/icons/BellDot.vue';
|
||||
|
||||
const activeTab = ref('all');
|
||||
const { t } = useTranslation();
|
||||
@@ -18,10 +23,10 @@ onMounted(() => {
|
||||
const unreadCount = computed(() => notificationStore.unreadCount.value);
|
||||
|
||||
const tabs = computed(() => [
|
||||
{ key: 'all', label: t('notification.tabs.all'), icon: 'i-lucide-inbox', count: notificationStore.notifications.value.length },
|
||||
{ key: 'unread', label: t('notification.tabs.unread'), icon: 'i-lucide-bell-dot', count: unreadCount.value },
|
||||
{ key: 'video', label: t('notification.tabs.videos'), icon: 'i-lucide-video', count: notificationStore.notifications.value.filter(n => n.type === 'video').length },
|
||||
{ key: 'payment', label: t('notification.tabs.payments'), icon: 'i-lucide-credit-card', count: notificationStore.notifications.value.filter(n => n.type === 'payment').length },
|
||||
{ key: 'all', label: t('notification.tabs.all'), icon: Inbox, count: notificationStore.notifications.value.length },
|
||||
{ key: 'unread', label: t('notification.tabs.unread'), icon: BellDot, count: unreadCount.value },
|
||||
{ key: 'video', label: t('notification.tabs.videos'), icon: Video, count: notificationStore.notifications.value.filter(n => n.type === 'video').length },
|
||||
{ key: 'payment', label: t('notification.tabs.payments'), icon: Credit, count: notificationStore.notifications.value.filter(n => n.type === 'payment').length },
|
||||
]);
|
||||
|
||||
const filteredNotifications = computed(() => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Bell from '@/components/icons/Bell.vue';
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
interface Props {
|
||||
@@ -21,7 +24,7 @@ const { t } = useTranslation();
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="stats flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<span class="i-lucide-bell w-4 h-4 text-gray-400"></span>
|
||||
<Bell filled class="w-4 h-4 text-gray-400" />
|
||||
<span class="text-gray-600">{{ t('notification.stats.total', { count: totalCount }) }}</span>
|
||||
</div>
|
||||
<div v-if="unreadCount > 0" class="flex items-center gap-2 text-sm">
|
||||
@@ -39,7 +42,7 @@ const { t } = useTranslation();
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-primary
|
||||
hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span class="i-lucide-check-check w-4 h-4"></span>
|
||||
<CheckIcon class="w-4 h-4" />
|
||||
{{ t('notification.actions.markAllRead') }}
|
||||
</button>
|
||||
<button
|
||||
@@ -49,7 +52,7 @@ const { t } = useTranslation();
|
||||
class="px-3 py-2 text-sm font-medium text-gray-600 hover:text-red-600
|
||||
hover:bg-red-50 rounded-lg transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span class="i-lucide-trash w-4 h-4"></span>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
{{ t('notification.actions.clearAll') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import NotificationItem from './NotificationItem.vue';
|
||||
import BellOff from '@/components/icons/BellOff.vue';
|
||||
|
||||
interface Notification {
|
||||
id: string;
|
||||
@@ -60,7 +61,7 @@ const { t } = useTranslation();
|
||||
class="py-16 text-center"
|
||||
>
|
||||
<div class="w-20 h-20 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<span class="i-lucide-bell-off w-10 h-10 text-gray-400"></span>
|
||||
<BellOff class="w-10 h-10 text-gray-400" />
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-1">{{ t('notification.empty.title') }}</h3>
|
||||
<p class="text-gray-500">{{ t('notification.empty.subtitle') }}</p>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
interface Tab {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
icon: any;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ const emit = defineEmits<{
|
||||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||||
]"
|
||||
>
|
||||
<span :class="[tab.icon, 'w-4 h-4']"></span>
|
||||
<component :is="tab.icon" :filled="activeTab === tab.key" class="w-4 h-4" />
|
||||
{{ tab.label }}
|
||||
<span
|
||||
v-if="tab.count && tab.count > 0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="tsx">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||
import type { Video as ModelVideo } from '@/server/api/proto/app/v1/common';
|
||||
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
@@ -76,7 +76,7 @@ const quickActions = computed(() => [
|
||||
'group press-animated',
|
||||
]">
|
||||
<div
|
||||
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted-dark group-hover:bg-primary/10">
|
||||
class="w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-muted-dark group-hover:(bg-primary/10 text-primary) transition-colors">
|
||||
<component filled :is="action.icon" class="w-6 h-6" />
|
||||
</div>
|
||||
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
||||
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||
import type { Video as ModelVideo } from '@/server/api/proto/app/v1/common';
|
||||
import { formatDate, formatDuration } from '@/lib/utils';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'i18next-vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import AdsVastDialog from './components/AdsVastDialog.vue';
|
||||
import AdsVastNotices from './components/AdsVastNotices.vue';
|
||||
import AdsVastTable from './components/AdsVastTable.tsx';
|
||||
import AdsVastTable from './components/AdsVastTable';
|
||||
import AdsVastToolbar from './components/AdsVastToolbar.vue';
|
||||
import type {
|
||||
AdTemplate,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineComponent, computed, type PropType } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, defineComponent, type PropType } from 'vue';
|
||||
import type { AdTemplate } from '../types';
|
||||
|
||||
// Components
|
||||
@@ -13,6 +13,7 @@ import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdTemplateTable',
|
||||
@@ -79,7 +80,7 @@ export default defineComponent({
|
||||
header: t('settings.adsVast.table.template'),
|
||||
accessorFn: (row) => row.name || '',
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-foreground">{row.original.name || ''}</span>
|
||||
{row.original.isDefault && (
|
||||
@@ -89,9 +90,9 @@ export default defineComponent({
|
||||
)}
|
||||
</div>
|
||||
<p class="mt-0.5 text-xs text-foreground/50">
|
||||
{t('settings.adsVast.createdOn', { date: row.original.createdAt || '-' })}
|
||||
{t('settings.adsVast.createdOn', { date: formatDate(row.original.createdAt) || '-' })}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
@@ -170,12 +171,8 @@ export default defineComponent({
|
||||
header: t('common.actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
{row.original.isDefault ? (
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{t('settings.adsVast.actions.default')}
|
||||
</span>
|
||||
) : (
|
||||
<div class="flex flex-wrap items-center justify-center gap-2">
|
||||
{!row.original.isDefault && (
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -195,38 +192,35 @@ export default defineComponent({
|
||||
)}
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
size="icon"
|
||||
disabled={props.isReadOnly || props.isMutating}
|
||||
onClick={() => emit('edit', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <PencilIcon class="h-4 w-4" />
|
||||
icon: () => <PencilIcon filled class="h-4 w-4" />
|
||||
}}
|
||||
/>
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
size="icon"
|
||||
disabled={props.isReadOnly || props.isMutating}
|
||||
onClick={() => emit('delete', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <TrashIcon class="h-4 w-4 text-danger" />
|
||||
icon: () => <TrashIcon filled class="h-4 w-4" />
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-right',
|
||||
headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center',
|
||||
cellClass: 'px-6 py-3 text-center',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
return () => (
|
||||
<>
|
||||
{props.isInitialLoading ? (
|
||||
<SettingsTableSkeleton columns={5} rows={4} />
|
||||
) : (
|
||||
<BaseTable
|
||||
data={props.templates}
|
||||
loading={props.isInitialLoading}
|
||||
columns={columns.value}
|
||||
getRowId={(row: AdTemplate, index: number) =>
|
||||
row.id || `${row.name || 'template'}:${row.vastTagUrl || index}`
|
||||
@@ -245,8 +239,6 @@ export default defineComponent({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
export type { AdTemplate } from '@/server/gen/proto/app/v1/common';
|
||||
export type { AdTemplate } from '@/server/api/proto/app/v1/common';
|
||||
export type {
|
||||
CreateAdTemplateRequest,
|
||||
DeleteAdTemplateRequest,
|
||||
UpdateAdTemplateRequest,
|
||||
} from '@/server/gen/proto/app/v1/catalog';
|
||||
} from '@/server/api/proto/app/v1/catalog';
|
||||
|
||||
@@ -1,512 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
||||
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
||||
import BillingHistorySection from '@/routes/settings/Billing/components/BillingHistorySection.vue';
|
||||
import BillingPlansSection from '@/routes/settings/Billing/components/BillingPlansSection.vue';
|
||||
import BillingTopupDialog from '@/routes/settings/Billing/components/BillingTopupDialog.vue';
|
||||
import BillingUsageSection from '@/routes/settings/Billing/components/BillingUsageSection.vue';
|
||||
import BillingWalletRow from '@/routes/settings/Billing/components/BillingWalletRow.vue';
|
||||
import type { Plan as ModelPlan, PaymentHistoryItem as PaymentHistoryApiItem } from '@/server/gen/proto/app/v1/common';
|
||||
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
||||
import type { Plan as ModelPlan } from '@/server/api/proto/app/v1/common';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useQuery } from '@pinia/colada';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import SettingsRow from '../components/SettingsRow.vue';
|
||||
import PaymentHistory from './components/PaymentHistory';
|
||||
import PlanSelection from './components/PlanSelection';
|
||||
import UpgradePlan from './components/UpgradePlan';
|
||||
|
||||
const TERM_OPTIONS = [1, 3, 6, 12] as const;
|
||||
type UpgradePaymentMethod = 'wallet' | 'topup';
|
||||
|
||||
type InvoiceDownloadResponse = {
|
||||
filename?: string;
|
||||
contentType?: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
type PaymentHistoryItem = {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
plan: string;
|
||||
status: string;
|
||||
invoiceId: string;
|
||||
currency: string;
|
||||
kind: string;
|
||||
details?: string[];
|
||||
};
|
||||
|
||||
type ApiErrorPayload = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
|
||||
const toast = useAppToast();
|
||||
const auth = useAuthStore();
|
||||
const { t, i18next } = useTranslation();
|
||||
|
||||
const { data: plansResponse, isLoading } = useQuery({
|
||||
key: () => ['billing-plans'],
|
||||
query: () => rpcClient.listPlans(),
|
||||
});
|
||||
const { data: usageSnapshot, refetch: refetchUsage } = useUsageQuery();
|
||||
const { refetch: refetchUsage } = useUsageQuery();
|
||||
|
||||
const topupDialogVisible = ref(false);
|
||||
const topupAmount = ref<number | null>(null);
|
||||
const topupLoading = ref(false);
|
||||
const historyLoading = ref(false);
|
||||
const downloadingInvoiceId = ref<string | null>(null);
|
||||
const topupPresets = [10, 20, 50, 100];
|
||||
const paymentHistory = ref<PaymentHistoryItem[]>([]);
|
||||
|
||||
const upgradeDialogVisible = ref(false);
|
||||
const selectedPlan = ref<ModelPlan | null>(null);
|
||||
const selectedTermMonths = ref<number>(1);
|
||||
const selectedPaymentMethod = ref<UpgradePaymentMethod>('wallet');
|
||||
const purchaseTopupAmount = ref<number | null>(null);
|
||||
const purchaseLoading = ref(false);
|
||||
const purchaseError = ref<string | null>(null);
|
||||
|
||||
const plans = computed(() => plansResponse.value?.plans || [] as ModelPlan[]);
|
||||
|
||||
const currentPlanId = computed(() => auth.user?.plan_id || undefined);
|
||||
const currentPlan = computed(() => plans.value.find(plan => plan.id === currentPlanId.value));
|
||||
const currentPlanName = computed(() => currentPlan.value?.name || t('settings.billing.unknownPlan'));
|
||||
const selectedPlanId = computed(() => (upgradeDialogVisible.value ? selectedPlan.value?.id || '' : ''));
|
||||
const walletBalance = computed(() => auth.user?.wallet_balance || 0);
|
||||
const storageUsed = computed(() => usageSnapshot.value?.totalStorage ?? 0);
|
||||
const uploadsUsed = computed(() => usageSnapshot.value?.totalVideos ?? 0);
|
||||
const storageLimit = computed(() => {
|
||||
const activePlan = plans.value.find(plan => plan.id === currentPlanId.value);
|
||||
return activePlan?.storageLimit || 10737418240;
|
||||
});
|
||||
const uploadsLimit = computed(() => {
|
||||
const activePlan = plans.value.find(plan => plan.id === currentPlanId.value);
|
||||
return activePlan?.uploadLimit || 50;
|
||||
});
|
||||
const storagePercentage = computed(() =>
|
||||
Math.min(Math.round((storageUsed.value / storageLimit.value) * 100), 100),
|
||||
);
|
||||
const uploadsPercentage = computed(() =>
|
||||
Math.min(Math.round((uploadsUsed.value / uploadsLimit.value) * 100), 100),
|
||||
);
|
||||
|
||||
const localeTag = computed(() => i18next.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US');
|
||||
const currencyFormatter = computed(() => new Intl.NumberFormat(localeTag.value, {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
maximumFractionDigits: 2,
|
||||
}));
|
||||
const shortDateFormatter = computed(() => new Intl.DateTimeFormat(localeTag.value, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}));
|
||||
|
||||
const selectedPlanId = computed(() => upgradeDialogVisible.value ? selectedPlan.value?.id || null : null);
|
||||
const selectedPlanPrice = computed(() => selectedPlan.value?.price || 0);
|
||||
const selectedTotalAmount = computed(() => selectedPlanPrice.value * selectedTermMonths.value);
|
||||
const selectedShortfall = computed(() => Math.max(selectedTotalAmount.value - walletBalance.value, 0));
|
||||
const selectedNeedsTopup = computed(() => selectedShortfall.value > 0.000001);
|
||||
const canSubmitUpgrade = computed(() => {
|
||||
if (!selectedPlan.value?.id || purchaseLoading.value) return false;
|
||||
if (!selectedNeedsTopup.value) return true;
|
||||
if (selectedPaymentMethod.value !== 'topup') return false;
|
||||
return (purchaseTopupAmount.value || 0) >= selectedShortfall.value && (purchaseTopupAmount.value || 0) > 0;
|
||||
});
|
||||
const upgradeSubmitLabel = computed(() => {
|
||||
if (selectedNeedsTopup.value && selectedPaymentMethod.value === 'topup') {
|
||||
return t('settings.billing.upgradeDialog.topupAndUpgrade');
|
||||
}
|
||||
|
||||
return t('settings.billing.upgradeDialog.payWithWallet');
|
||||
});
|
||||
|
||||
const formatMoney = (amount: number) => currencyFormatter.value.format(amount);
|
||||
|
||||
const formatDuration = (seconds?: number) => {
|
||||
if (!seconds) return t('settings.billing.durationMinutes', { minutes: 0 });
|
||||
if (seconds < 0) return t('settings.billing.durationMinutes', { minutes: -1 }).replace("-1", "∞")
|
||||
return t('settings.billing.durationMinutes', { minutes: Math.floor(seconds / 60) });
|
||||
};
|
||||
|
||||
const formatHistoryDate = (value?: string) => {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '-';
|
||||
return shortDateFormatter.value.format(date);
|
||||
};
|
||||
|
||||
const formatTermLabel = (months: number) => t('settings.billing.termOption', { months });
|
||||
|
||||
const formatPaymentMethodLabel = (value?: string) => {
|
||||
switch ((value || '').toLowerCase()) {
|
||||
case 'topup':
|
||||
return t('settings.billing.paymentMethod.topup');
|
||||
case 'wallet':
|
||||
default:
|
||||
return t('settings.billing.paymentMethod.wallet');
|
||||
}
|
||||
};
|
||||
|
||||
const getPlanStorageText = (plan: ModelPlan) => t('settings.billing.planStorage', { storage: formatBytes(plan.storageLimit || 0) });
|
||||
const getPlanDurationText = (plan: ModelPlan) => t('settings.billing.planDuration', { duration: formatDuration(plan.durationLimit) });
|
||||
const getPlanUploadsText = (plan: ModelPlan) => t('settings.billing.planUploads', { count: plan.uploadLimit || 0 });
|
||||
|
||||
const getStatusStyles = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'bg-success/10 text-success';
|
||||
case 'failed':
|
||||
return 'bg-danger/10 text-danger';
|
||||
case 'pending':
|
||||
return 'bg-warning/10 text-warning';
|
||||
default:
|
||||
return 'bg-info/10 text-info';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
success: t('settings.billing.status.success'),
|
||||
failed: t('settings.billing.status.failed'),
|
||||
pending: t('settings.billing.status.pending'),
|
||||
};
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const normalizeHistoryStatus = (status?: string) => {
|
||||
switch ((status || '').toLowerCase()) {
|
||||
case 'success':
|
||||
case 'succeeded':
|
||||
case 'paid':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
case 'error':
|
||||
case 'canceled':
|
||||
case 'cancelled':
|
||||
return 'failed';
|
||||
case 'pending':
|
||||
case 'processing':
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
};
|
||||
|
||||
const getApiErrorPayload = (error: unknown): ApiErrorPayload | null => {
|
||||
if (!error || typeof error !== 'object') return null;
|
||||
const candidate = error as { error?: ApiErrorPayload; data?: ApiErrorPayload; message?: string };
|
||||
|
||||
if (candidate.error && typeof candidate.error === 'object') return candidate.error;
|
||||
if (candidate.data && typeof candidate.data === 'object') return candidate.data;
|
||||
if (candidate.message) return { message: candidate.message };
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const getApiErrorMessage = (error: unknown, fallback: string) => {
|
||||
const payload = getApiErrorPayload(error);
|
||||
return payload?.message || fallback;
|
||||
};
|
||||
|
||||
const getApiErrorData = (error: unknown) => getApiErrorPayload(error)?.data || null;
|
||||
|
||||
const mapHistoryItem = (item: PaymentHistoryApiItem): PaymentHistoryItem => {
|
||||
const details: string[] = [];
|
||||
|
||||
if (item.kind !== 'wallet_topup' && item.termMonths) {
|
||||
details.push(formatTermLabel(item.termMonths));
|
||||
}
|
||||
if (item.kind !== 'wallet_topup' && item.paymentMethod) {
|
||||
details.push(formatPaymentMethodLabel(item.paymentMethod));
|
||||
}
|
||||
if (item.kind !== 'wallet_topup' && item.expiresAt) {
|
||||
details.push(t('settings.billing.history.validUntil', { date: formatHistoryDate(item.expiresAt) }));
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id || '',
|
||||
date: formatHistoryDate(item.createdAt),
|
||||
amount: item.amount || 0,
|
||||
plan: item.kind === 'wallet_topup'
|
||||
? t('settings.billing.walletTopup')
|
||||
: (item.planName || t('settings.billing.unknownPlan')),
|
||||
status: normalizeHistoryStatus(item.status),
|
||||
invoiceId: item.invoiceId || '-',
|
||||
currency: item.currency || 'USD',
|
||||
kind: item.kind || 'subscription',
|
||||
details,
|
||||
};
|
||||
};
|
||||
|
||||
const loadPaymentHistory = async () => {
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const response = await rpcClient.listPaymentHistory();
|
||||
paymentHistory.value = (response.payments || []).map(mapHistoryItem);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
paymentHistory.value = [];
|
||||
} finally {
|
||||
historyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const refreshBillingState = async () => {
|
||||
await Promise.allSettled([
|
||||
auth.fetchMe(),
|
||||
loadPaymentHistory(),
|
||||
refetchUsage(),
|
||||
]);
|
||||
};
|
||||
|
||||
void loadPaymentHistory();
|
||||
|
||||
const subscriptionSummary = computed(() => {
|
||||
const expiresAt = auth.user?.planExpiresAt || auth.user?.plan_expires_at;
|
||||
const formattedDate = formatHistoryDate(expiresAt);
|
||||
|
||||
if (auth.user?.plan_id) {
|
||||
if (auth.user?.plan_expiring_soon && expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.expiringTitle'),
|
||||
description: t('settings.billing.subscription.expiringDescription', {
|
||||
plan: currentPlanName.value,
|
||||
date: formattedDate,
|
||||
}),
|
||||
tone: 'warning' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.activeTitle'),
|
||||
description: t('settings.billing.subscription.activeDescription', {
|
||||
plan: currentPlanName.value,
|
||||
date: formattedDate,
|
||||
}),
|
||||
tone: 'default' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('settings.billing.subscription.activeTitle'),
|
||||
description: currentPlanName.value,
|
||||
tone: 'default' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.expiredTitle'),
|
||||
description: t('settings.billing.subscription.expiredDescription', { date: formattedDate }),
|
||||
tone: 'warning' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('settings.billing.subscription.freeTitle'),
|
||||
description: t('settings.billing.subscription.freeDescription'),
|
||||
tone: 'default' as const,
|
||||
};
|
||||
});
|
||||
|
||||
const resetUpgradeState = () => {
|
||||
selectedPlan.value = null;
|
||||
selectedTermMonths.value = 1;
|
||||
selectedPaymentMethod.value = 'wallet';
|
||||
purchaseTopupAmount.value = null;
|
||||
purchaseError.value = null;
|
||||
};
|
||||
|
||||
const openUpgradeDialog = (plan: ModelPlan) => {
|
||||
selectedPlan.value = plan;
|
||||
selectedTermMonths.value = 1;
|
||||
purchaseError.value = null;
|
||||
selectedPaymentMethod.value = walletBalance.value >= (plan.price || 0) ? 'wallet' : 'topup';
|
||||
purchaseTopupAmount.value = null;
|
||||
upgradeDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const closeUpgradeDialog = () => {
|
||||
if (purchaseLoading.value) return;
|
||||
upgradeDialogVisible.value = false;
|
||||
resetUpgradeState();
|
||||
selectedPlan.value = null;
|
||||
};
|
||||
|
||||
const onUpgradeDialogVisibilityChange = (visible: boolean) => {
|
||||
if (visible) {
|
||||
upgradeDialogVisible.value = true;
|
||||
return;
|
||||
}
|
||||
const handleUpgradeVisibilityChange = (visible: boolean) => {
|
||||
upgradeDialogVisible.value = visible;
|
||||
|
||||
closeUpgradeDialog();
|
||||
};
|
||||
|
||||
watch(selectedShortfall, (value) => {
|
||||
if (!upgradeDialogVisible.value) return;
|
||||
|
||||
if (value <= 0) {
|
||||
selectedPaymentMethod.value = 'wallet';
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.value === 'topup' && ((purchaseTopupAmount.value || 0) < value)) {
|
||||
purchaseTopupAmount.value = Number(value.toFixed(2));
|
||||
}
|
||||
});
|
||||
|
||||
const selectUpgradePaymentMethod = (method: UpgradePaymentMethod) => {
|
||||
selectedPaymentMethod.value = method;
|
||||
purchaseError.value = null;
|
||||
|
||||
if (method === 'topup' && selectedShortfall.value > 0 && ((purchaseTopupAmount.value || 0) < selectedShortfall.value)) {
|
||||
purchaseTopupAmount.value = Number(selectedShortfall.value.toFixed(2));
|
||||
if (!visible) {
|
||||
selectedPlan.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const updatePurchaseTopupAmount = (value: string | number | null) => {
|
||||
if (typeof value === 'number' || value === null) {
|
||||
purchaseTopupAmount.value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
purchaseTopupAmount.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
purchaseTopupAmount.value = Number.isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
const submitUpgrade = async () => {
|
||||
if (!selectedPlan.value?.id) return;
|
||||
|
||||
purchaseLoading.value = true;
|
||||
purchaseError.value = null;
|
||||
|
||||
try {
|
||||
const paymentMethod: UpgradePaymentMethod = selectedNeedsTopup.value ? selectedPaymentMethod.value : 'wallet';
|
||||
const payload: Parameters<typeof rpcClient.createPayment>[0] = {
|
||||
planId: selectedPlan.value.id,
|
||||
termMonths: selectedTermMonths.value,
|
||||
paymentMethod: paymentMethod,
|
||||
};
|
||||
|
||||
if (paymentMethod === 'topup') {
|
||||
payload.topupAmount = purchaseTopupAmount.value || selectedShortfall.value;
|
||||
}
|
||||
|
||||
await rpcClient.createPayment(payload);
|
||||
await refreshBillingState();
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.subscriptionSuccessSummary'),
|
||||
detail: t('settings.billing.toast.subscriptionSuccessDetail', {
|
||||
plan: selectedPlan.value.name || '',
|
||||
term: formatTermLabel(selectedTermMonths.value),
|
||||
}),
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
closeUpgradeDialog();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
const errorData = getApiErrorData(error);
|
||||
const nextShortfall = typeof errorData?.shortfall === 'number'
|
||||
? errorData.shortfall
|
||||
: selectedShortfall.value;
|
||||
|
||||
if (nextShortfall > 0) {
|
||||
selectedPaymentMethod.value = 'topup';
|
||||
if ((purchaseTopupAmount.value || 0) < nextShortfall) {
|
||||
purchaseTopupAmount.value = Number(nextShortfall.toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
purchaseError.value = getApiErrorMessage(error, t('settings.billing.toast.subscriptionFailedDetail'));
|
||||
} finally {
|
||||
purchaseLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleTopup = async (amount: number) => {
|
||||
topupLoading.value = true;
|
||||
try {
|
||||
await rpcClient.topupWallet({ amount });
|
||||
await refreshBillingState();
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.topupSuccessSummary'),
|
||||
detail: t('settings.billing.toast.topupSuccessDetail', { amount: formatMoney(amount) }),
|
||||
life: 3000,
|
||||
});
|
||||
topupDialogVisible.value = false;
|
||||
topupAmount.value = null;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('settings.billing.toast.topupFailedSummary'),
|
||||
detail: getApiErrorMessage(error, t('settings.billing.toast.topupFailedDetail')),
|
||||
life: 5000,
|
||||
});
|
||||
} finally {
|
||||
topupLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadInvoice = async (item: PaymentHistoryItem) => {
|
||||
if (!item.id) return;
|
||||
|
||||
downloadingInvoiceId.value = item.id;
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: t('settings.billing.toast.downloadingSummary'),
|
||||
detail: t('settings.billing.toast.downloadingDetail', { invoiceId: item.invoiceId }),
|
||||
life: 2000,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await rpcClient.downloadInvoice({ id: item.id }) as InvoiceDownloadResponse;
|
||||
const content = response.content || '';
|
||||
const contentType = response.contentType || 'text/plain;charset=utf-8';
|
||||
const filename = response.filename || `${item.invoiceId}.txt`;
|
||||
const blob = new Blob([content], { type: contentType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.downloadedSummary'),
|
||||
detail: t('settings.billing.toast.downloadedDetail', { invoiceId: item.invoiceId }),
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('settings.billing.toast.downloadFailedSummary'),
|
||||
detail: getApiErrorMessage(error, t('settings.billing.toast.downloadFailedDetail')),
|
||||
life: 5000,
|
||||
});
|
||||
} finally {
|
||||
downloadingInvoiceId.value = null;
|
||||
}
|
||||
const handleUpgradeSuccess = async () => {
|
||||
await refreshBillingState();
|
||||
};
|
||||
|
||||
const openTopupDialog = () => {
|
||||
@@ -514,262 +62,46 @@ const openTopupDialog = () => {
|
||||
topupDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const selectPreset = (amount: number) => {
|
||||
topupAmount.value = amount;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsSectionCard
|
||||
:title="t('settings.content.billing.title')"
|
||||
:description="t('settings.content.billing.subtitle')"
|
||||
>
|
||||
<BillingWalletRow
|
||||
:title="t('settings.billing.walletBalance')"
|
||||
:description="t('settings.billing.currentBalance', { balance: formatMoney(walletBalance) })"
|
||||
:button-label="t('settings.billing.topUp')"
|
||||
:subscription-title="subscriptionSummary.title"
|
||||
:subscription-description="subscriptionSummary.description"
|
||||
:subscription-tone="subscriptionSummary.tone"
|
||||
@topup="openTopupDialog"
|
||||
/>
|
||||
<SettingsSectionCard :title="$t('settings.content.billing.title')"
|
||||
:description="$t('settings.content.billing.subtitle')">
|
||||
<SettingsRow :title="$t('settings.billing.walletBalance')"
|
||||
:description="$t('settings.billing.currentBalance', { balance: auth.formatMoney(walletBalance) })"
|
||||
iconBoxClass="bg-primary/10">
|
||||
<template #icon>
|
||||
<CoinsIcon filled class="w-5 h-5 text-primary" />
|
||||
</template>
|
||||
|
||||
<BillingPlansSection
|
||||
:title="t('settings.billing.availablePlans')"
|
||||
:description="t('settings.billing.availablePlansHint')"
|
||||
:is-loading="isLoading"
|
||||
:plans="plans"
|
||||
<template #actions>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<AppButton size="sm" @click="openTopupDialog">
|
||||
<template #icon>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
</template>
|
||||
{{ $t('settings.billing.topUp') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsRow>
|
||||
|
||||
<PlanSelection
|
||||
:current-plan-id="currentPlanId"
|
||||
:selecting-plan-id="selectedPlanId"
|
||||
:format-money="formatMoney"
|
||||
:get-plan-storage-text="getPlanStorageText"
|
||||
:get-plan-duration-text="getPlanDurationText"
|
||||
:get-plan-uploads-text="getPlanUploadsText"
|
||||
:current-plan-label="t('settings.billing.currentPlan')"
|
||||
:selecting-label="t('settings.billing.upgradeDialog.selecting')"
|
||||
:choose-label="t('settings.billing.upgradeDialog.choosePlan')"
|
||||
@select="openUpgradeDialog"
|
||||
/>
|
||||
|
||||
<BillingUsageSection
|
||||
:storage-title="t('settings.billing.storage')"
|
||||
:storage-description="t('settings.billing.storageUsedOfLimit', { used: formatBytes(storageUsed), limit: formatBytes(storageLimit) })"
|
||||
:storage-percentage="storagePercentage"
|
||||
:uploads-title="t('settings.billing.totalVideos')"
|
||||
:uploads-description="t('settings.billing.totalVideosUsedOfLimit', { used: uploadsUsed, limit: uploadsLimit })"
|
||||
:uploads-percentage="uploadsPercentage"
|
||||
/>
|
||||
|
||||
<BillingHistorySection
|
||||
:title="t('settings.billing.paymentHistory')"
|
||||
:description="t('settings.billing.paymentHistorySubtitle')"
|
||||
:items="paymentHistory"
|
||||
:loading="historyLoading"
|
||||
:downloading-id="downloadingInvoiceId"
|
||||
:format-money="formatMoney"
|
||||
:get-status-styles="getStatusStyles"
|
||||
:get-status-label="getStatusLabel"
|
||||
:date-label="t('settings.billing.table.date')"
|
||||
:amount-label="t('settings.billing.table.amount')"
|
||||
:plan-label="t('settings.billing.table.plan')"
|
||||
:status-label="t('settings.billing.table.status')"
|
||||
:invoice-label="t('settings.billing.table.invoice')"
|
||||
:empty-label="t('settings.billing.noPaymentHistory')"
|
||||
:download-label="t('settings.billing.download')"
|
||||
@download="handleDownloadInvoice"
|
||||
:selected-plan-id="selectedPlanId"
|
||||
@upgrade="openUpgradeDialog"
|
||||
/>
|
||||
<BillingUsageSection />
|
||||
<PaymentHistory />
|
||||
</SettingsSectionCard>
|
||||
|
||||
<AppDialog
|
||||
<BillingTopupDialog v-model="topupDialogVisible" />
|
||||
|
||||
<UpgradePlan
|
||||
:visible="upgradeDialogVisible"
|
||||
:title="t('settings.billing.upgradeDialog.title')"
|
||||
maxWidthClass="max-w-2xl"
|
||||
@update:visible="onUpgradeDialogVisibilityChange"
|
||||
:selected-plan="selectedPlan"
|
||||
@update:visible="handleUpgradeVisibilityChange"
|
||||
@close="closeUpgradeDialog"
|
||||
>
|
||||
<div v-if="selectedPlan" class="space-y-5">
|
||||
<div class="rounded-lg border border-border bg-muted/20 p-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase tracking-[0.18em] text-foreground/50">
|
||||
{{ t('settings.billing.upgradeDialog.selectedPlan') }}
|
||||
</p>
|
||||
<h3 class="mt-1 text-lg font-semibold text-foreground">{{ selectedPlan.name }}</h3>
|
||||
<p class="mt-1 text-sm text-foreground/70">
|
||||
{{ selectedPlan.description || t('settings.billing.availablePlansHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-left md:text-right">
|
||||
<p class="text-xs text-foreground/50">{{ t('settings.billing.upgradeDialog.basePrice') }}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-foreground">{{ formatMoney(selectedPlan.price || 0) }}</p>
|
||||
<p class="text-xs text-foreground/60">{{ t('settings.billing.upgradeDialog.perMonthBase') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.billing.upgradeDialog.termTitle') }}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{{ t('settings.billing.upgradeDialog.termHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<button
|
||||
v-for="months in TERM_OPTIONS"
|
||||
:key="months"
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-lg border px-4 py-3 text-left transition-all',
|
||||
selectedTermMonths === months
|
||||
? 'border-primary bg-primary/5 text-primary'
|
||||
: 'border-border bg-header text-foreground hover:border-primary/30 hover:bg-muted/30',
|
||||
]"
|
||||
@click="selectedTermMonths = months"
|
||||
>
|
||||
<p class="text-sm font-medium">{{ formatTermLabel(months) }}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{{ formatMoney((selectedPlan.price || 0) * months) }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-border bg-header p-4">
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.totalLabel') }}</p>
|
||||
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(selectedTotalAmount) }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-header p-4">
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.walletBalanceLabel') }}</p>
|
||||
<p class="mt-2 text-xl font-semibold text-foreground">{{ formatMoney(walletBalance) }}</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border p-4"
|
||||
:class="selectedNeedsTopup
|
||||
? 'border-warning/30 bg-warning/10'
|
||||
: 'border-success/20 bg-success/5'"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">{{ t('settings.billing.upgradeDialog.shortfallLabel') }}</p>
|
||||
<p class="mt-2 text-xl font-semibold" :class="selectedNeedsTopup ? 'text-warning' : 'text-success'">
|
||||
{{ formatMoney(selectedShortfall) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedNeedsTopup" class="space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.billing.upgradeDialog.paymentMethodTitle') }}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{{ t('settings.billing.upgradeDialog.paymentMethodHint') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-lg border p-4 text-left transition-all',
|
||||
selectedPaymentMethod === 'wallet'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
|
||||
]"
|
||||
@click="selectUpgradePaymentMethod('wallet')"
|
||||
>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.billing.paymentMethod.wallet') }}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">
|
||||
{{ t('settings.billing.upgradeDialog.walletOptionDescription') }}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-lg border p-4 text-left transition-all',
|
||||
selectedPaymentMethod === 'topup'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
|
||||
]"
|
||||
@click="selectUpgradePaymentMethod('topup')"
|
||||
>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.billing.paymentMethod.topup') }}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">
|
||||
{{ t('settings.billing.upgradeDialog.topupOptionDescription', { shortfall: formatMoney(selectedShortfall) }) }}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-lg border border-success/20 bg-success/5 p-4 text-sm text-success">
|
||||
{{ t('settings.billing.upgradeDialog.walletCoveredHint') }}
|
||||
</div>
|
||||
|
||||
<div v-if="selectedNeedsTopup && selectedPaymentMethod === 'topup'" class="grid gap-2">
|
||||
<label class="text-sm font-medium text-foreground">{{ t('settings.billing.upgradeDialog.topupAmountLabel') }}</label>
|
||||
<AppInput
|
||||
:model-value="purchaseTopupAmount"
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
:placeholder="t('settings.billing.upgradeDialog.topupAmountPlaceholder')"
|
||||
@update:model-value="updatePurchaseTopupAmount"
|
||||
/>
|
||||
<p class="text-xs text-foreground/60">
|
||||
{{ t('settings.billing.upgradeDialog.topupAmountHint', { shortfall: formatMoney(selectedShortfall) }) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedNeedsTopup && selectedPaymentMethod === 'wallet'"
|
||||
class="rounded-lg border border-warning/30 bg-warning/10 p-4 text-sm text-warning"
|
||||
>
|
||||
{{ t('settings.billing.upgradeDialog.walletInsufficientHint', { shortfall: formatMoney(selectedShortfall) }) }}
|
||||
</div>
|
||||
|
||||
<div v-if="purchaseError" class="rounded-lg border border-danger bg-danger/10 p-4 text-sm text-danger">
|
||||
{{ purchaseError }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-xs text-foreground/60">
|
||||
{{ t('settings.billing.upgradeDialog.footerHint') }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
:disabled="purchaseLoading"
|
||||
@click="closeUpgradeDialog"
|
||||
>
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
size="sm"
|
||||
:loading="purchaseLoading"
|
||||
:disabled="!canSubmitUpgrade"
|
||||
@click="submitUpgrade"
|
||||
>
|
||||
{{ upgradeSubmitLabel }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AppDialog>
|
||||
|
||||
<BillingTopupDialog
|
||||
:visible="topupDialogVisible"
|
||||
:title="t('settings.billing.topupDialog.title')"
|
||||
:subtitle="t('settings.billing.topupDialog.subtitle')"
|
||||
:presets="topupPresets"
|
||||
:amount="topupAmount"
|
||||
:loading="topupLoading"
|
||||
:custom-amount-label="t('settings.billing.topupDialog.customAmount')"
|
||||
:amount-placeholder="t('settings.billing.topupDialog.enterAmount')"
|
||||
:hint="t('settings.billing.topupDialog.hint')"
|
||||
:cancel-label="t('common.cancel')"
|
||||
:proceed-label="t('settings.billing.topupDialog.proceed')"
|
||||
:format-money="formatMoney"
|
||||
@update:visible="topupDialogVisible = $event"
|
||||
@update:amount="topupAmount = $event"
|
||||
@selectPreset="selectPreset"
|
||||
@submit="handleTopup(topupAmount || 0)"
|
||||
@success="handleUpgradeSuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import DownloadIcon from '@/components/icons/DownloadIcon.vue';
|
||||
|
||||
type PaymentHistoryItem = {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
plan: string;
|
||||
status: string;
|
||||
invoiceId: string;
|
||||
currency: string;
|
||||
kind: string;
|
||||
details?: string[];
|
||||
};
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
description: string;
|
||||
items: PaymentHistoryItem[];
|
||||
loading?: boolean;
|
||||
downloadingId?: string | null;
|
||||
formatMoney: (amount: number) => string;
|
||||
getStatusStyles: (status: string) => string;
|
||||
getStatusLabel: (status: string) => string;
|
||||
dateLabel: string;
|
||||
amountLabel: string;
|
||||
planLabel: string;
|
||||
statusLabel: string;
|
||||
invoiceLabel: string;
|
||||
emptyLabel: string;
|
||||
downloadLabel: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'download', item: PaymentHistoryItem): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
||||
<DownloadIcon class="w-5 h-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ title }}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-border rounded-lg overflow-hidden">
|
||||
<div class="grid grid-cols-12 gap-4 px-4 py-3 text-xs font-medium text-foreground/60 uppercase tracking-wider bg-muted/30">
|
||||
<div class="col-span-3">{{ dateLabel }}</div>
|
||||
<div class="col-span-2">{{ amountLabel }}</div>
|
||||
<div class="col-span-3">{{ planLabel }}</div>
|
||||
<div class="col-span-2">{{ statusLabel }}</div>
|
||||
<div class="col-span-2 text-right">{{ invoiceLabel }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="px-4 py-6 space-y-3">
|
||||
<div v-for="index in 3" :key="index" class="grid grid-cols-12 gap-4 items-center animate-pulse">
|
||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-6 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-8 rounded bg-muted/50" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="items.length === 0" class="text-center py-12 text-foreground/60">
|
||||
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
||||
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
||||
</div>
|
||||
<p>{{ emptyLabel }}</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
class="grid grid-cols-12 gap-4 px-4 py-3 items-center hover:bg-muted/30 transition-all border-t border-border"
|
||||
>
|
||||
<div class="col-span-3">
|
||||
<p class="text-sm font-medium text-foreground">{{ item.date }}</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm text-foreground">{{ formatMoney(item.amount) }}</p>
|
||||
</div>
|
||||
<div class="col-span-3">
|
||||
<p class="text-sm text-foreground">{{ item.plan }}</p>
|
||||
<p v-if="item.details?.length" class="mt-1 text-xs text-foreground/60">
|
||||
{{ item.details.join(' · ') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span :class="`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(item.status)}`">
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-span-2 flex justify-end">
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all disabled:opacity-60 disabled:cursor-wait"
|
||||
:disabled="downloadingId === item.id"
|
||||
@click="emit('download', item)"
|
||||
>
|
||||
<DownloadIcon class="w-4 h-4" />
|
||||
<span>{{ downloadingId === item.id ? '...' : downloadLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,98 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
|
||||
import type { Plan as ModelPlan } from '@/server/gen/proto/app/v1/common';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
description: string;
|
||||
isLoading: boolean;
|
||||
plans: ModelPlan[];
|
||||
currentPlanId?: string;
|
||||
selectingPlanId?: string | null;
|
||||
formatMoney: (amount: number) => string;
|
||||
getPlanStorageText: (plan: ModelPlan) => string;
|
||||
getPlanDurationText: (plan: ModelPlan) => string;
|
||||
getPlanUploadsText: (plan: ModelPlan) => string;
|
||||
currentPlanLabel: string;
|
||||
selectingLabel: string;
|
||||
chooseLabel: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', plan: ModelPlan): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-10 h-10 rounded-md bg-primary/10 flex items-center justify-center shrink-0">
|
||||
<CreditCardIcon class="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ title }}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div v-for="i in 3" :key="i">
|
||||
<div class="h-[200px] rounded-lg bg-muted/50 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="plan in plans.sort((a,b) => (a.price || 0) - (b.price || 0))"
|
||||
:key="plan.id"
|
||||
:class="[
|
||||
'border rounded-lg p-4 hover:bg-muted/30 transition-all flex flex-col',
|
||||
plan.id === currentPlanId ? 'border-primary/40 bg-primary/5' : 'border-border',
|
||||
]"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">{{ plan.name }}</h3>
|
||||
<span
|
||||
v-if="plan.id === currentPlanId"
|
||||
class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-[11px] font-medium text-primary"
|
||||
>
|
||||
{{ currentPlanLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm text-foreground/60 mt-1 min-h-[2.5rem]">{{ plan.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<span class="text-2xl font-bold text-foreground">{{ formatMoney(plan.price || 0) }}</span>
|
||||
<span class="text-foreground/60 text-sm"> / {{ $t('settings.billing.cycle.'+plan.cycle) }}</span>
|
||||
</div>
|
||||
<ul class="space-y-2 mb-4 text-sm">
|
||||
<li
|
||||
v-for="feature in plan.features || []"
|
||||
:key="feature"
|
||||
class="flex items-center gap-2 text-foreground/70"
|
||||
>
|
||||
<CheckIcon class="w-4 h-4 text-success shrink-0" />
|
||||
{{ feature }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
v-if="plan.id !== currentPlanId"
|
||||
:disabled="selectingPlanId === plan.id"
|
||||
:class="[
|
||||
'w-full py-2 px-4 rounded-md text-sm font-medium transition-all mt-a',
|
||||
selectingPlanId === plan.id
|
||||
? 'bg-muted/50 text-foreground/60 cursor-wait'
|
||||
: 'bg-primary text-white hover:bg-primary/90'
|
||||
]"
|
||||
@click="emit('select', plan)"
|
||||
>
|
||||
{{ selectingPlanId === plan.id ? selectingLabel : chooseLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,79 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { client } from '@/api/rpcclient';
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||
import { getApiErrorMessage } from '@/lib/utils';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
presets: number[];
|
||||
amount: number | null;
|
||||
loading: boolean;
|
||||
customAmountLabel: string;
|
||||
amountPlaceholder: string;
|
||||
hint: string;
|
||||
cancelLabel: string;
|
||||
proceedLabel: string;
|
||||
}>();
|
||||
const visible = defineModel<boolean>();
|
||||
const toast = useAppToast();
|
||||
const auth = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
const { refetch: refetchUsage } = useUsageQuery();
|
||||
|
||||
const topupDialogVisible = ref(false);
|
||||
const topupAmount = ref<number | null>(null);
|
||||
const topupLoading = ref(false);
|
||||
const topupPresets = [10, 20, 50, 100];
|
||||
|
||||
const refreshBillingState = async () => {
|
||||
await Promise.allSettled([
|
||||
auth.fetchMe(),
|
||||
refetchUsage(),
|
||||
]);
|
||||
};
|
||||
|
||||
const handleTopup = async (amount: number) => {
|
||||
topupLoading.value = true;
|
||||
try {
|
||||
await client.topupWallet({ amount });
|
||||
await refreshBillingState();
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.topupSuccessSummary'),
|
||||
detail: t('settings.billing.toast.topupSuccessDetail', { amount: auth.formatMoney(amount) }),
|
||||
life: 3000,
|
||||
});
|
||||
topupDialogVisible.value = false;
|
||||
topupAmount.value = null;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('settings.billing.toast.topupFailedSummary'),
|
||||
detail: getApiErrorMessage(error, t('settings.billing.toast.topupFailedDetail')),
|
||||
life: 5000,
|
||||
});
|
||||
} finally {
|
||||
topupLoading.value = false;
|
||||
visible.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'update:amount', value: number | null): void;
|
||||
(e: 'selectPreset', amount: number): void;
|
||||
(e: 'submit'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
:visible="visible"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
:title="title"
|
||||
:visible="visible!"
|
||||
@update:visible="visible = $event"
|
||||
:title="$t('settings.billing.topupDialog.title')"
|
||||
maxWidthClass="max-w-md"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-foreground/70">
|
||||
{{ subtitle }}
|
||||
{{ $t('settings.billing.topupDialog.subtitle') }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
v-for="preset in topupPresets"
|
||||
:key="preset"
|
||||
:class="[
|
||||
'py-2 px-3 rounded-md bg-header text-sm font-medium transition-all hover:bg-gray-500',
|
||||
amount === preset
|
||||
topupAmount === preset
|
||||
? 'bg-primary text-white'
|
||||
: 'bg-muted/50 text-foreground hover:bg-muted'
|
||||
]"
|
||||
@click="emit('selectPreset', preset)"
|
||||
@click="topupAmount = preset"
|
||||
>
|
||||
${{ preset }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-foreground">{{ customAmountLabel }}</label>
|
||||
<label class="text-sm font-medium text-foreground">{{ $t('settings.billing.topupDialog.customAmount') }}</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-semibold text-foreground">$</span>
|
||||
<AppInput
|
||||
:model-value="amount"
|
||||
:model-value="topupAmount"
|
||||
type="number"
|
||||
:placeholder="amountPlaceholder"
|
||||
:placeholder="$t('settings.billing.topupDialog.enterAmount')"
|
||||
inputClass="flex-1"
|
||||
min="1"
|
||||
step="1"
|
||||
@update:model-value="emit('update:amount', typeof $event === 'number' || $event === null
|
||||
@update:model-value="topupAmount = typeof $event === 'number' || $event === null
|
||||
? $event
|
||||
: ($event === '' ? null : Number($event)))"
|
||||
: ($event === '' ? null : Number($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-muted/30 rounded-md p-3 text-xs text-foreground/60">
|
||||
<p>{{ hint }}</p>
|
||||
<p>{{ $t('settings.billing.topupDialog.hint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,21 +115,21 @@ const emit = defineEmits<{
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
:disabled="loading"
|
||||
@click="emit('update:visible', false)"
|
||||
:disabled="topupLoading"
|
||||
@click="visible = false; topupAmount = null"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
{{ $t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
size="sm"
|
||||
:loading="loading"
|
||||
:disabled="!amount || amount < 1 || loading"
|
||||
@click="emit('submit')"
|
||||
:loading="topupLoading"
|
||||
:disabled="!topupAmount || topupAmount < 1 || topupLoading"
|
||||
@click="handleTopup(topupAmount!)"
|
||||
>
|
||||
<template #icon>
|
||||
<CheckIcon class="w-4 h-4" />
|
||||
</template>
|
||||
{{ proceedLabel }}
|
||||
{{ $t('settings.billing.topupDialog.proceed') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,53 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import ActivityIcon from '@/components/icons/ActivityIcon.vue';
|
||||
import UploadIcon from '@/components/icons/UploadIcon.vue';
|
||||
import HardDrive from '@/components/icons/hard-drive.vue';
|
||||
import Video from '@/components/icons/Video.vue';
|
||||
import { useUsageQuery } from '@/composables/useUsageQuery';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
storageTitle: string;
|
||||
storageDescription: string;
|
||||
storagePercentage: number;
|
||||
uploadsTitle: string;
|
||||
uploadsDescription: string;
|
||||
uploadsPercentage: number;
|
||||
}>();
|
||||
defineProps<{}>();
|
||||
const { data: usageSnapshot } = useUsageQuery();
|
||||
const dataList = computed(() => [
|
||||
{
|
||||
id: 'storage',
|
||||
title: 'settings.billing.storage',
|
||||
description: { key: 'settings.billing.storageUsedOfLimit', params: { used: formatBytes(usageSnapshot.value?.totalStorage ?? 0) } },
|
||||
icon: HardDrive,
|
||||
},
|
||||
{
|
||||
id: 'videos',
|
||||
title: 'settings.billing.totalVideos',
|
||||
description: { key: 'settings.billing.totalVideosUsedOfLimit', params: { used: usageSnapshot.value?.totalVideos ?? 0 } },
|
||||
icon: Video,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-6 py-4 hover:bg-muted/30 transition-all">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<div class="px-6 py-4 hover:bg-muted/30 transition-all grid grid-cols-1 md:grid-cols-2 gap-6 rounded-md">
|
||||
<div v-for="item in dataList" :key="item.id" class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
||||
<ActivityIcon class="w-5 h-5 text-accent" />
|
||||
<component :is="item.icon" filled class="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ storageTitle }}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{{ storageDescription }}</p>
|
||||
<div class="hover:underline">
|
||||
<p class="text-sm font-medium text-foreground">{{ $t(item.title) }}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{{ $t(item.description.key, item.description.params) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
||||
<div
|
||||
class="bg-primary h-full rounded-full transition-all duration-300"
|
||||
:style="{ width: `${storagePercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 hover:bg-muted/30 transition-all border-t border-border">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<div class="w-10 h-10 rounded-md bg-info/10 flex items-center justify-center shrink-0">
|
||||
<UploadIcon class="w-5 h-5 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ uploadsTitle }}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{{ uploadsDescription }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-muted/50 rounded-full overflow-hidden" style="height: 6px">
|
||||
<div
|
||||
class="bg-info h-full rounded-full transition-all duration-300"
|
||||
:style="{ width: `${uploadsPercentage}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import CoinsIcon from '@/components/icons/CoinsIcon.vue';
|
||||
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
||||
import SettingsRow from '@/routes/settings/components/SettingsRow.vue';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
description: string;
|
||||
buttonLabel: string;
|
||||
subscriptionTitle?: string;
|
||||
subscriptionDescription?: string;
|
||||
subscriptionTone?: 'default' | 'warning';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'topup'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsRow :title="title" :description="description" iconBoxClass="bg-primary/10">
|
||||
<template #icon>
|
||||
<CoinsIcon class="w-5 h-5 text-primary" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<!-- <div
|
||||
v-if="subscriptionTitle || subscriptionDescription"
|
||||
class="rounded-md border px-3 py-2 text-right"
|
||||
:class="subscriptionTone === 'warning'
|
||||
? 'border-warning/30 bg-warning/10 text-warning'
|
||||
: 'border-border bg-muted/30 text-foreground/70'"
|
||||
>
|
||||
<p v-if="subscriptionTitle" class="text-xs font-medium">{{ subscriptionTitle }}</p>
|
||||
<p v-if="subscriptionDescription" class="mt-0.5 text-xs">{{ subscriptionDescription }}</p>
|
||||
</div> -->
|
||||
|
||||
<AppButton size="sm" @click="emit('topup')">
|
||||
<template #icon>
|
||||
<PlusIcon class="w-4 h-4" />
|
||||
</template>
|
||||
{{ buttonLabel }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsRow>
|
||||
</template>
|
||||
294
src/routes/settings/Billing/components/PaymentHistory.tsx
Normal file
294
src/routes/settings/Billing/components/PaymentHistory.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
import { client } from "@/api/rpcclient";
|
||||
import DownloadIcon from "@/components/icons/DownloadIcon.vue";
|
||||
import ListIcon from "@/components/icons/ListIcon.vue";
|
||||
import AppButton from "@/components/ui/AppButton.vue";
|
||||
import BaseTable from "@/components/ui/BaseTable.vue";
|
||||
import { useAppToast } from "@/composables/useAppToast";
|
||||
import { getApiErrorMessage, getStatusStyles } from "@/lib/utils";
|
||||
import { PaymentHistoryItem } from "@/server/api/proto/app/v1/common";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useQuery } from "@pinia/colada";
|
||||
import type { ColumnDef } from "@tanstack/vue-table";
|
||||
import { useTranslation } from "i18next-vue";
|
||||
import { computed, defineComponent, ref } from "vue";
|
||||
|
||||
const pageSizeOptions = [5, 10, 20, 50] as const;
|
||||
|
||||
const normalizeHistoryStatus = (status?: string) => {
|
||||
switch ((status || '').toLowerCase()) {
|
||||
case 'success':
|
||||
case 'succeeded':
|
||||
case 'paid':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
case 'error':
|
||||
case 'canceled':
|
||||
case 'cancelled':
|
||||
return 'failed';
|
||||
case 'pending':
|
||||
case 'processing':
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
};
|
||||
|
||||
const PaymentHistory = defineComponent({
|
||||
name: 'PaymentHistory',
|
||||
setup() {
|
||||
const auth = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
const toast = useAppToast();
|
||||
const downloadingInvoiceId = ref<string | null>(null);
|
||||
const page = ref(1);
|
||||
const limit = ref(10);
|
||||
|
||||
const formatTermLabel = (months: number) => t('settings.billing.termOption', { months });
|
||||
const formatPaymentMethodLabel = (value?: string) => {
|
||||
switch ((value || '').toLowerCase()) {
|
||||
case 'topup':
|
||||
return t('settings.billing.paymentMethod.topup');
|
||||
case 'wallet':
|
||||
default:
|
||||
return t('settings.billing.paymentMethod.wallet');
|
||||
}
|
||||
};
|
||||
const mapHistoryItem = (item: PaymentHistoryItem) => {
|
||||
const details: string[] = [];
|
||||
|
||||
if (item.kind !== 'wallet_topup' && item.termMonths) {
|
||||
details.push(formatTermLabel(item.termMonths));
|
||||
}
|
||||
if (item.kind !== 'wallet_topup' && item.paymentMethod) {
|
||||
details.push(formatPaymentMethodLabel(item.paymentMethod));
|
||||
}
|
||||
if (item.kind !== 'wallet_topup' && item.expiresAt) {
|
||||
details.push(t('settings.billing.history.validUntil', { date: auth.formatHistoryDate(item.expiresAt) }));
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id || '',
|
||||
date: auth.formatHistoryDate(item.createdAt),
|
||||
amount: item.amount || 0,
|
||||
plan: item.kind === 'wallet_topup'
|
||||
? t('settings.billing.walletTopup')
|
||||
: (item.planName || t('settings.billing.unknownPlan')),
|
||||
status: normalizeHistoryStatus(item.status),
|
||||
invoiceId: item.invoiceId || '-',
|
||||
currency: item.currency || 'USD',
|
||||
kind: item.kind || 'subscription',
|
||||
details,
|
||||
};
|
||||
};
|
||||
const { data, isLoading } = useQuery({
|
||||
key: () => ['paymentHistory', page.value, limit.value],
|
||||
query: () => client.listPaymentHistory(page.value, limit.value).then(res => ({
|
||||
...res,
|
||||
items: (res.payments || []).map(mapHistoryItem)
|
||||
})),
|
||||
});
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((data.value?.total || 0) / (data.value?.limit || limit.value || 1))));
|
||||
|
||||
const handleDownloadInvoice = async (item: PaymentHistoryItem) => {
|
||||
if (!item.id) return;
|
||||
|
||||
downloadingInvoiceId.value = item.id;
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: t('settings.billing.toast.downloadingSummary'),
|
||||
detail: t('settings.billing.toast.downloadingDetail', { invoiceId: item.invoiceId }),
|
||||
life: 2000,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await client.downloadInvoice({ id: item.id });
|
||||
const content = response.content || '';
|
||||
const contentType = response.contentType || 'text/plain;charset=utf-8';
|
||||
const filename = response.filename || `${item.invoiceId}.txt`;
|
||||
const blob = new Blob([content], { type: contentType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.downloadedSummary'),
|
||||
detail: t('settings.billing.toast.downloadedDetail', { invoiceId: item.invoiceId }),
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('settings.billing.toast.downloadFailedSummary'),
|
||||
detail: getApiErrorMessage(error, t('settings.billing.toast.downloadFailedDetail')),
|
||||
life: 5000,
|
||||
});
|
||||
} finally {
|
||||
downloadingInvoiceId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<ReturnType<typeof mapHistoryItem>>[] = [
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: t('settings.billing.table.date'),
|
||||
cell: ({ getValue }) => (
|
||||
<p class="text-sm font-medium text-foreground">{getValue<string>()}</p>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'col-span-3',
|
||||
cellClass: 'col-span-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount',
|
||||
header: t('settings.billing.table.amount'),
|
||||
cell: ({ row }) => (
|
||||
<p class="text-sm text-foreground">{auth.formatMoney(row.original.amount)}</p>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'col-span-2',
|
||||
cellClass: 'col-span-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'plan',
|
||||
header: t('settings.billing.table.plan'),
|
||||
cell: ({ row }) => (
|
||||
<>
|
||||
<p class="text-sm text-foreground">{row.original.plan}</p>
|
||||
{row.original.details?.length ? (
|
||||
<p class="mt-1 text-xs text-foreground/60">
|
||||
{row.original.details.join(' · ')}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'col-span-3',
|
||||
cellClass: 'col-span-3',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: t('settings.billing.table.status'),
|
||||
cell: ({ row }) => (
|
||||
<span class={`inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium ${getStatusStyles(row.original.status)}`}>
|
||||
{t('settings.billing.status.' + row.original.status)}
|
||||
</span>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'col-span-2',
|
||||
cellClass: 'col-span-2',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'invoice',
|
||||
enableSorting: false,
|
||||
header: t('settings.billing.table.invoice'),
|
||||
cell: ({ row }) => (
|
||||
<button
|
||||
class="flex items-center gap-2 px-3 py-1.5 text-sm text-foreground/70 hover:text-foreground hover:bg-muted/50 rounded-md transition-all disabled:opacity-60 disabled:cursor-wait"
|
||||
disabled={downloadingInvoiceId.value === row.original.id}
|
||||
onClick={() => handleDownloadInvoice(row.original as PaymentHistoryItem)}
|
||||
>
|
||||
<DownloadIcon class="w-4 h-4" />
|
||||
<span>{downloadingInvoiceId.value === row.original.id ? '...' : t('settings.billing.download')}</span>
|
||||
</button>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'col-span-2 flex justify-center',
|
||||
cellClass: 'col-span-2 flex justify-center',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const previousPage = () => {
|
||||
if (!data.value?.hasPrev || isLoading.value) return;
|
||||
page.value -= 1;
|
||||
};
|
||||
const nextPage = () => {
|
||||
if (!data.value?.hasNext || isLoading.value) return;
|
||||
page.value += 1;
|
||||
};
|
||||
const changePageSize = (event: Event) => {
|
||||
const nextLimit = Number((event.target as HTMLSelectElement).value) || 10;
|
||||
if (nextLimit === limit.value) return;
|
||||
limit.value = nextLimit;
|
||||
page.value = 1;
|
||||
};
|
||||
|
||||
return () => (
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div class="w-10 h-10 rounded-md bg-accent/10 flex items-center justify-center shrink-0">
|
||||
<ListIcon filled class="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.paymentHistory')}</p>
|
||||
<p class="text-xs text-foreground/60 mt-0.5">{t('settings.billing.paymentHistorySubtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseTable
|
||||
data={data.value?.items || []}
|
||||
columns={columns}
|
||||
loading={isLoading.value}
|
||||
emptyText={t('settings.billing.noPaymentHistory')}
|
||||
>
|
||||
{{
|
||||
loading: () => (
|
||||
<div class="px-4 py-6 space-y-3">
|
||||
{Array.from({ length: 10 }).map((_, index) => (
|
||||
<div key={index} class="grid grid-cols-12 gap-4 items-center animate-pulse">
|
||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-3 h-4 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-6 rounded bg-muted/50" />
|
||||
<div class="col-span-2 h-8 rounded bg-muted/50" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
empty: () => (
|
||||
<div class="text-center py-12 text-foreground/60">
|
||||
<div class="w-16 h-16 rounded-full bg-muted/50 flex items-center justify-center mx-auto mb-4">
|
||||
<DownloadIcon class="w-8 h-8 text-foreground/40" />
|
||||
</div>
|
||||
<p>{t('settings.billing.noPaymentHistory')}</p>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</BaseTable>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-3 text-xs text-foreground/55 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>{t('common.page', { current: data.value?.page || page.value, total: totalPages.value })} · {data.value?.total || 0} {t('common.records')}</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<label class="flex items-center gap-2">
|
||||
<span>{t('common.rowsPerPage')}</span>
|
||||
<select
|
||||
class="rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground"
|
||||
value={String(limit.value)}
|
||||
onChange={changePageSize}
|
||||
>
|
||||
{pageSizeOptions.map((option) => (
|
||||
<option key={option} value={String(option)}>{option}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 xl:justify-end">
|
||||
<AppButton size="sm" variant="secondary" disabled={!data.value?.hasPrev || isLoading.value} onClick={previousPage}>{t('common.previous')}</AppButton>
|
||||
<AppButton size="sm" variant="secondary" disabled={!data.value?.hasNext || isLoading.value} onClick={nextPage}>{t('common.next')}</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
export default PaymentHistory;
|
||||
169
src/routes/settings/Billing/components/PlanSelection.tsx
Normal file
169
src/routes/settings/Billing/components/PlanSelection.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import Credit from '@/components/icons/Credit.vue';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Plan as ModelPlan } from '@/server/api/proto/app/v1/common';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useQuery } from '@pinia/colada';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
const PlanSelection = defineComponent({
|
||||
name: 'PlanSelection',
|
||||
props: {
|
||||
currentPlanId: String,
|
||||
selectedPlanId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
"upgrade": (plan: ModelPlan) => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { t } = useTranslation();
|
||||
const auth = useAuthStore();
|
||||
const { data, isLoading } = useQuery({
|
||||
key: () => ['billing-plans'],
|
||||
query: () => rpcClient.listPlans(),
|
||||
});
|
||||
|
||||
const currentPlanId = computed(() => props.currentPlanId || undefined);
|
||||
|
||||
const subscriptionSummary = computed(() => {
|
||||
const expiresAt = auth.user?.planExpiresAt || auth.user?.plan_expires_at;
|
||||
const formattedDate = auth.formatHistoryDate(expiresAt);
|
||||
const currentPlanName = data.value?.plans?.find((plan) => plan.id === currentPlanId.value)?.name
|
||||
|| t('settings.billing.subscription.unknownPlan');
|
||||
|
||||
if (currentPlanId.value) {
|
||||
if (auth.user?.plan_expiring_soon && expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.expiringTitle'),
|
||||
description: t('settings.billing.subscription.expiringDescription', {
|
||||
date: formattedDate,
|
||||
}),
|
||||
tone: 'warning' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.activeTitle'),
|
||||
description: t('settings.billing.subscription.activeDescription', {
|
||||
date: formattedDate,
|
||||
}),
|
||||
tone: 'default' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('settings.billing.subscription.activeTitle'),
|
||||
description: currentPlanName,
|
||||
tone: 'default' as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
return {
|
||||
title: t('settings.billing.subscription.expiredTitle'),
|
||||
description: t('settings.billing.subscription.expiredDescription', { date: formattedDate }),
|
||||
tone: 'warning' as const,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: t('settings.billing.subscription.freeTitle'),
|
||||
description: t('settings.billing.subscription.freeDescription'),
|
||||
tone: 'default' as const,
|
||||
};
|
||||
});
|
||||
|
||||
const sortedPlans = computed(() =>
|
||||
[...(data.value?.plans || [])].sort((a, b) => (a.price || 0) - (b.price || 0))
|
||||
);
|
||||
|
||||
const isCurrentPlan = (planId?: string) => planId === currentPlanId.value;
|
||||
const isSelectingPlan = (planId?: string) => planId === props.selectedPlanId;
|
||||
|
||||
return () => (
|
||||
<div class="px-6 py-4">
|
||||
<div class="mb-4 flex items-center gap-4">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-primary/10">
|
||||
<Credit filled class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.availablePlans')}</p>
|
||||
<p class="mt-0.5 text-xs text-foreground/60">{t('settings.billing.availablePlansHint')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading.value ? (
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i}>
|
||||
<div class="h-[200px] animate-pulse rounded-lg bg-muted/50"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{sortedPlans.value.map((plan) => (
|
||||
<div
|
||||
key={plan.id}
|
||||
class={[
|
||||
'flex flex-col rounded-lg border p-4 transition-all hover:bg-muted/30',
|
||||
isCurrentPlan(plan.id) ? 'border-primary/40 bg-primary/5' : 'border-border',
|
||||
]}
|
||||
>
|
||||
<div class="mb-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-lg font-semibold text-foreground">{plan.name}</h3>
|
||||
{isCurrentPlan(plan.id) && (
|
||||
<span class={cn('inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-[11px] font-medium text-primary', subscriptionSummary.value.tone === 'warning' && 'bg-warning/10 text-warning')}>
|
||||
{subscriptionSummary.value.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p class="mt-1 min-h-[2.5rem] text-sm text-foreground/60">{plan.description}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<span class="text-2xl font-bold text-foreground">{auth.formatMoney(plan.price || 0)}</span>
|
||||
<span class="text-sm text-foreground/60"> / {t(`settings.billing.cycle.${plan.cycle}`)}</span>
|
||||
</div>
|
||||
|
||||
<ul class="mb-4 space-y-2 text-sm">
|
||||
{(plan.features || []).map((feature: string) => (
|
||||
<li key={feature} class="flex items-center gap-2 text-foreground/70">
|
||||
<CheckIcon class="h-4 w-4 shrink-0 text-success" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{!isCurrentPlan(plan.id) && (
|
||||
<button
|
||||
disabled={isSelectingPlan(plan.id)}
|
||||
class={[
|
||||
'mt-auto w-full rounded-md px-4 py-2 text-sm font-medium transition-all',
|
||||
isSelectingPlan(plan.id)
|
||||
? 'cursor-wait bg-muted/50 text-foreground/60'
|
||||
: 'bg-primary text-white hover:bg-primary/90',
|
||||
]}
|
||||
onClick={() => emit('upgrade', plan)}
|
||||
>
|
||||
{isSelectingPlan(plan.id)
|
||||
? t('settings.billing.upgradeDialog.selecting')
|
||||
: t('settings.billing.upgradeDialog.choosePlan')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default PlanSelection;
|
||||
396
src/routes/settings/Billing/components/UpgradePlan.tsx
Normal file
396
src/routes/settings/Billing/components/UpgradePlan.tsx
Normal file
@@ -0,0 +1,396 @@
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, defineComponent, ref, watch, type PropType } from 'vue';
|
||||
|
||||
import { client } from '@/api/rpcclient';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { getApiErrorMessage, getApiErrorPayload } from '@/lib/utils';
|
||||
import type { Plan as ModelPlan } from '@/server/api/proto/app/v1/common';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const TERM_OPTIONS = [1, 3, 6, 12] as const;
|
||||
|
||||
type UpgradePaymentMethod = 'wallet' | 'topup';
|
||||
|
||||
const UpgradePlan = defineComponent({
|
||||
name: 'UpgradePlan',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
selectedPlan: {
|
||||
type: Object as PropType<ModelPlan | null>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
emits: {
|
||||
'update:visible': (_visible: boolean) => true,
|
||||
close: () => true,
|
||||
success: () => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const toast = useAppToast();
|
||||
const auth = useAuthStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const selectedTermMonths = ref<number>(1);
|
||||
const selectedPaymentMethod = ref<UpgradePaymentMethod>('wallet');
|
||||
const purchaseTopupAmount = ref<number | null>(null);
|
||||
const purchaseLoading = ref(false);
|
||||
const purchaseError = ref<string | null>(null);
|
||||
|
||||
const walletBalance = computed(() => auth.user?.wallet_balance || 0);
|
||||
const selectedPlanPrice = computed(() => props.selectedPlan?.price || 0);
|
||||
const selectedTotalAmount = computed(() => selectedPlanPrice.value * selectedTermMonths.value);
|
||||
const selectedShortfall = computed(() => Math.max(selectedTotalAmount.value - walletBalance.value, 0));
|
||||
const selectedNeedsTopup = computed(() => selectedShortfall.value > 0.000001);
|
||||
|
||||
const canSubmitUpgrade = computed(() => {
|
||||
if (!props.selectedPlan?.id || purchaseLoading.value) return false;
|
||||
if (!selectedNeedsTopup.value) return true;
|
||||
if (selectedPaymentMethod.value !== 'topup') return false;
|
||||
|
||||
const topupAmount = purchaseTopupAmount.value || 0;
|
||||
return topupAmount >= selectedShortfall.value && topupAmount > 0;
|
||||
});
|
||||
|
||||
const upgradeSubmitLabel = computed(() => {
|
||||
if (selectedNeedsTopup.value && selectedPaymentMethod.value === 'topup') {
|
||||
return t('settings.billing.upgradeDialog.topupAndUpgrade');
|
||||
}
|
||||
|
||||
return t('settings.billing.upgradeDialog.payWithWallet');
|
||||
});
|
||||
|
||||
const getApiErrorData = (error: unknown) => getApiErrorPayload(error)?.data || null;
|
||||
|
||||
const resetUpgradeState = (plan: ModelPlan | null = props.selectedPlan) => {
|
||||
const shortfall = Math.max((plan?.price || 0) - walletBalance.value, 0);
|
||||
const needsTopup = shortfall > 0.000001;
|
||||
|
||||
selectedTermMonths.value = 1;
|
||||
selectedPaymentMethod.value = needsTopup ? 'topup' : 'wallet';
|
||||
purchaseTopupAmount.value = needsTopup ? Number(shortfall.toFixed(2)) : null;
|
||||
purchaseLoading.value = false;
|
||||
purchaseError.value = null;
|
||||
};
|
||||
|
||||
const emitClose = () => {
|
||||
emit('update:visible', false);
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
if (purchaseLoading.value) return;
|
||||
emitClose();
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
resetUpgradeState(props.selectedPlan);
|
||||
return;
|
||||
}
|
||||
|
||||
resetUpgradeState(null);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.selectedPlan?.id,
|
||||
(planId, previousPlanId) => {
|
||||
if (!props.visible) return;
|
||||
if (planId === previousPlanId) return;
|
||||
resetUpgradeState(props.selectedPlan);
|
||||
},
|
||||
);
|
||||
|
||||
watch(selectedShortfall, (value) => {
|
||||
if (!props.visible) return;
|
||||
|
||||
if (value <= 0) {
|
||||
selectedPaymentMethod.value = 'wallet';
|
||||
purchaseTopupAmount.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedPaymentMethod.value === 'topup' && ((purchaseTopupAmount.value || 0) < value)) {
|
||||
purchaseTopupAmount.value = Number(value.toFixed(2));
|
||||
}
|
||||
});
|
||||
|
||||
const selectUpgradePaymentMethod = (method: UpgradePaymentMethod) => {
|
||||
selectedPaymentMethod.value = method;
|
||||
purchaseError.value = null;
|
||||
|
||||
if (method === 'topup' && selectedShortfall.value > 0 && ((purchaseTopupAmount.value || 0) < selectedShortfall.value)) {
|
||||
purchaseTopupAmount.value = Number(selectedShortfall.value.toFixed(2));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePurchaseTopupAmount = (value: string | number | null) => {
|
||||
if (typeof value === 'number' || value === null) {
|
||||
purchaseTopupAmount.value = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
purchaseTopupAmount.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = Number(value);
|
||||
purchaseTopupAmount.value = Number.isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
const submitUpgrade = async () => {
|
||||
if (!props.selectedPlan?.id) return;
|
||||
|
||||
purchaseLoading.value = true;
|
||||
purchaseError.value = null;
|
||||
|
||||
try {
|
||||
const paymentMethod: UpgradePaymentMethod = selectedNeedsTopup.value ? selectedPaymentMethod.value : 'wallet';
|
||||
const payload: Parameters<typeof client.createPayment>[0] = {
|
||||
planId: props.selectedPlan.id,
|
||||
termMonths: selectedTermMonths.value,
|
||||
paymentMethod,
|
||||
};
|
||||
|
||||
if (paymentMethod === 'topup') {
|
||||
payload.topupAmount = purchaseTopupAmount.value || selectedShortfall.value;
|
||||
}
|
||||
|
||||
await client.createPayment(payload);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('settings.billing.toast.subscriptionSuccessSummary'),
|
||||
detail: t('settings.billing.toast.subscriptionSuccessDetail', {
|
||||
plan: props.selectedPlan.name || '',
|
||||
term: t('settings.billing.termOption', { months: selectedTermMonths.value }),
|
||||
}),
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
emit('success');
|
||||
emitClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
const errorData = getApiErrorData(error);
|
||||
const nextShortfall = typeof errorData?.shortfall === 'number'
|
||||
? errorData.shortfall
|
||||
: selectedShortfall.value;
|
||||
|
||||
if (nextShortfall > 0) {
|
||||
selectedPaymentMethod.value = 'topup';
|
||||
if ((purchaseTopupAmount.value || 0) < nextShortfall) {
|
||||
purchaseTopupAmount.value = Number(nextShortfall.toFixed(2));
|
||||
}
|
||||
}
|
||||
|
||||
purchaseError.value = getApiErrorMessage(error, t('settings.billing.toast.subscriptionFailedDetail'));
|
||||
} finally {
|
||||
purchaseLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return () => (
|
||||
<AppDialog
|
||||
visible={props.visible}
|
||||
closable={!purchaseLoading.value}
|
||||
title={t('settings.billing.upgradeDialog.title')}
|
||||
maxWidthClass="max-w-2xl"
|
||||
onUpdate:visible={(visible: boolean) => { emit('update:visible', visible); }}
|
||||
onClose={() => { emit('close'); }}
|
||||
v-slots={{
|
||||
default: () => (
|
||||
<>
|
||||
{props.selectedPlan ? (
|
||||
<div class="space-y-5">
|
||||
<div class="rounded-lg border border-border bg-muted/20 p-4">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-medium uppercase tracking-[0.18em] text-foreground/50">
|
||||
{t('settings.billing.upgradeDialog.selectedPlan')}
|
||||
</p>
|
||||
<h3 class="mt-1 text-lg font-semibold text-foreground">{props.selectedPlan.name}</h3>
|
||||
<p class="mt-1 text-sm text-foreground/70">
|
||||
{props.selectedPlan.description || t('settings.billing.availablePlansHint')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="text-left md:text-right">
|
||||
<p class="text-xs text-foreground/50">{t('settings.billing.upgradeDialog.basePrice')}</p>
|
||||
<p class="mt-1 text-2xl font-semibold text-foreground">
|
||||
{auth.formatMoney(props.selectedPlan.price || 0)}
|
||||
</p>
|
||||
<p class="text-xs text-foreground/60">{t('settings.billing.upgradeDialog.perMonthBase')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.upgradeDialog.termTitle')}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{t('settings.billing.upgradeDialog.termHint')}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
{TERM_OPTIONS.map((months) => (
|
||||
<button
|
||||
key={months}
|
||||
type="button"
|
||||
class={[
|
||||
'rounded-lg border px-4 py-3 text-left transition-all',
|
||||
selectedTermMonths.value === months
|
||||
? 'border-primary bg-primary/5 text-primary'
|
||||
: 'border-border bg-header text-foreground hover:border-primary/30 hover:bg-muted/30',
|
||||
]}
|
||||
onClick={() => (selectedTermMonths.value = months)}
|
||||
>
|
||||
<p class="text-sm font-medium">{t('settings.billing.termOption', { months })}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">
|
||||
{auth.formatMoney((props.selectedPlan?.price || 0) * months)}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-border bg-header p-4">
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">
|
||||
{t('settings.billing.upgradeDialog.totalLabel')}
|
||||
</p>
|
||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
||||
{auth.formatMoney(selectedTotalAmount.value)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-border bg-header p-4">
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">
|
||||
{t('settings.billing.upgradeDialog.walletBalanceLabel')}
|
||||
</p>
|
||||
<p class="mt-2 text-xl font-semibold text-foreground">
|
||||
{auth.formatMoney(walletBalance.value)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class={[
|
||||
'rounded-lg border p-4',
|
||||
selectedNeedsTopup.value ? 'border-warning/30 bg-warning/10' : 'border-success/20 bg-success/5',
|
||||
]}
|
||||
>
|
||||
<p class="text-xs uppercase tracking-wide text-foreground/50">
|
||||
{t('settings.billing.upgradeDialog.shortfallLabel')}
|
||||
</p>
|
||||
<p class={[
|
||||
'mt-2 text-xl font-semibold',
|
||||
selectedNeedsTopup.value ? 'text-warning' : 'text-success',
|
||||
]}>
|
||||
{auth.formatMoney(selectedShortfall.value)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedNeedsTopup.value ? (
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">
|
||||
{t('settings.billing.upgradeDialog.paymentMethodTitle')}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{t('settings.billing.upgradeDialog.paymentMethodHint')}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
'rounded-lg border p-4 text-left transition-all',
|
||||
selectedPaymentMethod.value === 'wallet' ? 'border-primary bg-primary/5' : 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
|
||||
]}
|
||||
onClick={() => selectUpgradePaymentMethod('wallet')}
|
||||
>
|
||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.paymentMethod.wallet')}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">{t('settings.billing.upgradeDialog.walletOptionDescription')}</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={[
|
||||
'rounded-lg border p-4 text-left transition-all',
|
||||
selectedPaymentMethod.value === 'topup' ? 'border-primary bg-primary/5' : 'border-border bg-header hover:border-primary/30 hover:bg-muted/30',
|
||||
]}
|
||||
onClick={() => selectUpgradePaymentMethod('topup')}
|
||||
>
|
||||
<p class="text-sm font-medium text-foreground">{t('settings.billing.paymentMethod.topup')}</p>
|
||||
<p class="mt-1 text-xs text-foreground/60">
|
||||
{t('settings.billing.upgradeDialog.topupOptionDescription', { shortfall: auth.formatMoney(selectedShortfall.value) })}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="rounded-lg border border-success/20 bg-success/5 p-4 text-sm text-success">
|
||||
{t('settings.billing.upgradeDialog.walletCoveredHint')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedNeedsTopup.value && selectedPaymentMethod.value === 'topup' && (
|
||||
<div class="grid gap-2">
|
||||
<label class="text-sm font-medium text-foreground">{t('settings.billing.upgradeDialog.topupAmountLabel')}</label>
|
||||
<AppInput
|
||||
modelValue={purchaseTopupAmount.value}
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
placeholder={t('settings.billing.upgradeDialog.topupAmountPlaceholder')}
|
||||
onUpdate:modelValue={updatePurchaseTopupAmount}
|
||||
/>
|
||||
<p class="text-xs text-foreground/60">
|
||||
{t('settings.billing.upgradeDialog.topupAmountHint', { shortfall: auth.formatMoney(selectedShortfall.value) })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedNeedsTopup.value && selectedPaymentMethod.value === 'wallet' && (
|
||||
<div class="rounded-lg border border-warning/30 bg-warning/10 p-4 text-sm text-warning">
|
||||
{t('settings.billing.upgradeDialog.walletInsufficientHint', { shortfall: auth.formatMoney(selectedShortfall.value) })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{purchaseError.value && (
|
||||
<div class="rounded-lg border border-danger bg-danger/10 p-4 text-sm text-danger">
|
||||
{purchaseError.value}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
footer: () => (
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-xs text-foreground/60">{t('settings.billing.upgradeDialog.footerHint')}</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<AppButton variant="secondary" size="sm" disabled={purchaseLoading.value} onClick={closeDialog}>
|
||||
{t('common.cancel')}
|
||||
</AppButton>
|
||||
<AppButton size="sm" loading={purchaseLoading.value} disabled={!canSubmitUpgrade.value} onClick={submitUpgrade}>
|
||||
{upgradeSubmitLabel.value}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export default UpgradePlan;
|
||||
@@ -104,14 +104,11 @@ const handleClearData = () => {
|
||||
hoverClass="hover:bg-danger/5"
|
||||
>
|
||||
<template #icon>
|
||||
<AlertTriangleIcon class="w-5 h-5 text-danger" />
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-danger" viewBox="-10 -258 532 532"><path d="M256 264c141 0 256-115 256-256S397-248 256-248 0-133 0 8s115 256 256 256zM416-24v64H96v-64h320z" fill="currentColor"/></svg>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<AppButton variant="danger" size="sm" :loading="deletingAccount" :disabled="clearingData" @click="handleDeleteAccount">
|
||||
<template #icon>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</template>
|
||||
{{ t('settings.dangerZone.deleteAccount.button') }}
|
||||
</AppButton>
|
||||
</template>
|
||||
@@ -124,18 +121,11 @@ const handleClearData = () => {
|
||||
hoverClass="hover:bg-danger/5"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-danger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
|
||||
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
|
||||
</svg>
|
||||
<TrashIcon filled class="w-5 h-5 text-danger" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<AppButton variant="danger" size="sm" :loading="clearingData" :disabled="deletingAccount" @click="handleClearData">
|
||||
<template #icon>
|
||||
<SlidersIcon class="w-4 h-4" />
|
||||
</template>
|
||||
{{ t('settings.dangerZone.clearData.button') }}
|
||||
</AppButton>
|
||||
</template>
|
||||
|
||||
@@ -11,8 +11,8 @@ import DomainsDnsEmbedCode from './components/DomainsDnsEmbedCode.vue';
|
||||
import DomainsDnsNotices from './components/DomainsDnsNotices.vue';
|
||||
import DomainsDnsTable from './components/DomainsDnsTable.vue';
|
||||
import DomainsDnsToolbar from './components/DomainsDnsToolbar.vue';
|
||||
import { mapDomainItem, normalizeDomainInput } from './helpers';
|
||||
import type { DomainItem } from './types';
|
||||
import { normalizeDomainInput } from './helpers';
|
||||
import type { Domain } from '@/server/api/proto/app/v1/common';
|
||||
|
||||
const toast = useAppToast();
|
||||
const confirm = useAppConfirm();
|
||||
@@ -27,7 +27,7 @@ const { data: domainsSnapshot, error, isPending, refetch } = useQuery({
|
||||
key: () => ['settings', 'domains'],
|
||||
query: async () => {
|
||||
const response = await rpcClient.listDomains();
|
||||
return (response.domains || []).map(mapDomainItem);
|
||||
return (response.domains || []);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,16 +126,16 @@ const handleAddDomain = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveDomain = (domain: DomainItem) => {
|
||||
const handleRemoveDomain = (domain: Domain) => {
|
||||
confirm.require({
|
||||
message: t('settings.domainsDns.confirm.removeMessage', { domain: domain.name }),
|
||||
header: t('settings.domainsDns.confirm.removeHeader'),
|
||||
acceptLabel: t('settings.domainsDns.confirm.removeAccept'),
|
||||
rejectLabel: t('settings.domainsDns.confirm.removeReject'),
|
||||
accept: async () => {
|
||||
removingId.value = domain.id;
|
||||
removingId.value = domain.id!;
|
||||
try {
|
||||
await rpcClient.deleteDomain({ id: domain.id });
|
||||
await rpcClient.deleteDomain({ id: domain.id! });
|
||||
await refetch();
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
|
||||
@@ -3,31 +3,31 @@ import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import { formatDate } from '@/lib/utils';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
import type { Domain } from '@/server/api/proto/app/v1/common';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, h } from 'vue';
|
||||
import type { DomainItem } from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
domains: DomainItem[];
|
||||
domains: Domain[];
|
||||
isInitialLoading: boolean;
|
||||
adding: boolean;
|
||||
removingId: string | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', domain: DomainItem): void;
|
||||
(e: 'remove', domain: Domain): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
const columns = computed<ColumnDef<Domain>[]>(() => [
|
||||
{
|
||||
id: 'domain',
|
||||
header: t('settings.domainsDns.table.domain'),
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => h('div', { class: 'flex items-center gap-2' }, [
|
||||
cell: ({ row, getValue }) => h('div', { class: 'flex items-center gap-2' }, [
|
||||
h(LinkIcon, { class: 'h-4 w-4 text-foreground/40' }),
|
||||
h('span', { class: 'text-sm font-medium text-foreground' }, row.original.name),
|
||||
]),
|
||||
@@ -39,8 +39,8 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
{
|
||||
id: 'addedAt',
|
||||
header: t('settings.domainsDns.table.addedDate'),
|
||||
accessorFn: row => row.addedAt,
|
||||
cell: ({ row }) => h('span', { class: 'text-sm text-foreground/60' }, row.original.addedAt),
|
||||
accessorFn: row => formatDate(row.createdAt),
|
||||
cell: ({ getValue }) => h('span', { class: 'text-sm text-foreground/60' }, getValue<string>()),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
@@ -58,22 +58,19 @@ const columns = computed<ColumnDef<DomainItem>[]>(() => [
|
||||
}, {
|
||||
icon: () => h(TrashIcon, { class: 'h-4 w-4 text-danger' }),
|
||||
}),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-right',
|
||||
},
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsTableSkeleton v-if="isInitialLoading" :columns="3" :rows="4" />
|
||||
|
||||
<BaseTable
|
||||
v-else
|
||||
:data="domains"
|
||||
:columns="columns"
|
||||
:get-row-id="(row) => row.id"
|
||||
:loading="isInitialLoading"
|
||||
:skeleton-rows="4"
|
||||
:get-row-id="(row) => row.id!"
|
||||
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||
tableClass="w-full"
|
||||
headerRowClass="bg-muted/30"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { DomainApiItem, DomainItem } from './types';
|
||||
|
||||
export const normalizeDomainInput = (value: string) => value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
@@ -7,19 +5,19 @@ export const normalizeDomainInput = (value: string) => value
|
||||
.replace(/^www\./, '')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
export const formatDate = (value?: string) => {
|
||||
if (!value) return '-';
|
||||
// export const formatDate = (value?: string) => {
|
||||
// if (!value) return '-';
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value.split('T')[0] || value;
|
||||
}
|
||||
// const date = new Date(value);
|
||||
// if (Number.isNaN(date.getTime())) {
|
||||
// return value.split('T')[0] || value;
|
||||
// }
|
||||
|
||||
return date.toISOString().split('T')[0];
|
||||
};
|
||||
// return date.toISOString().split('T')[0];
|
||||
// };
|
||||
|
||||
export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
|
||||
id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
|
||||
name: item.name || '',
|
||||
addedAt: formatDate(item.created_at),
|
||||
});
|
||||
// export const mapDomainItem = (item: DomainApiItem): DomainItem => ({
|
||||
// id: item.id || `${item.name || 'domain'}:${item.created_at || ''}`,
|
||||
// name: item.name || '',
|
||||
// addedAt: formatDate(item.created_at),
|
||||
// });
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export type DomainApiItem = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
created_at?: string;
|
||||
};
|
||||
|
||||
export type DomainItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
addedAt: string;
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import SettingsRowSkeleton from '@/routes/settings/components/SettingsRowSkeleto
|
||||
import SettingsSectionCard from '@/routes/settings/components/SettingsSectionCard.vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import Bell from '@/components/icons/Bell.vue';
|
||||
|
||||
const toast = useAppToast();
|
||||
const { t } = useTranslation();
|
||||
@@ -40,7 +41,7 @@ const notificationTypes = computed(() => [
|
||||
key: 'push' as const,
|
||||
title: t('settings.notificationSettings.types.push.title'),
|
||||
description: t('settings.notificationSettings.types.push.description'),
|
||||
icon: BellIcon,
|
||||
icon: Bell,
|
||||
bgColor: 'bg-accent/10',
|
||||
iconColor: 'text-accent',
|
||||
},
|
||||
@@ -140,7 +141,7 @@ const handleSave = async () => {
|
||||
:iconBoxClass="type.bgColor"
|
||||
>
|
||||
<template #icon>
|
||||
<component :is="type.icon" :class="[type.iconColor, 'w-5 h-5']" />
|
||||
<component :is="type.icon" filled :class="[type.iconColor, 'w-5 h-5']" />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="tsx">
|
||||
import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
@@ -8,9 +8,9 @@ import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import SettingsTableSkeleton from '@/routes/settings/components/SettingsTableSkeleton.vue';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, h } from 'vue';
|
||||
import PlayerConfigSettingsBadges from './PlayerConfigSettingsBadges.vue';
|
||||
import { computed } from 'vue';
|
||||
import type { PlayerConfig } from '../types';
|
||||
import PlayerConfigSettingsBadges from './PlayerConfigSettingsBadges.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
configs: PlayerConfig[];
|
||||
@@ -37,19 +37,30 @@ const columns = computed<ColumnDef<PlayerConfig>[]>(() => [
|
||||
id: 'config',
|
||||
header: t('settings.playerConfigs.table.name'),
|
||||
accessorFn: row => row.name,
|
||||
cell: ({ row }) => h('div', [
|
||||
h('div', { class: 'flex flex-wrap items-center gap-2' }, [
|
||||
h('span', { class: 'text-sm font-medium text-foreground cursor-pointer hover:underline', onClick: () => emit('edit', row.original) }, row.original.name),
|
||||
row.original.isDefault
|
||||
? h('span', {
|
||||
class: 'inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary',
|
||||
}, t('settings.playerConfigs.defaultBadge'))
|
||||
: null,
|
||||
]),
|
||||
row.original.description
|
||||
? h('p', { class: 'mt-0.5 text-xs text-foreground/50' }, row.original.description)
|
||||
: h('p', { class: 'mt-0.5 text-xs text-foreground/40' }, t('settings.playerConfigs.createdOn', { date: row.original.createdAt || '-' })),
|
||||
]),
|
||||
cell: ({ row }) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="text-sm font-medium text-foreground cursor-pointer hover:underline"
|
||||
onClick={() => emit('edit', row.original)}
|
||||
>
|
||||
{row.original.name}
|
||||
</span>
|
||||
{row.original.isDefault && (
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
|
||||
{t('settings.playerConfigs.defaultBadge')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{row.original.description ? (
|
||||
<p class="mt-0.5 text-xs text-foreground/50">{row.original.description}</p>
|
||||
) : (
|
||||
<p class="mt-0.5 text-xs text-foreground/40">
|
||||
{t('settings.playerConfigs.createdOn', { date: row.original.createdAt || '-' })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
@@ -69,7 +80,7 @@ const columns = computed<ColumnDef<PlayerConfig>[]>(() => [
|
||||
row.encrytionM3u8 ? 'encrytionM3u8' : '',
|
||||
row.logoUrl ? 'logo' : '',
|
||||
].filter(Boolean).join(', '),
|
||||
cell: ({ row }) => h(PlayerConfigSettingsBadges, { config: row.original }),
|
||||
cell: ({ row }) => <PlayerConfigSettingsBadges config={row.original} />,
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3',
|
||||
@@ -79,13 +90,23 @@ const columns = computed<ColumnDef<PlayerConfig>[]>(() => [
|
||||
id: 'status',
|
||||
header: t('common.status'),
|
||||
accessorFn: row => Number(row.isActive),
|
||||
cell: ({ row }) => h('div', { class: 'text-center' }, [
|
||||
h(AppSwitch, {
|
||||
modelValue: row.original.isActive,
|
||||
disabled: !props.canManageExistingConfig || props.saving || props.deletingId !== null || props.defaultingId !== null || props.togglingId === row.original.id,
|
||||
'onUpdate:modelValue': (value: boolean) => emit('toggle-active', { config: row.original, value }),
|
||||
}),
|
||||
]),
|
||||
cell: ({ row }) => (
|
||||
<div class="text-center">
|
||||
<AppSwitch
|
||||
modelValue={row.original.isActive}
|
||||
disabled={
|
||||
!props.canManageExistingConfig ||
|
||||
props.saving ||
|
||||
props.deletingId !== null ||
|
||||
props.defaultingId !== null ||
|
||||
props.togglingId === row.original.id
|
||||
}
|
||||
onUpdate:modelValue={(value: boolean) =>
|
||||
emit('toggle-active', { config: row.original, value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50',
|
||||
cellClass: 'px-6 py-3 text-center',
|
||||
@@ -95,35 +116,48 @@ const columns = computed<ColumnDef<PlayerConfig>[]>(() => [
|
||||
id: 'actions',
|
||||
header: t('common.actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => h('div', { class: 'flex flex-wrap items-center justify-end gap-2' }, [
|
||||
row.original.isDefault
|
||||
? h('span', {
|
||||
class: 'inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary',
|
||||
}, t('settings.playerConfigs.actions.default'))
|
||||
: h(AppButton, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
loading: props.defaultingId === row.original.id,
|
||||
disabled: !props.canManageExistingConfig || props.saving || props.deletingId !== null || props.togglingId !== null || props.defaultingId !== null || !row.original.isActive,
|
||||
onClick: () => emit('set-default', row.original),
|
||||
}, () => t('settings.playerConfigs.actions.setDefault')),
|
||||
h(AppButton, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
disabled: !props.canManageExistingConfig,
|
||||
onClick: () => emit('edit', row.original),
|
||||
}, {
|
||||
icon: () => h(PencilIcon, { class: 'h-4 w-4' }),
|
||||
}),
|
||||
h(AppButton, {
|
||||
variant: 'ghost',
|
||||
size: 'sm',
|
||||
disabled: !props.canDeleteConfig,
|
||||
onClick: () => emit('delete', row.original),
|
||||
}, {
|
||||
icon: () => h(TrashIcon, { class: 'h-4 w-4 text-danger' }),
|
||||
}),
|
||||
]),
|
||||
cell: ({ row }) => (<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
{row.original.isDefault ? (
|
||||
<span class="inline-flex items-center rounded-full bg-primary/10 px-2 py-1 text-xs font-medium text-primary">
|
||||
{t('settings.playerConfigs.actions.default')}
|
||||
</span>
|
||||
) : (
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
loading={props.defaultingId === row.original.id}
|
||||
disabled={
|
||||
!props.canManageExistingConfig ||
|
||||
props.saving ||
|
||||
props.deletingId !== null ||
|
||||
props.togglingId !== null ||
|
||||
props.defaultingId !== null ||
|
||||
!row.original.isActive
|
||||
}
|
||||
onClick={() => emit('set-default', row.original)}
|
||||
>
|
||||
{t('settings.playerConfigs.actions.setDefault')}
|
||||
</AppButton>
|
||||
)}
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!props.canManageExistingConfig}
|
||||
onClick={() => emit('edit', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <PencilIcon class="h-4 w-4" />,
|
||||
}}
|
||||
/>
|
||||
<AppButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!props.canDeleteConfig}
|
||||
onClick={() => emit('delete', row.original)}
|
||||
v-slots={{
|
||||
icon: () => <TrashIcon class="h-4 w-4 text-danger" />,
|
||||
}}
|
||||
/>
|
||||
</div>),
|
||||
meta: {
|
||||
headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center',
|
||||
cellClass: 'px-6 py-3 text-right',
|
||||
@@ -133,10 +167,9 @@ const columns = computed<ColumnDef<PlayerConfig>[]>(() => [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsTableSkeleton v-if="isInitialLoading" :columns="5" :rows="4" />
|
||||
|
||||
<BaseTable
|
||||
v-else
|
||||
:loading="isInitialLoading"
|
||||
:skeleton-rows="4"
|
||||
:data="configs"
|
||||
:columns="columns"
|
||||
:get-row-id="(row) => row.id"
|
||||
|
||||
242
src/routes/settings/PopupAds/PopupAds.vue
Normal file
242
src/routes/settings/PopupAds/PopupAds.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import SettingsSectionCard from '../components/SettingsSectionCard.vue';
|
||||
import PopupAdsDialog from './components/PopupAdsDialog.vue';
|
||||
import PopupAdsTable from './components/PopupAdsTable';
|
||||
import PopupAdsToolbar from './components/PopupAdsToolbar.vue';
|
||||
import type { PopupAdFormData, PopupAdItem } from './types';
|
||||
import { useQuery } from '@pinia/colada';
|
||||
import { computed, watch, ref } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
const toast = useAppToast();
|
||||
const confirm = useAppConfirm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showDialog = ref(false);
|
||||
const saving = ref(false);
|
||||
const deletingId = ref<string | null>(null);
|
||||
const togglingId = ref<string | null>(null);
|
||||
const editingItem = ref<PopupAdItem | null>(null);
|
||||
|
||||
const createInitialFormData = (): PopupAdFormData => ({
|
||||
type: 'url',
|
||||
label: '',
|
||||
value: '',
|
||||
isActive: true,
|
||||
maxTriggersPerSession: 3,
|
||||
});
|
||||
|
||||
const formData = ref<PopupAdFormData>(createInitialFormData());
|
||||
|
||||
const pageSizeOptions = [5, 10, 20, 50] as const;
|
||||
const page = ref(1);
|
||||
const limit = ref(10);
|
||||
|
||||
const { data: popupSnapshot, error, isPending, refetch } = useQuery({
|
||||
key: () => ['settings', 'popup-ads', page.value, limit.value],
|
||||
query: async () => {
|
||||
return await rpcClient.listPopupAds({ page: page.value, limit: limit.value });
|
||||
},
|
||||
});
|
||||
|
||||
const items = computed<PopupAdItem[]>(() => popupSnapshot.value?.items || []);
|
||||
const total = computed(() => popupSnapshot.value?.total || 0);
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / limit.value)));
|
||||
const hasPrev = computed(() => page.value > 1);
|
||||
const hasNext = computed(() => page.value < totalPages.value);
|
||||
const isMutating = computed(() => saving.value || deletingId.value !== null || togglingId.value !== null);
|
||||
const isInitialLoading = computed(() => isPending.value && !popupSnapshot.value);
|
||||
|
||||
const getErrorMessage = (value: any, fallback: string) => value?.error?.message || value?.message || value?.data?.message || fallback;
|
||||
const showActionErrorToast = (value: any) => {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.failedSummary'), detail: getErrorMessage(value, t('settings.popupAds.toast.failedDetail')), life: 5000 });
|
||||
};
|
||||
|
||||
watch(error, (value, previous) => {
|
||||
if (!value || value === previous || isMutating.value) return;
|
||||
showActionErrorToast(value);
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = createInitialFormData();
|
||||
editingItem.value = null;
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
showDialog.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
const openCreateDialog = () => {
|
||||
resetForm();
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
const openEditDialog = (item: PopupAdItem) => {
|
||||
editingItem.value = item;
|
||||
formData.value = {
|
||||
type: (item.type as 'url' | 'script') || 'url',
|
||||
label: item.label || '',
|
||||
value: item.value || '',
|
||||
isActive: Boolean(item.isActive),
|
||||
maxTriggersPerSession: Number(item.maxTriggersPerSession || 3),
|
||||
};
|
||||
showDialog.value = true;
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (saving.value) return;
|
||||
|
||||
const label = formData.value.label.trim();
|
||||
const value = formData.value.value.trim();
|
||||
|
||||
if (!label) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.labelRequiredSummary'), detail: t('settings.popupAds.toast.labelRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.valueRequiredSummary'), detail: t('settings.popupAds.toast.valueRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (formData.value.type === 'url') {
|
||||
try {
|
||||
new URL(value);
|
||||
} catch {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.invalidUrlSummary'), detail: t('settings.popupAds.toast.invalidUrlDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (formData.value.type === 'url' && formData.value.maxTriggersPerSession < 1) {
|
||||
toast.add({ severity: 'error', summary: t('settings.popupAds.toast.maxTriggersRequiredSummary'), detail: t('settings.popupAds.toast.maxTriggersRequiredDetail'), life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
type: formData.value.type,
|
||||
label,
|
||||
value,
|
||||
isActive: formData.value.isActive,
|
||||
maxTriggersPerSession: formData.value.type === 'url' ? formData.value.maxTriggersPerSession : undefined,
|
||||
};
|
||||
|
||||
if (editingItem.value?.id) {
|
||||
await rpcClient.updatePopupAd({ id: editingItem.value.id, ...payload });
|
||||
toast.add({ severity: 'success', summary: t('settings.popupAds.toast.updatedSummary'), detail: t('settings.popupAds.toast.updatedDetail'), life: 2500 });
|
||||
} else {
|
||||
await rpcClient.createPopupAd(payload);
|
||||
toast.add({ severity: 'success', summary: t('settings.popupAds.toast.createdSummary'), detail: t('settings.popupAds.toast.createdDetail'), life: 2500 });
|
||||
}
|
||||
|
||||
await refetch();
|
||||
closeDialog();
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (item: PopupAdItem) => {
|
||||
confirm.require({
|
||||
message: t('settings.popupAds.confirm.deleteMessage', { name: item.label || '' }),
|
||||
header: t('settings.popupAds.confirm.deleteHeader'),
|
||||
acceptLabel: t('settings.popupAds.confirm.deleteAccept'),
|
||||
rejectLabel: t('settings.popupAds.confirm.deleteReject'),
|
||||
accept: async () => {
|
||||
deletingId.value = item.id || null;
|
||||
try {
|
||||
await rpcClient.deletePopupAd({ id: item.id || '' });
|
||||
await refetch();
|
||||
toast.add({ severity: 'info', summary: t('settings.popupAds.toast.deletedSummary'), detail: t('settings.popupAds.toast.deletedDetail'), life: 2500 });
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
deletingId.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleActive = async ({ item, value }: { item: PopupAdItem; value: boolean }) => {
|
||||
togglingId.value = item.id || null;
|
||||
try {
|
||||
await rpcClient.updatePopupAd({
|
||||
id: item.id || '',
|
||||
type: item.type || 'url',
|
||||
label: item.label || '',
|
||||
value: item.value || '',
|
||||
isActive: value,
|
||||
maxTriggersPerSession: item.type === 'url' ? item.maxTriggersPerSession : undefined,
|
||||
});
|
||||
await refetch();
|
||||
} catch (value: any) {
|
||||
console.error(value);
|
||||
showActionErrorToast(value);
|
||||
} finally {
|
||||
togglingId.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const previousPage = () => {
|
||||
if (!hasPrev.value || isInitialLoading.value) return;
|
||||
page.value -= 1;
|
||||
};
|
||||
|
||||
const nextPage = () => {
|
||||
if (!hasNext.value || isInitialLoading.value) return;
|
||||
page.value += 1;
|
||||
};
|
||||
|
||||
const changePageSize = (value: number) => {
|
||||
const nextLimit = Number(value) || 10;
|
||||
if (nextLimit === limit.value) return;
|
||||
limit.value = nextLimit;
|
||||
page.value = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsSectionCard :title="t('settings.content.popupAds.title')" :description="t('settings.content.popupAds.subtitle')" bodyClass="">
|
||||
<template #header-actions>
|
||||
<PopupAdsToolbar :disabled="isInitialLoading || isMutating" @create="openCreateDialog" />
|
||||
</template>
|
||||
|
||||
<PopupAdsTable
|
||||
:items="items"
|
||||
:disabled="isMutating"
|
||||
:is-loading="isInitialLoading"
|
||||
:current-page="page"
|
||||
:total-pages="totalPages"
|
||||
:total-records="total"
|
||||
:rows-per-page="limit"
|
||||
:page-size-options="pageSizeOptions as unknown as number[]"
|
||||
:can-previous-page="hasPrev"
|
||||
:can-next-page="hasNext"
|
||||
@edit="openEditDialog"
|
||||
@delete="handleDelete"
|
||||
@toggle-active="handleToggleActive"
|
||||
@previous-page="previousPage"
|
||||
@next-page="nextPage"
|
||||
@page-size-change="changePageSize"
|
||||
/>
|
||||
|
||||
<div class="px-4 py-3 bg-header">
|
||||
<p class="text-xs leading-5 text-foreground/60">
|
||||
<strong class="text-foreground/80">{{ t('settings.popupAds.info.urlTitle') }}</strong>
|
||||
{{ t('settings.popupAds.info.urlDescription') }}
|
||||
<br>
|
||||
<strong class="text-foreground/80">{{ t('settings.popupAds.info.scriptTitle') }}</strong>
|
||||
{{ t('settings.popupAds.info.scriptDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<PopupAdsDialog :visible="showDialog" :editing-item="editingItem" :form-data="formData" :saving="saving" @update:visible="showDialog = $event" @update:form-data="formData = $event" @save="handleSave" @close="closeDialog" />
|
||||
</SettingsSectionCard>
|
||||
</template>
|
||||
137
src/routes/settings/PopupAds/components/PopupAdsDialog.vue
Normal file
137
src/routes/settings/PopupAds/components/PopupAdsDialog.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import CheckIcon from '@/components/icons/CheckIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AppInput from '@/components/ui/AppInput.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed } from 'vue';
|
||||
import type { PopupAdFormData, PopupAdItem } from '../types';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
editingItem: PopupAdItem | null;
|
||||
formData: PopupAdFormData;
|
||||
saving: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'update:formData', value: PopupAdFormData): void;
|
||||
(e: 'save'): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = computed(() => props.editingItem
|
||||
? t('settings.popupAds.dialog.editTitle')
|
||||
: t('settings.popupAds.dialog.createTitle'));
|
||||
|
||||
const updateForm = (patch: Partial<PopupAdFormData>) => {
|
||||
emit('update:formData', {
|
||||
...props.formData,
|
||||
...patch,
|
||||
});
|
||||
};
|
||||
|
||||
const updateTextField = (key: 'label' | 'value', value: string | number | null) => {
|
||||
updateForm({
|
||||
[key]: typeof value === 'string' ? value : value == null ? '' : String(value),
|
||||
} as Partial<PopupAdFormData>);
|
||||
};
|
||||
|
||||
const updateNumberField = (value: string | number | null) => {
|
||||
const parsed = typeof value === 'number' ? value : Number(value ?? 0);
|
||||
updateForm({ maxTriggersPerSession: Number.isFinite(parsed) && parsed > 0 ? parsed : 1 });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
:visible="visible"
|
||||
:title="title"
|
||||
maxWidthClass="max-w-lg"
|
||||
@update:visible="emit('update:visible', $event)"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-type" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.type') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-type"
|
||||
as="select"
|
||||
:model-value="formData.type"
|
||||
@update:model-value="updateForm({ type: ($event as 'url' | 'script') || 'url' })"
|
||||
>
|
||||
<option value="url">{{ t('settings.popupAds.types.url') }}</option>
|
||||
<option value="script">{{ t('settings.popupAds.types.script') }}</option>
|
||||
</AppInput>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-label" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.label') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-label"
|
||||
:model-value="formData.label"
|
||||
:placeholder="t('settings.popupAds.dialog.labelPlaceholder')"
|
||||
@update:model-value="updateTextField('label', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<label for="popup-ad-value" class="text-sm font-medium text-foreground">{{ t(formData.type === 'url' ? 'settings.popupAds.dialog.url' : 'settings.popupAds.dialog.script') }}</label>
|
||||
<AppInput
|
||||
v-if="formData.type === 'url'"
|
||||
id="popup-ad-value"
|
||||
:model-value="formData.value"
|
||||
:placeholder="t('settings.popupAds.dialog.urlPlaceholder')"
|
||||
@update:model-value="updateTextField('value', $event)"
|
||||
/>
|
||||
<AppInput
|
||||
v-else
|
||||
id="popup-ad-value"
|
||||
as="textarea"
|
||||
:rows="5"
|
||||
:model-value="formData.value"
|
||||
:placeholder="t('settings.popupAds.dialog.scriptPlaceholder')"
|
||||
inputClass="resize-y font-mono text-sm"
|
||||
@update:model-value="updateTextField('value', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.type === 'url'" class="grid gap-2">
|
||||
<label for="popup-ad-max-triggers" class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.maxTriggersPerSession') }}</label>
|
||||
<AppInput
|
||||
id="popup-ad-max-triggers"
|
||||
type="number"
|
||||
min="1"
|
||||
:model-value="formData.maxTriggersPerSession"
|
||||
@update:model-value="updateNumberField($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between rounded-md border border-border bg-header/40 px-3 py-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">{{ t('settings.popupAds.dialog.activeTitle') }}</p>
|
||||
<p class="mt-0.5 text-xs text-foreground/60">{{ t('settings.popupAds.dialog.activeDescription') }}</p>
|
||||
</div>
|
||||
<AppSwitch :model-value="formData.isActive" @update:model-value="updateForm({ isActive: $event })" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<AppButton variant="secondary" size="sm" :disabled="saving" @click="emit('close')">
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton size="sm" :loading="saving" @click="emit('save')">
|
||||
<template #icon>
|
||||
<CheckIcon class="h-4 w-4" />
|
||||
</template>
|
||||
{{ editingItem ? t('settings.popupAds.dialog.update') : t('settings.popupAds.dialog.create') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
144
src/routes/settings/PopupAds/components/PopupAdsTable.tsx
Normal file
144
src/routes/settings/PopupAds/components/PopupAdsTable.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import PencilIcon from '@/components/icons/PencilIcon.vue';
|
||||
import TrashIcon from '@/components/icons/TrashIcon.vue';
|
||||
import LinkIcon from '@/components/icons/LinkIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppSwitch from '@/components/ui/AppSwitch.vue';
|
||||
import BaseTable from '@/components/ui/BaseTable.vue';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, defineComponent, type PropType } from 'vue';
|
||||
import type { PopupAdItem } from '../types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PopupAdsTable',
|
||||
props: {
|
||||
items: { type: Array as PropType<PopupAdItem[]>, required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
isLoading: { type: Boolean, default: false },
|
||||
currentPage: { type: Number, default: 1 },
|
||||
totalPages: { type: Number, default: 1 },
|
||||
totalRecords: { type: Number, default: 0 },
|
||||
rowsPerPage: { type: Number, default: 10 },
|
||||
pageSizeOptions: { type: Array as PropType<number[]>, default: () => [] },
|
||||
canPreviousPage: { type: Boolean, default: false },
|
||||
canNextPage: { type: Boolean, default: false },
|
||||
},
|
||||
emits: {
|
||||
edit: (item: PopupAdItem) => true,
|
||||
delete: (item: PopupAdItem) => true,
|
||||
'toggle-active': (payload: { item: PopupAdItem; value: boolean }) => true,
|
||||
'previous-page': () => true,
|
||||
'next-page': () => true,
|
||||
'page-size-change': (value: number) => true,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns = computed<ColumnDef<PopupAdItem>[]>(() => [
|
||||
{
|
||||
id: 'label',
|
||||
header: t('settings.popupAds.table.label'),
|
||||
accessorFn: (row) => row.label || '',
|
||||
cell: ({ row }) => (
|
||||
<div class="max-w-[200px]">
|
||||
<p onClick={() => emit('edit', row.original)} class="block text-sm truncate font-medium text-foreground cursor-pointer hover:underline">
|
||||
{row.original.label}
|
||||
</p>
|
||||
<p class="block text-sm truncate mt-0.5 text-xs text-foreground/50">#{row.original.id}</p>
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: t('settings.popupAds.table.type'),
|
||||
accessorFn: (row) => row.type || '',
|
||||
cell: ({ row }) => (
|
||||
<span class={[
|
||||
'inline-flex rounded-full px-2 py-1 text-xs font-medium uppercase',
|
||||
row.original.type === 'url' ? 'bg-emerald-500/10 text-emerald-600' : 'bg-indigo-500/10 text-indigo-600',
|
||||
]}>
|
||||
{t(`settings.popupAds.types.${row.original.type}`)}
|
||||
</span>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'target',
|
||||
header: t('settings.popupAds.table.target'),
|
||||
accessorFn: (row) => row.value || '',
|
||||
cell: ({ row }) => (
|
||||
<div class="max-w-[320px]">
|
||||
<code class={[
|
||||
'block truncate text-xs',
|
||||
row.original.type === 'script' ? 'font-mono text-foreground/60' : 'text-foreground/60',
|
||||
]}>
|
||||
{row.original.value}
|
||||
</code>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'maxTriggersPerSession',
|
||||
header: t('settings.popupAds.table.maxTriggersPerSession'),
|
||||
accessorFn: (row) => row?.type === 'url' ? row.maxTriggersPerSession || 0 : '—',
|
||||
cell: ({ getValue }) => <span class="text-foreground/70">{getValue()}</span>,
|
||||
meta: { headerClass: 'px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: t('common.status'),
|
||||
cell: ({ row }) => (
|
||||
<div class="text-center">
|
||||
<AppSwitch
|
||||
modelValue={Boolean(row.original.isActive)}
|
||||
disabled={props.disabled}
|
||||
onUpdate:modelValue={(value: boolean) => emit('toggle-active', { item: row.original, value })}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: t('common.actions'),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<AppButton variant="ghost" size="icon" disabled={props.disabled} onClick={() => emit('delete', row.original)} v-slots={{ icon: () => <TrashIcon filled class="h-4 w-4" /> }} />
|
||||
</div>
|
||||
),
|
||||
meta: { headerClass: 'px-6 py-3 text-center text-xs font-medium uppercase tracking-wider text-foreground/50 [&>div]:justify-center', cellClass: 'px-6 py-3 text-center' },
|
||||
},
|
||||
]);
|
||||
|
||||
return () => (
|
||||
<BaseTable
|
||||
data={props.items}
|
||||
columns={columns.value}
|
||||
loading={props.isLoading}
|
||||
getRowId={(row: PopupAdItem) => String(row.id)}
|
||||
wrapperClass="mt-4 border-b border-border rounded-none border-x-0 border-t-0 bg-transparent"
|
||||
tableClass="w-full"
|
||||
headerRowClass="bg-muted/30"
|
||||
bodyRowClass="border-b border-border hover:bg-muted/30"
|
||||
pagination
|
||||
{...props}
|
||||
onPrevious-page={() => emit('previous-page')}
|
||||
onNext-page={() => emit('next-page')}
|
||||
onPage-size-change={(value: number) => emit('page-size-change', value)}
|
||||
v-slots={{
|
||||
empty: () => (
|
||||
<div class="px-6 py-12 text-center">
|
||||
<LinkIcon class="mx-auto mb-3 block h-10 w-10 text-foreground/30" />
|
||||
<p class="mb-1 text-sm text-foreground/60">{t('settings.popupAds.emptyTitle')}</p>
|
||||
<p class="text-xs text-foreground/40">{t('settings.popupAds.emptySubtitle')}</p>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
24
src/routes/settings/PopupAds/components/PopupAdsToolbar.vue
Normal file
24
src/routes/settings/PopupAds/components/PopupAdsToolbar.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import PlusIcon from '@/components/icons/PlusIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
defineProps<{
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'create'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppButton size="sm" :disabled="disabled" @click="emit('create')">
|
||||
<template #icon>
|
||||
<PlusIcon class="h-4 w-4" />
|
||||
</template>
|
||||
{{ t('settings.popupAds.createItem') }}
|
||||
</AppButton>
|
||||
</template>
|
||||
17
src/routes/settings/PopupAds/types.ts
Normal file
17
src/routes/settings/PopupAds/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type { PopupAd } from '@/server/api/proto/app/v1/common';
|
||||
export type {
|
||||
CreatePopupAdRequest,
|
||||
DeletePopupAdRequest,
|
||||
UpdatePopupAdRequest,
|
||||
} from '@/server/api/proto/app/v1/catalog';
|
||||
|
||||
export type PopupAdType = 'url' | 'script';
|
||||
export type PopupAdItem = PopupAd;
|
||||
|
||||
export interface PopupAdFormData {
|
||||
type: PopupAdType;
|
||||
label: string;
|
||||
value: string;
|
||||
isActive: boolean;
|
||||
maxTriggersPerSession: number;
|
||||
}
|
||||
@@ -9,12 +9,10 @@ const { t } = useTranslation();
|
||||
<SettingsRow
|
||||
:title="t('settings.securityConnected.accountStatus.label')"
|
||||
:description="t('settings.securityConnected.accountStatus.detail')"
|
||||
iconBoxClass="bg-primary/10 text-primary"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 532 532"><path d="M10 266c0 37 21 69 51 85-10 33-2 70 24 96s63 34 96 24c16 30 48 51 85 51s69-21 85-51c33 10 70 2 96-24s34-63 24-96c30-16 51-48 51-85s-21-69-51-85c10-33 2-70-24-96s-63-34-96-24c-16-30-48-51-85-51s-69 21-85 51c-33-10-70-2-96 24s-34 63-24 96c-30 16-51 48-51 85zm136 24 34-33c8 8 26 26 53 54l106-145 14-20 39 28-14 20-123 169-17 23-20-21c-43-45-68-70-72-75z" fill="var(--fill1)"/><path d="m392 174-14 20-123 169-17 23-20-21c-43-45-68-70-72-75l34-33c8 8 26 26 53 54l106-145 14-20 39 28z" fill="var(--fill4)"/></svg>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -14,11 +14,10 @@ const { t } = useTranslation();
|
||||
<SettingsRow
|
||||
:title="t('settings.securityConnected.changePassword.label')"
|
||||
:description="t('settings.securityConnected.changePassword.detail')"
|
||||
iconBoxClass="bg-primary/10 text-primary"
|
||||
>
|
||||
<template #icon>
|
||||
<svg aria-hidden="true" class="fill-primary w-6 h-6" height="24" viewBox="0 0 24 24" version="1.1" width="24" data-view-component="true">
|
||||
<path d="M22 9.75v5.5A1.75 1.75 0 0 1 20.25 17H3.75A1.75 1.75 0 0 1 2 15.25v-5.5C2 8.784 2.784 8 3.75 8h16.5c.966 0 1.75.784 1.75 1.75Zm-8.75 2.75a1.25 1.25 0 1 0-2.5 0 1.25 1.25 0 0 0 2.5 0Zm-6.5 1.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Zm10.5 0a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z"></path>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 404 564"><path d="M74 138v64h64v-64c0-35 29-64 64-64s64 29 64 64v64h64v-64c0-71-57-128-128-128S74 67 74 138z" fill="var(--fill1)"/><path d="M74 202c-35 0-64 29-64 64v224c0 35 29 64 64 64h256c35 0 64-29 64-64V266c0-35-29-64-64-64H74zm152 136v80c0 13-11 24-24 24s-24-11-24-24v-80c0-13 11-24 24-24s24 11 24 24z" fill="var(--fill4)"/></svg>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import MailIcon from '@/components/icons/MailIcon.vue';
|
||||
import SettingsRow from '@/routes/settings/components/SettingsRow.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
@@ -13,12 +14,10 @@ const { t } = useTranslation();
|
||||
<SettingsRow
|
||||
:title="t('settings.securityConnected.email.label')"
|
||||
:description="emailConnected ? t('settings.securityConnected.email.connected') : t('settings.securityConnected.email.disconnected')"
|
||||
iconBoxClass="bg-primary/10 text-primary"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="text-info w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="16" x="2" y="4" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
<MailIcon filled />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import LanguageIcon from '@/components/icons/LanguageIcon.vue';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import SettingsRow from '@/routes/settings/components/SettingsRow.vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
@@ -26,14 +27,10 @@ const updateSelectedLanguage = (event: Event) => {
|
||||
:title="t('settings.securityConnected.language.label')"
|
||||
:description="t('settings.securityConnected.language.detail')"
|
||||
actionsClass="flex items-center gap-2"
|
||||
iconBoxClass="bg-primary/10 text-primary"
|
||||
>
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="M12 2a15 15 0 0 1 0 20" />
|
||||
<path d="M12 2a15 15 0 0 0 0 20" />
|
||||
</svg>
|
||||
<LanguageIcon class="h-6 w-6" filled />
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -16,9 +16,10 @@ const { t } = useTranslation();
|
||||
:title="t('settings.securityConnected.logout.label')"
|
||||
:description="t('settings.securityConnected.logout.detail')"
|
||||
hoverClass="hover:bg-danger/5"
|
||||
iconBoxClass="bg-danger/10 text-danger"
|
||||
>
|
||||
<template #icon>
|
||||
<XCircleIcon class="w-6 h-6 text-danger" />
|
||||
<svg class="w-6 h-6 text-danger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 532 532"><path d="M10 266c0 141 115 256 256 256s256-115 256-256S407 10 266 10 10 125 10 266zm167-89c9-9 25-9 34 0l55 55 55-55c9-9 25-9 34 0s9 25 0 34l-55 55 55 55c9 9 9 25 0 34-10 9-25 9-34 0l-55-55-55 55c-9 9-25 9-34 0-9-10-9-25 0-34l55-55-55-55c-9-9-9-25 0-34z" fill="color-mix(in srgb, var(--colors-danger-DEFAULT) 40%, transparent)"/><path d="M211 177c-9-9-25-9-34 0s-9 25 0 34l55 55-55 55c-9 9-9 25 0 34 10 9 25 9 34 0l55-55 55 55c9 9 25 9 34 0 9-10 9-25 0-34l-55-55 55-55c9-9 9-25 0-34s-25-9-34 0l-55 55-55-55z" fill="var(--colors-danger-DEFAULT)"/></svg>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
|
||||
@@ -21,6 +21,7 @@ const { t } = useTranslation();
|
||||
<SettingsRow
|
||||
:title="t('settings.securityConnected.telegram.label')"
|
||||
:description="telegramConnected ? (telegramUsername || t('settings.securityConnected.telegram.connectedFallback')) : t('settings.securityConnected.telegram.detailDisconnected')"
|
||||
iconBoxClass="bg-[#0088cc]/10"
|
||||
>
|
||||
<template #icon>
|
||||
<TelegramIcon class="w-6 h-6 text-[#0088cc]" />
|
||||
|
||||
@@ -60,22 +60,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppConfirmHost from '@/components/ui/AppConfirmHost.vue';
|
||||
import AppToastHost from '@/components/ui/AppToastHost.vue';
|
||||
import ClientOnly from '@/components/ClientOnly';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import AdvertisementIcon from '@/components/icons/AdvertisementIcon.vue';
|
||||
import AlertTriangle from '@/components/icons/AlertTriangle.vue';
|
||||
import Bell from '@/components/icons/Bell.vue';
|
||||
import CreditCardIcon from '@/components/icons/CreditCardIcon.vue';
|
||||
import Credit from '@/components/icons/Credit.vue';
|
||||
import GlobeIcon from '@/components/icons/Globe.vue';
|
||||
import ShieldUser from '@/components/icons/shield-user.vue';
|
||||
import UserIcon from '@/components/icons/UserIcon.vue';
|
||||
import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
|
||||
import Windows from '@/components/icons/windows.vue';
|
||||
import AppConfirmHost from '@/components/ui/AppConfirmHost.vue';
|
||||
import AppToastHost from '@/components/ui/AppToastHost.vue';
|
||||
import { isAdmin } from '@/lib/utils';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, createStaticVNode } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { isAdmin } from '@/lib/utils';
|
||||
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
@@ -96,9 +98,9 @@ const menuSections = computed<{ title: string; items: MenuItem[] }[]>(() => [
|
||||
items: [
|
||||
{
|
||||
to: '/settings/security',
|
||||
value: 'security', label: t('settings.menu.security'), icon: createStaticVNode(`<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -258 596 564"><path d="M144-120c0-44 36-80 80-80s80 36 80 80-36 80-80 80-80-36-80-80zm208 0c0-71-57-128-128-128S96-191 96-120 153 8 224 8s128-57 128-128zM48 232c0-71 57-128 128-128h64V77c0-7 1-14 3-21h-67C79 56 0 135 0 232v8c0 13 11 24 24 24s24-11 24-24v-8zm397 9-13 6V59l96 32v19c0 56-32 107-83 131zM422 12 310 49c-13 4-22 16-22 30v31c0 75 43 142 110 174l19 9c5 2 10 3 15 3s10-1 15-3l19-9c67-32 110-99 110-174V79c0-14-9-26-22-30L442 11c-6-2-14-2-20 0zm0 0z" fill="currentColor"/></svg>`, 1)
|
||||
value: 'security', label: t('settings.menu.security'), icon: ShieldUser
|
||||
},
|
||||
{ to: '/settings/billing', value: 'billing', label: t('settings.menu.billing'), icon: CreditCardIcon },
|
||||
{ to: '/settings/billing', value: 'billing', label: t('settings.menu.billing'), icon: Credit },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -118,6 +120,7 @@ const menuSections = computed<{ title: string; items: MenuItem[] }[]>(() => [
|
||||
items: [
|
||||
{ to: '/settings/domains', value: 'domains', label: t('settings.menu.domains'), icon: GlobeIcon },
|
||||
{ to: '/settings/ads', value: 'ads', label: t('settings.menu.ads'), icon: AdvertisementIcon },
|
||||
{ to: '/settings/popup-ads', value: 'popup-ads', label: t('settings.menu.popupAds'), icon: Windows },
|
||||
],
|
||||
},
|
||||
...(isAdmin(auth.user?.role) ? [{
|
||||
@@ -133,6 +136,7 @@ const menuSections = computed<{ title: string; items: MenuItem[] }[]>(() => [
|
||||
title: 'Admin Operations',
|
||||
items: [
|
||||
{ to: '/settings/admin/ad-templates', value: 'admin-ad-templates', label: 'Ad Templates', description: 'VAST templates and defaults' },
|
||||
{ to: '/settings/admin/popup-ads', value: 'admin-popup-ads', label: 'Popup Ads', description: 'Popup campaigns, timing and cooldowns' },
|
||||
{ to: '/settings/admin/player-configs', value: 'admin-player-configs', label: 'Player Configs', description: 'Cross-user player presets and defaults' },
|
||||
{ to: '/settings/admin/jobs', value: 'admin-jobs', label: 'Jobs', description: 'Queue, retries and live logs' },
|
||||
{ to: '/settings/admin/agents', value: 'admin-agents', label: 'Agents', description: 'Workers, health and maintenance' },
|
||||
@@ -192,6 +196,10 @@ const content = computed(() => ({
|
||||
title: t('settings.content.ads.title'),
|
||||
subtitle: t('settings.content.ads.subtitle')
|
||||
},
|
||||
'settings-popup-ads': {
|
||||
title: t('settings.content.popupAds.title'),
|
||||
subtitle: t('settings.content.popupAds.subtitle')
|
||||
},
|
||||
'settings-player-configs': {
|
||||
title: t('settings.content.playerConfigs.title'),
|
||||
subtitle: t('settings.content.playerConfigs.subtitle')
|
||||
@@ -224,6 +232,10 @@ const content = computed(() => ({
|
||||
title: 'Ad Templates',
|
||||
subtitle: 'VAST templates, ownership metadata and default assignments.',
|
||||
},
|
||||
'admin-popup-ads': {
|
||||
title: 'Popup Ads',
|
||||
subtitle: 'Popup campaigns, timing windows and cooldown controls across users.',
|
||||
},
|
||||
'admin-player-configs': {
|
||||
title: 'Player Configs',
|
||||
subtitle: 'Cross-user player presets, flags and default assignments.',
|
||||
|
||||
@@ -440,7 +440,7 @@ onMounted(loadTemplates);
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-sm font-medium text-foreground/70">Description</label>
|
||||
<AdminTextarea v-model="createForm.description" rows="3" placeholder="Optional" />
|
||||
<AdminTextarea v-model="createForm.description" :rows="3" placeholder="Optional" />
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-sm font-medium text-foreground/70">VAST URL</label>
|
||||
@@ -488,7 +488,7 @@ onMounted(loadTemplates);
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-sm font-medium text-foreground/70">Description</label>
|
||||
<AdminTextarea v-model="editForm.description" rows="3" />
|
||||
<AdminTextarea v-model="editForm.description" :rows="3" />
|
||||
</div>
|
||||
<div class="space-y-2 md:col-span-2">
|
||||
<label class="text-sm font-medium text-foreground/70">VAST URL</label>
|
||||
|
||||
@@ -6,8 +6,8 @@ import AdminInput from "./components/AdminInput.vue";
|
||||
import AdminSelect from "./components/AdminSelect.vue";
|
||||
import AdminTable from "./components/AdminTable.vue";
|
||||
import AdminSectionCard from "./components/AdminSectionCard.vue";
|
||||
import BillingPlansSection from "@/routes/settings/Billing/components/BillingPlansSection.vue";
|
||||
import type { Plan as ModelPlan } from "@/server/gen/proto/app/v1/common";
|
||||
import PlanSelection from "@/routes/settings/Billing/components/PlanSelection.tsx";
|
||||
import type { Plan as ModelPlan } from "@/server/api/proto/app/v1/common";
|
||||
import { type ColumnDef } from "@tanstack/vue-table";
|
||||
import { computed, h, onMounted, reactive, ref, watch } from "vue";
|
||||
import AdminMetricCard from "./components/AdminMetricCard.vue";
|
||||
@@ -470,21 +470,10 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-lg border border-border">
|
||||
<BillingPlansSection
|
||||
title="Available plans"
|
||||
description="Reuse the same plan cards from the billing screen when creating an admin payment."
|
||||
:is-loading="plansLoading"
|
||||
:plans="plans"
|
||||
<PlanSelection
|
||||
:current-plan-id="selectedPlanId"
|
||||
:selecting-plan-id="selectedPlanId"
|
||||
:format-money="(amount) => formatMoney(amount, 'USD')"
|
||||
:get-plan-storage-text="getPlanStorageText"
|
||||
:get-plan-duration-text="getPlanDurationText"
|
||||
:get-plan-uploads-text="getPlanUploadsText"
|
||||
current-plan-label="Selected"
|
||||
selecting-label="Selected"
|
||||
choose-label="Select plan"
|
||||
@select="selectPlan"
|
||||
:selected-plan-id="selectedPlanId"
|
||||
@upgrade="selectPlan"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
306
src/routes/settings/admin/PopupAds.vue
Normal file
306
src/routes/settings/admin/PopupAds.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import AppButton from '@/components/ui/AppButton.vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import AdminInput from './components/AdminInput.vue';
|
||||
import AdminSelect from './components/AdminSelect.vue';
|
||||
import AdminTable from './components/AdminTable.vue';
|
||||
import AdminSectionCard from './components/AdminSectionCard.vue';
|
||||
import type { ColumnDef } from '@tanstack/vue-table';
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import AdminMetricCard from './components/AdminMetricCard.vue';
|
||||
import AdminPlaceholderTable from './components/AdminPlaceholderTable.vue';
|
||||
import AdminSectionShell from './components/AdminSectionShell.vue';
|
||||
import { useAdminPageHeader } from './components/useAdminPageHeader';
|
||||
|
||||
type ListPopupAdsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPopupAds>>;
|
||||
type AdminPopupAdRow = NonNullable<ListPopupAdsResponse['items']>[number];
|
||||
|
||||
const popupTypes = ['url', 'script'] as const;
|
||||
const loading = ref(true);
|
||||
const submitting = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const actionError = ref<string | null>(null);
|
||||
const rows = ref<AdminPopupAdRow[]>([]);
|
||||
const total = ref(0);
|
||||
const limit = ref(12);
|
||||
const page = ref(1);
|
||||
const search = ref('');
|
||||
const appliedSearch = ref('');
|
||||
const ownerFilter = ref('');
|
||||
const appliedOwnerFilter = ref('');
|
||||
const selectedRow = ref<AdminPopupAdRow | null>(null);
|
||||
const createOpen = ref(false);
|
||||
const editOpen = ref(false);
|
||||
const deleteOpen = ref(false);
|
||||
|
||||
const createForm = reactive({ userId: '', type: 'url', label: '', value: '', isActive: true, maxTriggersPerSession: 3 });
|
||||
const editForm = reactive({ id: '', userId: '', type: 'url', label: '', value: '', isActive: true, maxTriggersPerSession: 3 });
|
||||
|
||||
const canCreate = computed(() => createForm.userId.trim() && createForm.label.trim() && createForm.value.trim());
|
||||
const canUpdate = computed(() => editForm.id.trim() && editForm.userId.trim() && editForm.label.trim() && editForm.value.trim());
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / limit.value)));
|
||||
const summary = computed(() => [
|
||||
{ label: 'Visible records', value: rows.value.length },
|
||||
{ label: 'Active', value: rows.value.filter((row) => row.isActive).length },
|
||||
{ label: 'URL type', value: rows.value.filter((row) => row.type === 'url').length },
|
||||
{ label: 'Script type', value: rows.value.filter((row) => row.type === 'script').length },
|
||||
]);
|
||||
|
||||
const loadPopupAds = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await rpcClient.listAdminPopupAds({ page: page.value, limit: limit.value, userId: appliedOwnerFilter.value.trim() || undefined, search: appliedSearch.value.trim() || undefined });
|
||||
rows.value = response.items ?? [];
|
||||
total.value = response.total ?? rows.value.length;
|
||||
limit.value = response.limit ?? limit.value;
|
||||
page.value = response.page ?? page.value;
|
||||
} catch (err: any) {
|
||||
error.value = err?.message || 'Failed to load admin popup ads';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = async () => {
|
||||
page.value = 1;
|
||||
appliedSearch.value = search.value;
|
||||
appliedOwnerFilter.value = ownerFilter.value;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const resetCreateForm = () => {
|
||||
createForm.userId = '';
|
||||
createForm.type = 'url';
|
||||
createForm.label = '';
|
||||
createForm.value = '';
|
||||
createForm.isActive = true;
|
||||
createForm.maxTriggersPerSession = 3;
|
||||
};
|
||||
|
||||
const openEditDialog = (row: AdminPopupAdRow) => {
|
||||
selectedRow.value = row;
|
||||
actionError.value = null;
|
||||
editForm.id = row.id || '';
|
||||
editForm.userId = row.userId || '';
|
||||
editForm.type = (row.type as 'url' | 'script') || 'url';
|
||||
editForm.label = row.label || '';
|
||||
editForm.value = row.value || '';
|
||||
editForm.isActive = !!row.isActive;
|
||||
editForm.maxTriggersPerSession = Number(row.maxTriggersPerSession || 3);
|
||||
editOpen.value = true;
|
||||
};
|
||||
|
||||
const openDeleteDialog = (row: AdminPopupAdRow) => {
|
||||
selectedRow.value = row;
|
||||
actionError.value = null;
|
||||
deleteOpen.value = true;
|
||||
};
|
||||
|
||||
const submitCreate = async () => {
|
||||
if (!canCreate.value) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.createAdminPopupAd({
|
||||
userId: createForm.userId.trim(),
|
||||
type: createForm.type,
|
||||
label: createForm.label.trim(),
|
||||
value: createForm.value.trim(),
|
||||
isActive: createForm.isActive,
|
||||
maxTriggersPerSession: createForm.type === 'url' ? createForm.maxTriggersPerSession : undefined,
|
||||
});
|
||||
resetCreateForm();
|
||||
createOpen.value = false;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to create popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitEdit = async () => {
|
||||
if (!canUpdate.value) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.updateAdminPopupAd({
|
||||
id: editForm.id,
|
||||
userId: editForm.userId.trim(),
|
||||
type: editForm.type,
|
||||
label: editForm.label.trim(),
|
||||
value: editForm.value.trim(),
|
||||
isActive: editForm.isActive,
|
||||
maxTriggersPerSession: editForm.type === 'url' ? editForm.maxTriggersPerSession : undefined,
|
||||
});
|
||||
editOpen.value = false;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to update popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitDelete = async () => {
|
||||
if (!selectedRow.value?.id) return;
|
||||
submitting.value = true;
|
||||
actionError.value = null;
|
||||
try {
|
||||
await rpcClient.deleteAdminPopupAd({ id: selectedRow.value.id });
|
||||
deleteOpen.value = false;
|
||||
selectedRow.value = null;
|
||||
if (page.value > 1 && rows.value.length === 1) page.value -= 1;
|
||||
await loadPopupAds();
|
||||
} catch (err: any) {
|
||||
actionError.value = err?.message || 'Failed to delete popup ad';
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const previousPage = async () => {
|
||||
if (page.value <= 1) return;
|
||||
page.value -= 1;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const nextPage = async () => {
|
||||
if (page.value >= totalPages.value) return;
|
||||
page.value += 1;
|
||||
await loadPopupAds();
|
||||
};
|
||||
|
||||
const columns = computed<ColumnDef<AdminPopupAdRow>[]>(() => [
|
||||
{
|
||||
id: 'label',
|
||||
header: 'Popup',
|
||||
accessorFn: row => row.label || '',
|
||||
cell: ({ row }) => h('div', { class: 'text-left' }, [
|
||||
h('div', { class: 'font-medium text-foreground' }, row.original.label),
|
||||
h('div', { class: 'mt-1 text-xs text-foreground/60' }, row.original.ownerEmail || row.original.userId || 'No owner'),
|
||||
]),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
header: 'Type',
|
||||
accessorFn: row => row.type || '',
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'value',
|
||||
header: 'Value',
|
||||
accessorFn: row => row.value || '',
|
||||
cell: ({ row }) => h('code', { class: 'block max-w-[360px] truncate text-xs text-foreground/60' }, row.original.value || '—'),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'maxTriggersPerSession',
|
||||
header: 'Max triggers/session',
|
||||
accessorFn: row => row.maxTriggersPerSession || 0,
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-foreground/70' },
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
accessorFn: row => row.isActive ? 'ACTIVE' : 'INACTIVE',
|
||||
cell: ({ row }) => h('span', { class: 'text-foreground/70' }, row.original.isActive ? 'ACTIVE' : 'INACTIVE'),
|
||||
meta: { headerClass: 'px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3' },
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => h('div', { class: 'flex justify-end gap-2' }, [
|
||||
h(AppButton, { size: 'sm', variant: 'secondary', onClick: () => openEditDialog(row.original) }, { default: () => 'Edit' }),
|
||||
h(AppButton, { size: 'sm', variant: 'danger', onClick: () => openDeleteDialog(row.original) }, { default: () => 'Delete' }),
|
||||
]),
|
||||
meta: { headerClass: 'px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-foreground/50', cellClass: 'px-4 py-3 text-right' },
|
||||
},
|
||||
]);
|
||||
|
||||
useAdminPageHeader(() => ({
|
||||
eyebrow: 'Advertising',
|
||||
badge: loading.value ? 'Syncing popup inventory' : `${total.value} total popups`,
|
||||
actions: [
|
||||
{ label: 'Refresh', variant: 'secondary', loading: loading.value, onClick: loadPopupAds },
|
||||
{ label: 'Create popup', onClick: () => { actionError.value = null; createOpen.value = true; } },
|
||||
],
|
||||
}));
|
||||
|
||||
onMounted(loadPopupAds);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminSectionShell>
|
||||
<template #stats>
|
||||
<AdminMetricCard v-for="item in summary" :key="item.label" :label="item.label" :value="item.value" />
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<AdminSectionCard title="Filters" description="Search popup ads by label and narrow by owner reference if needed." bodyClass="p-5">
|
||||
<div class="grid gap-3 xl:grid-cols-[minmax(0,1fr)_220px_auto] xl:items-end">
|
||||
<div class="space-y-2"><label class="text-xs font-medium text-foreground/60">Search</label><AdminInput v-model="search" placeholder="Search popup label" @enter="applyFilters" /></div>
|
||||
<div class="space-y-2"><label class="text-xs font-medium text-foreground/60">Owner reference</label><AdminInput v-model="ownerFilter" placeholder="Optional owner reference" @enter="applyFilters" /></div>
|
||||
<div class="flex items-center gap-2 xl:justify-end"><AppButton size="sm" variant="ghost" @click="search = ''; ownerFilter = ''; appliedSearch = ''; appliedOwnerFilter = ''; loadPopupAds()">Reset</AppButton><AppButton size="sm" variant="secondary" @click="applyFilters">Apply</AppButton></div>
|
||||
</div>
|
||||
</AdminSectionCard>
|
||||
|
||||
<div v-if="error" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ error }}</div>
|
||||
|
||||
<AdminSectionCard v-else title="Popup Ads" description="Popup ad inventory across users." bodyClass="">
|
||||
<AdminPlaceholderTable v-if="loading" :columns="6" :rows="4" />
|
||||
<AdminTable v-else :data="rows" :columns="columns" :get-row-id="(row) => row.id || row.label || ''" wrapperClass="border-x-0 border-t-0 rounded-none bg-transparent" tableClass="w-full" headerRowClass="bg-muted/30" bodyRowClass="border-b border-border hover:bg-muted/30">
|
||||
<template #empty>
|
||||
<div class="px-6 py-12 text-center"><p class="mb-1 text-sm text-foreground/60">No popup ads matched the current filters.</p><p class="text-xs text-foreground/40">Try a broader label or clear the owner filter.</p></div>
|
||||
</template>
|
||||
</AdminTable>
|
||||
<div class="flex flex-col gap-3 border-t border-border bg-muted/20 px-6 py-4 md:flex-row md:items-center md:justify-between">
|
||||
<div class="text-xs text-foreground/55">Page {{ page }} of {{ totalPages }} · {{ total }} records</div>
|
||||
<div class="flex items-center gap-2"><AppButton size="sm" variant="secondary" :disabled="page <= 1 || loading" @click="previousPage">Previous</AppButton><AppButton size="sm" variant="secondary" :disabled="page >= totalPages || loading" @click="nextPage">Next</AppButton></div>
|
||||
</div>
|
||||
</AdminSectionCard>
|
||||
</div>
|
||||
</AdminSectionShell>
|
||||
|
||||
<AppDialog v-model:visible="createOpen" title="Create popup ad" maxWidthClass="max-w-2xl" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Owner user ID</label><AdminInput v-model="createForm.userId" placeholder="user-id" /></div>
|
||||
<div class="space-y-2"><label class="text-sm font-medium text-foreground/70">Type</label><AdminSelect v-model="createForm.type"><option v-for="type in popupTypes" :key="type" :value="type">{{ type }}</option></AdminSelect></div>
|
||||
<div v-if="createForm.type === 'url'" class="space-y-2"><label class="text-sm font-medium text-foreground/70">Max triggers / session</label><AdminInput v-model="createForm.maxTriggersPerSession" type="number" min="1" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Label</label><AdminInput v-model="createForm.label" placeholder="Homepage campaign" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Value</label><AdminInput v-if="createForm.type === 'url'" v-model="createForm.value" placeholder="https://..." /><textarea v-else v-model="createForm.value" rows="4" class="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm" placeholder="<script async src='//example.com/ad.js'></script>" /></div>
|
||||
<label class="flex items-center gap-2 text-sm text-foreground/70"><input v-model="createForm.isActive" type="checkbox" class="h-4 w-4" />Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="createOpen = false">Cancel</AppButton><AppButton size="sm" :loading="submitting" :disabled="!canCreate" @click="submitCreate">Create</AppButton></div></template>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model:visible="editOpen" title="Edit popup ad" maxWidthClass="max-w-2xl" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Owner user ID</label><AdminInput v-model="editForm.userId" /></div>
|
||||
<div class="space-y-2"><label class="text-sm font-medium text-foreground/70">Type</label><AdminSelect v-model="editForm.type"><option v-for="type in popupTypes" :key="type" :value="type">{{ type }}</option></AdminSelect></div>
|
||||
<div v-if="editForm.type === 'url'" class="space-y-2"><label class="text-sm font-medium text-foreground/70">Max triggers / session</label><AdminInput v-model="editForm.maxTriggersPerSession" type="number" min="1" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Label</label><AdminInput v-model="editForm.label" /></div>
|
||||
<div class="space-y-2 md:col-span-2"><label class="text-sm font-medium text-foreground/70">Value</label><AdminInput v-if="editForm.type === 'url'" v-model="editForm.value" /><textarea v-else v-model="editForm.value" rows="4" class="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm" /></div>
|
||||
<label class="flex items-center gap-2 text-sm text-foreground/70"><input v-model="editForm.isActive" type="checkbox" class="h-4 w-4" />Active</label>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="editOpen = false">Cancel</AppButton><AppButton size="sm" :loading="submitting" :disabled="!canUpdate" @click="submitEdit">Save</AppButton></div></template>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model:visible="deleteOpen" title="Delete popup ad" maxWidthClass="max-w-md" @close="actionError = null">
|
||||
<div class="space-y-4">
|
||||
<div v-if="actionError" class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">{{ actionError }}</div>
|
||||
<p class="text-sm text-foreground/70">Delete popup ad <span class="font-medium">{{ selectedRow?.label || 'this popup' }}</span>.</p>
|
||||
</div>
|
||||
<template #footer><div class="flex justify-end gap-2"><AppButton variant="secondary" size="sm" :disabled="submitting" @click="deleteOpen = false">Cancel</AppButton><AppButton variant="danger" size="sm" :loading="submitting" @click="submitDelete">Delete</AppButton></div></template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import AppDialog from '@/components/ui/AppDialog.vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { useUploadQueue } from '@/composables/useUploadQueue';
|
||||
import { useUIState } from '@/stores/uiState';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { ref } from 'vue';
|
||||
import RemoteUrlForm from './components/RemoteUrlForm.vue';
|
||||
import UploadDropzone from './components/UploadDropzone.vue';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
|
||||
const uiState = useUIState();
|
||||
const toast = useAppToast();
|
||||
const mode = ref<'local' | 'remote'>('local');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||
import type { Video as ModelVideo } from '@/server/api/proto/app/v1/common';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import type { AdTemplate as ManualAdTemplate, Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||
import type { AdTemplate as ManualAdTemplate, Video as ModelVideo } from '@/server/api/proto/app/v1/common';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { client as rpcClient } from '@/api/rpcclient';
|
||||
import type { Video as ModelVideo } from '@/server/gen/proto/app/v1/common';
|
||||
import type { Video as ModelVideo } from '@/server/api/proto/app/v1/common';
|
||||
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import { createStaticVNode, computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useTranslation } from 'i18next-vue';
|
||||
import { computed, createStaticVNode, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import ClientOnly from '@/components/ClientOnly';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import { useUploadQueue } from '@/composables/useUploadQueue';
|
||||
import { useUIState } from '@/stores/uiState';
|
||||
import { useAppToast } from '@/composables/useAppToast';
|
||||
import VideoBulkActions from './components/VideoBulkActions.vue';
|
||||
import VideoFilters from './components/VideoFilters.vue';
|
||||
import VideoTable from './components/VideoTable.vue';
|
||||
@@ -29,7 +30,7 @@ const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
const searchQuery = ref('');
|
||||
const selectedStatus = ref<string>('all');
|
||||
const iconHoist = createStaticVNode(`<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h10a4 4 0 004-4v-1a4 4 0 00-4-4H7a4 4 0 00-4 4v1zM16 7l-4-4m0 0L8 7m4-4v12" /></svg>`, 1)
|
||||
const iconHoist = createStaticVNode(`<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 468 534"><path d="M10 364v64c0 53 43 96 96 96h256c53 0 96-43 96-96v-64c0-18-14-32-32-32s-32 14-32 32v64c0 18-14 32-32 32H106c-18 0-32-14-32-32v-64c0-18-14-32-32-32s-32 14-32 32z" fill="color-mix(in srgb, var(--colors-white-DEFAULT) 60%, transparent)"/><path d="M217 19c9-9 25-9 34 0l144 144c7 7 9 17 5 26-3 9-12 15-22 15h-80v112c0 27-21 48-48 48h-32c-26 0-48-21-48-48V204H90c-10 0-18-6-22-15s-2-19 5-26L217 19z" fill="var(--colors-white-DEFAULT)"/></svg>`, 1)
|
||||
|
||||
// Pagination
|
||||
const page = ref(1);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user