ai-vibe
This commit is contained in:
400
bun.lock
400
bun.lock
@@ -10,19 +10,17 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.971.0",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hiogawa/utils": "^1.7.0",
|
||||
"@primeuix/themes": "^2.0.3",
|
||||
"@primevue/forms": "^4.5.4",
|
||||
"@tanstack/vue-form": "^1.28.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.1.2",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"hono": "^4.11.4",
|
||||
"is-mobile": "^5.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"primevue": "^4.5.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-router": "^5.0.2",
|
||||
"zod": "^4.3.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -255,40 +253,6 @@
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@3.2.0", "", {}, "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA=="],
|
||||
|
||||
"@firebase/app-check-interop-types": ["@firebase/app-check-interop-types@0.3.3", "", {}, "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A=="],
|
||||
|
||||
"@firebase/app-types": ["@firebase/app-types@0.9.3", "", {}, "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw=="],
|
||||
|
||||
"@firebase/auth-interop-types": ["@firebase/auth-interop-types@0.2.4", "", {}, "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA=="],
|
||||
|
||||
"@firebase/component": ["@firebase/component@0.7.0", "", { "dependencies": { "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg=="],
|
||||
|
||||
"@firebase/database": ["@firebase/database@1.1.0", "", { "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", "@firebase/component": "0.7.0", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" } }, "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg=="],
|
||||
|
||||
"@firebase/database-compat": ["@firebase/database-compat@2.1.0", "", { "dependencies": { "@firebase/component": "0.7.0", "@firebase/database": "1.1.0", "@firebase/database-types": "1.0.16", "@firebase/logger": "0.5.0", "@firebase/util": "1.13.0", "tslib": "^2.1.0" } }, "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg=="],
|
||||
|
||||
"@firebase/database-types": ["@firebase/database-types@1.0.16", "", { "dependencies": { "@firebase/app-types": "0.9.3", "@firebase/util": "1.13.0" } }, "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw=="],
|
||||
|
||||
"@firebase/logger": ["@firebase/logger@0.5.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g=="],
|
||||
|
||||
"@firebase/util": ["@firebase/util@1.13.0", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ=="],
|
||||
|
||||
"@google-cloud/firestore": ["@google-cloud/firestore@7.11.6", "", { "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", "google-gax": "^4.3.3", "protobufjs": "^7.2.6" } }, "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw=="],
|
||||
|
||||
"@google-cloud/paginator": ["@google-cloud/paginator@5.0.2", "", { "dependencies": { "arrify": "^2.0.0", "extend": "^3.0.2" } }, "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg=="],
|
||||
|
||||
"@google-cloud/projectify": ["@google-cloud/projectify@4.0.0", "", {}, "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA=="],
|
||||
|
||||
"@google-cloud/promisify": ["@google-cloud/promisify@4.0.0", "", {}, "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g=="],
|
||||
|
||||
"@google-cloud/storage": ["@google-cloud/storage@7.18.0", "", { "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", "@google-cloud/promisify": "<4.1.0", "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", "fast-xml-parser": "^4.4.1", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", "mime": "^3.0.0", "p-limit": "^3.0.1", "retry-request": "^7.0.0", "teeny-request": "^9.0.0", "uuid": "^8.0.0" } }, "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
|
||||
|
||||
"@hiogawa/tiny-rpc": ["@hiogawa/tiny-rpc@0.2.3-pre.18", "", {}, "sha512-BiNHrutG9G9yV622QvkxZxF+PhkaH2Aspp4/X1KYTfnaQTcg4fFUTBWf5Kf533swon2SuVJwi6U6H1LQbhVOQQ=="],
|
||||
|
||||
"@hiogawa/utils": ["@hiogawa/utils@1.7.0", "", {}, "sha512-ghiEFWBR1NENoHn+lSuW7liicTIzVPN+8Srm5UedCTw43gus0mlse6Wp2lz6GmbOXJ/CalMPp/0Tz2X8tajkAg=="],
|
||||
@@ -355,11 +319,7 @@
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
@@ -369,46 +329,10 @@
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
||||
|
||||
"@primeuix/forms": ["@primeuix/forms@0.1.0", "", { "dependencies": { "@primeuix/utils": "^0.6.0" } }, "sha512-LctcQidb+B5PuvAFWH24YH/SIzmHlOabLHpaTeGY/k51iBv1WyCp+5w9JMYuMB/BplSvV0ZGySxQVkN5Azr/aQ=="],
|
||||
|
||||
"@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="],
|
||||
|
||||
"@primeuix/styles": ["@primeuix/styles@2.0.2", "", { "dependencies": { "@primeuix/styled": "^0.7.4" } }, "sha512-LNtkJsTonNHF5ag+9s3+zQzm00+LRmffw68QRIHy6S/dam1JpdrrAnUzNYlWbaY7aE2EkZvQmx7Np7+PyHn+ow=="],
|
||||
|
||||
"@primeuix/themes": ["@primeuix/themes@2.0.3", "", { "dependencies": { "@primeuix/styled": "^0.7.4" } }, "sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg=="],
|
||||
|
||||
"@primeuix/utils": ["@primeuix/utils@0.6.3", "", {}, "sha512-/SLNQSKQ73WbBIsflKVqbpVjCfFYvQO3Sf1LMheXyxh8JqxO4M63dzP56wwm9OPGuCQ6MYOd2AHgZXz+g7PZcg=="],
|
||||
|
||||
"@primevue/auto-import-resolver": ["@primevue/auto-import-resolver@4.5.4", "", { "dependencies": { "@primevue/metadata": "4.5.4" } }, "sha512-YQHrZ9PQSG/4K2BwthA2Xuna4WyS0JMHajiHD9PljaDyQtBVwCadX5ZpKcrAUWR8E/1gjva8x/si0RYxxYrRJw=="],
|
||||
|
||||
"@primevue/core": ["@primevue/core@4.5.4", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/utils": "^0.6.2" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-lYJJB3wTrDJ8MkLctzHfrPZAqXVxoatjIsswSJzupatf6ZogJHVYADUKcn1JAkLLk8dtV1FA2AxDek663fHO5Q=="],
|
||||
|
||||
"@primevue/forms": ["@primevue/forms@4.5.4", "", { "dependencies": { "@primeuix/forms": "^0.1.0", "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.4" } }, "sha512-2TlD8oJEtb8vuKzY3jY0W+7NVBC/Qj0m57iWzpMUmGnEKg9sbQ2/ZiU1sTof710/liYgm4FneRTOYHIpVkiJNA=="],
|
||||
|
||||
"@primevue/icons": ["@primevue/icons@4.5.4", "", { "dependencies": { "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.4" } }, "sha512-DxgryEc7ZmUqcEhYMcxGBRyFzdtLIoy3jLtlH1zsVSRZaG+iSAcjQ88nvfkZxGUZtZBFL7sRjF6KLq3bJZJwUw=="],
|
||||
|
||||
"@primevue/metadata": ["@primevue/metadata@4.5.4", "", {}, "sha512-jJFD0KYm8bPYgFo0JP3Dc2RkyXzrMI1XHQGsEKTysx9Jx2d1XdxtFji/ZsQeoo/RmwUNof5ciZ72URq37rnK+g=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@quansync/fs": ["@quansync/fs@1.0.0", "", { "dependencies": { "quansync": "^1.0.0" } }, "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||
@@ -569,44 +493,26 @@
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="],
|
||||
|
||||
"@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="],
|
||||
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.0", "", {}, "sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw=="],
|
||||
|
||||
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
|
||||
"@tanstack/form-core": ["@tanstack/form-core@1.28.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.0", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.7.7" } }, "sha512-MX3YveB6SKHAJ2yUwp+Ca/PCguub8bVEnLcLUbFLwdkSRMkP0lMGdaZl+F0JuEgZw56c6iFoRyfILhS7OQpydA=="],
|
||||
|
||||
"@types/caseless": ["@types/caseless@0.12.5", "", {}, "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="],
|
||||
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
|
||||
|
||||
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
|
||||
"@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
|
||||
|
||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||
|
||||
"@tanstack/vue-form": ["@tanstack/vue-form@1.28.0", "", { "dependencies": { "@tanstack/form-core": "1.28.0", "@tanstack/vue-store": "^0.7.7" }, "peerDependencies": { "vue": "^3.4.0" } }, "sha512-UBO9DLFHrOyxDRQ0qLd789vgaKRECsNOYupW+ATjmqnOOV6l7JzTjCnjc35GVjIAgYyiZ1un4bPw1qyFM2G1OQ=="],
|
||||
|
||||
"@tanstack/vue-store": ["@tanstack/vue-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "vue-demi": "^0.14.10" }, "peerDependencies": { "@vue/composition-api": "^1.2.1", "vue": "^2.5.0 || ^3.0.0" }, "optionalPeers": ["@vue/composition-api"] }, "sha512-6iv1Odmreff6TgEjQN11xoddsCnpn+/ul7MZ2DadHT3/RSY1YdoFafK8lCa889MEFi/5K0zAhf8psIkgTrRa9A=="],
|
||||
|
||||
"@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="],
|
||||
|
||||
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="],
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
||||
|
||||
"@types/long": ["@types/long@4.0.2", "", {}, "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
|
||||
"@types/request": ["@types/request@2.48.13", "", { "dependencies": { "@types/caseless": "*", "@types/node": "*", "@types/tough-cookie": "*", "form-data": "^2.5.5" } }, "sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg=="],
|
||||
|
||||
"@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="],
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@unhead/vue": ["@unhead/vue@2.1.2", "", { "dependencies": { "hookable": "^6.0.1", "unhead": "2.1.2" }, "peerDependencies": { "vue": ">=3.5.18" } }, "sha512-w5yxH/fkkLWAFAOnMSIbvAikNHYn6pgC7zGF/BasXf+K3CO1cYIPFehYAk5jpcsbiNPMc3goyyw1prGLoyD14g=="],
|
||||
@@ -663,6 +569,8 @@
|
||||
|
||||
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.1.3", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5", "@rolldown/pluginutils": "^1.0.0-beta.56", "@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-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@2.0.1", "", {}, "sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA=="],
|
||||
|
||||
"@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@2.0.1", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@vue/babel-helper-vue-transform-on": "2.0.1", "@vue/babel-plugin-resolve-type": "2.0.1", "@vue/shared": "^3.5.22" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA=="],
|
||||
@@ -699,28 +607,14 @@
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@14.1.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
"ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"arrify": ["arrify@2.0.1", "", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
|
||||
|
||||
"async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
"ast-walker-scope": ["ast-walker-scope@0.8.3", "", { "dependencies": { "@babel/parser": "^7.28.4", "ast-kit": "^2.1.3" } }, "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
@@ -729,28 +623,16 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
|
||||
|
||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
@@ -769,38 +651,18 @@
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="],
|
||||
|
||||
"duplexify": ["duplexify@4.1.3", "", { "dependencies": { "end-of-stream": "^1.4.1", "inherits": "^2.0.3", "readable-stream": "^3.1.1", "stream-shift": "^1.0.2" } }, "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||
|
||||
"entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
@@ -809,148 +671,48 @@
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"farmhash-modern": ["farmhash-modern@1.1.0", "", {}, "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
|
||||
|
||||
"faye-websocket": ["faye-websocket@0.11.4", "", { "dependencies": { "websocket-driver": ">=0.5.1" } }, "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g=="],
|
||||
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"firebase-admin": ["firebase-admin@13.6.0", "", { "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", "@firebase/database-types": "^1.0.6", "@types/node": "^22.8.7", "farmhash-modern": "^1.1.0", "fast-deep-equal": "^3.1.1", "google-auth-library": "^9.14.2", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.1.0", "node-forge": "^1.3.1", "uuid": "^11.0.2" }, "optionalDependencies": { "@google-cloud/firestore": "^7.11.0", "@google-cloud/storage": "^7.14.0" } }, "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw=="],
|
||||
|
||||
"form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"functional-red-black-tree": ["functional-red-black-tree@1.0.1", "", {}, "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g=="],
|
||||
|
||||
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
|
||||
|
||||
"google-gax": ["google-gax@4.6.1", "", { "dependencies": { "@grpc/grpc-js": "^1.10.9", "@grpc/proto-loader": "^0.7.13", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "google-auth-library": "^9.3.0", "node-fetch": "^2.7.0", "object-hash": "^3.0.0", "proto3-json-serializer": "^2.0.2", "protobufjs": "^7.3.2", "retry-request": "^7.0.0", "uuid": "^9.0.1" } }, "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
|
||||
|
||||
"gzip-size": ["gzip-size@6.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
|
||||
|
||||
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="],
|
||||
|
||||
"html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="],
|
||||
|
||||
"http-parser-js": ["http-parser-js@0.5.10", "", {}, "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA=="],
|
||||
|
||||
"http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-mobile": ["is-mobile@5.0.0", "", {}, "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jwks-rsa": ["jwks-rsa@3.2.0", "", { "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", "limiter": "^1.1.5", "lru-memoizer": "^2.2.0" } }, "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"limiter": ["limiter@1.1.5", "", {}, "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="],
|
||||
|
||||
"local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="],
|
||||
|
||||
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"lodash.clonedeep": ["lodash.clonedeep@4.5.0", "", {}, "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lru-memoizer": ["lru-memoizer@2.3.0", "", { "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" } }, "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
"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=="],
|
||||
|
||||
"mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20260114.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.14.0", "workerd": "1.20260114.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "^3.25.76" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-QwHT7S6XqGdQxIvql1uirH/7/i3zDEt0B/YBXTYzMfJtVCR4+ue3KPkU+Bl0zMxvpgkvjh9+eCHhJbKEqya70A=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
@@ -961,26 +723,18 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
@@ -999,30 +753,14 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"primevue": ["primevue@4.5.4", "", { "dependencies": { "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^2.0.2", "@primeuix/utils": "^0.6.2", "@primevue/core": "4.5.4", "@primevue/icons": "4.5.4" } }, "sha512-nTyEohZABFJhVIpeUxgP0EJ8vKcJAhD+Z7DYj95e7ie/MNUCjRNcGjqmE1cXtXi4z54qDfTSI9h2uJ51qz2DIw=="],
|
||||
|
||||
"proto3-json-serializer": ["proto3-json-serializer@2.0.2", "", { "dependencies": { "protobufjs": "^7.2.5" } }, "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||
|
||||
"retry-request": ["retry-request@7.0.2", "", { "dependencies": { "@types/request": "^2.48.8", "extend": "^3.0.2", "teeny-request": "^9.0.0" } }, "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -1035,21 +773,9 @@
|
||||
|
||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||
|
||||
"stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="],
|
||||
|
||||
"stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
||||
|
||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
||||
|
||||
"stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="],
|
||||
"strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
||||
|
||||
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
|
||||
|
||||
@@ -1057,16 +783,12 @@
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"teeny-request": ["teeny-request@9.0.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.9", "stream-events": "^1.0.5", "uuid": "^9.0.0" } }, "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
@@ -1097,49 +819,29 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
|
||||
|
||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
"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.27", "", { "dependencies": { "@vue/compiler-dom": "3.5.27", "@vue/compiler-sfc": "3.5.27", "@vue/runtime-dom": "3.5.27", "@vue/server-renderer": "3.5.27", "@vue/shared": "3.5.27" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw=="],
|
||||
|
||||
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
|
||||
|
||||
"vue-flow-layout": ["vue-flow-layout@0.2.0", "", {}, "sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q=="],
|
||||
|
||||
"vue-router": ["vue-router@4.6.4", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
"vue-router": ["vue-router@5.0.2", "", { "dependencies": { "@babel/generator": "^7.28.6", "@vue-macros/common": "^3.1.1", "@vue/devtools-api": "^8.0.0", "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-YFhwaE5c5JcJpNB1arpkl4/GnO32wiUWRB+OEj1T0DlDxEZoOfbltl2xEwktNU/9o1sGcGburIXSpbLpPFe/6w=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"websocket-driver": ["websocket-driver@0.7.4", "", { "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg=="],
|
||||
|
||||
"websocket-extensions": ["websocket-extensions@0.1.4", "", {}, "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"workerd": ["workerd@1.20260114.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260114.0", "@cloudflare/workerd-darwin-arm64": "1.20260114.0", "@cloudflare/workerd-linux-64": "1.20260114.0", "@cloudflare/workerd-linux-arm64": "1.20260114.0", "@cloudflare/workerd-windows-64": "1.20260114.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kTJ+jNdIllOzWuVA3NRQRvywP0T135zdCjAE2dAUY1BFbxM6fmMZV8BbskEoQ4hAODVQUfZQmyGctcwvVCKxFA=="],
|
||||
|
||||
"wrangler": ["wrangler@4.59.2", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.10.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260114.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260114.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260114.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-Z4xn6jFZTaugcOKz42xvRAYKgkVUERHVbuCJ5+f+gK+R6k12L02unakPGOA0L0ejhUl16dqDjKe4tmL9sedHcw=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
@@ -1153,17 +855,7 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@google-cloud/storage/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
|
||||
|
||||
"@jridgewell/gen-mapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@jridgewell/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||
|
||||
@@ -1185,18 +877,6 @@
|
||||
|
||||
"@vue/devtools-kit/perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"firebase-admin/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
||||
|
||||
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"google-gax/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"lru-memoizer/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"miniflare/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -1205,15 +885,13 @@
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"teeny-request/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"unconfig/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||
|
||||
"unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
|
||||
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
"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/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
|
||||
|
||||
"wrangler/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
|
||||
|
||||
@@ -1223,8 +901,6 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@aws-sdk/xml-builder/fast-xml-parser/strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
|
||||
|
||||
"@unocss/transformer-attributify-jsx/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-core": ["@vue/compiler-core@3.5.26", "", { "dependencies": { "@babel/parser": "^7.28.5", "@vue/shared": "3.5.26", "entities": "^7.0.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w=="],
|
||||
@@ -1237,13 +913,9 @@
|
||||
|
||||
"@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"firebase-admin/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"lru-memoizer/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
"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=="],
|
||||
|
||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
|
||||
|
||||
@@ -1302,5 +974,9 @@
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"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/hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
52
components.d.ts
vendored
52
components.d.ts
vendored
@@ -16,10 +16,12 @@ declare module 'vue' {
|
||||
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||
Avatar: typeof import('./src/components/ui/Avatar.vue')['default']
|
||||
Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
Button: typeof import('primevue/button')['default']
|
||||
Button: typeof import('./src/components/ui/Button.vue')['default']
|
||||
Card: typeof import('./src/components/ui/Card.vue')['default']
|
||||
Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
Checkbox: typeof import('primevue/checkbox')['default']
|
||||
Checkbox: typeof import('./src/components/ui/Checkbox.vue')['default']
|
||||
CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||
@@ -28,31 +30,33 @@ declare module 'vue' {
|
||||
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||
DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
||||
DataTable: typeof import('./src/components/table/DataTable.vue')['default']
|
||||
Dialog: typeof import('./src/components/ui/Dialog.vue')['default']
|
||||
EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||
FloatLabel: typeof import('primevue/floatlabel')['default']
|
||||
Field: typeof import('./src/components/form/Field.vue')['default']
|
||||
Form: typeof import('./src/components/form/Form.vue')['default']
|
||||
GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||
Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||
IconField: typeof import('primevue/iconfield')['default']
|
||||
InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||
InputIcon: typeof import('primevue/inputicon')['default']
|
||||
InputText: typeof import('primevue/inputtext')['default']
|
||||
Input: typeof import('./src/components/ui/Input.vue')['default']
|
||||
InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
|
||||
Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
Message: typeof import('primevue/message')['default']
|
||||
Message: typeof import('./src/components/form/Message.vue')['default']
|
||||
NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||
PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||
Paginator: typeof import('primevue/paginator')['default']
|
||||
PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||
Password: typeof import('primevue/password')['default']
|
||||
ProgressBar: typeof import('./src/components/ui/ProgressBar.vue')['default']
|
||||
RootLayout: typeof import('./src/components/RootLayout.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Select: typeof import('primevue/select')['default']
|
||||
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||
Skeleton: typeof import('primevue/skeleton')['default']
|
||||
Skeleton: typeof import('./src/components/ui/Skeleton.vue')['default']
|
||||
StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||
Tag: typeof import('./src/components/ui/Tag.vue')['default']
|
||||
TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||
Toast: typeof import('./src/components/ui/Toast.vue')['default']
|
||||
TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||
Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
@@ -68,10 +72,12 @@ declare global {
|
||||
const AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
|
||||
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
|
||||
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.vue')['default']
|
||||
const Avatar: typeof import('./src/components/ui/Avatar.vue')['default']
|
||||
const Bell: typeof import('./src/components/icons/Bell.vue')['default']
|
||||
const Button: typeof import('primevue/button')['default']
|
||||
const Button: typeof import('./src/components/ui/Button.vue')['default']
|
||||
const Card: typeof import('./src/components/ui/Card.vue')['default']
|
||||
const Chart: typeof import('./src/components/icons/Chart.vue')['default']
|
||||
const Checkbox: typeof import('primevue/checkbox')['default']
|
||||
const Checkbox: typeof import('./src/components/ui/Checkbox.vue')['default']
|
||||
const CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
|
||||
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
|
||||
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
|
||||
@@ -80,31 +86,33 @@ declare global {
|
||||
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
|
||||
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
|
||||
const DashboardNav: typeof import('./src/components/DashboardNav.vue')['default']
|
||||
const DataTable: typeof import('./src/components/table/DataTable.vue')['default']
|
||||
const Dialog: typeof import('./src/components/ui/Dialog.vue')['default']
|
||||
const EmptyState: typeof import('./src/components/dashboard/EmptyState.vue')['default']
|
||||
const FloatLabel: typeof import('primevue/floatlabel')['default']
|
||||
const Field: typeof import('./src/components/form/Field.vue')['default']
|
||||
const Form: typeof import('./src/components/form/Form.vue')['default']
|
||||
const GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
|
||||
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
|
||||
const Home: typeof import('./src/components/icons/Home.vue')['default']
|
||||
const IconField: typeof import('primevue/iconfield')['default']
|
||||
const InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
|
||||
const InputIcon: typeof import('primevue/inputicon')['default']
|
||||
const InputText: typeof import('primevue/inputtext')['default']
|
||||
const Input: typeof import('./src/components/ui/Input.vue')['default']
|
||||
const InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
|
||||
const Layout: typeof import('./src/components/icons/Layout.vue')['default']
|
||||
const LinkIcon: typeof import('./src/components/icons/LinkIcon.vue')['default']
|
||||
const Message: typeof import('primevue/message')['default']
|
||||
const Message: typeof import('./src/components/form/Message.vue')['default']
|
||||
const NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
|
||||
const PageHeader: typeof import('./src/components/dashboard/PageHeader.vue')['default']
|
||||
const Paginator: typeof import('primevue/paginator')['default']
|
||||
const PanelLeft: typeof import('./src/components/icons/PanelLeft.vue')['default']
|
||||
const Password: typeof import('primevue/password')['default']
|
||||
const ProgressBar: typeof import('./src/components/ui/ProgressBar.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 Select: typeof import('primevue/select')['default']
|
||||
const SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['default']
|
||||
const Skeleton: typeof import('primevue/skeleton')['default']
|
||||
const Skeleton: typeof import('./src/components/ui/Skeleton.vue')['default']
|
||||
const StatsCard: typeof import('./src/components/dashboard/StatsCard.vue')['default']
|
||||
const Tag: typeof import('./src/components/ui/Tag.vue')['default']
|
||||
const TestIcon: typeof import('./src/components/icons/TestIcon.vue')['default']
|
||||
const Toast: typeof import('./src/components/ui/Toast.vue')['default']
|
||||
const TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
|
||||
const Upload: typeof import('./src/components/icons/Upload.vue')['default']
|
||||
const Video: typeof import('./src/components/icons/Video.vue')['default']
|
||||
|
||||
@@ -15,24 +15,21 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.971.0",
|
||||
"@hiogawa/tiny-rpc": "^0.2.3-pre.18",
|
||||
"@hiogawa/utils": "^1.7.0",
|
||||
"@primeuix/themes": "^2.0.3",
|
||||
"@primevue/forms": "^4.5.4",
|
||||
"@tanstack/vue-form": "^1.28.0",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.1.2",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"hono": "^4.11.4",
|
||||
"is-mobile": "^5.0.0",
|
||||
"pinia": "^3.0.4",
|
||||
"primevue": "^4.5.4",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"vue": "^3.5.27",
|
||||
"vue-router": "^4.6.4",
|
||||
"vue-router": "^5.0.2",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vite-plugin": "^1.21.0",
|
||||
"@primevue/auto-import-resolver": "^4.5.4",
|
||||
"@types/node": "^25.0.9",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.3",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts" setup>
|
||||
import Bell from "@/components/icons/Bell.vue";
|
||||
import Home from "@/components/icons/Home.vue";
|
||||
import Video from "@/components/icons/Video.vue";
|
||||
import Credit from "@/components/icons/Credit.vue";
|
||||
import Home from "@/components/icons/Home.vue";
|
||||
import Upload from "@/components/icons/Upload.vue";
|
||||
import NotificationDrawer from "./NotificationDrawer.vue";
|
||||
import Video from "@/components/icons/Video.vue";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createStaticVNode, ref } from "vue";
|
||||
import NotificationDrawer from "./NotificationDrawer.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);
|
||||
@@ -40,7 +40,7 @@ const links = [
|
||||
|
||||
<template v-for="i in links" :key="i.label">
|
||||
<component :name="i.label" :is="i.type === 'a' ? 'router-link' : 'div'"
|
||||
v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label" @click="i.action && i.action($event)"
|
||||
v-bind="i.type === 'a' ? { to: i.href } : {}" :title="i.label" @click="i.action && i.action($event)"
|
||||
:class="cn(
|
||||
i.className,
|
||||
($route.path === i.href || i.isActive?.value) && 'bg-primary/15'
|
||||
|
||||
22
src/components/form/Field.vue
Normal file
22
src/components/form/Field.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { useField } from '@tanstack/vue-form';
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
form?: any
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const field = useField({
|
||||
name: props.name,
|
||||
form: props.form
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="props.class">
|
||||
<slot :field="field" />
|
||||
</div>
|
||||
</template>
|
||||
55
src/components/form/Form.vue
Normal file
55
src/components/form/Form.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { useForm } from '@tanstack/vue-form'
|
||||
import { type ZodType } from 'zod'
|
||||
|
||||
interface Props {
|
||||
initialValues?: Record<string, any>
|
||||
onSubmit?: (values: any) => void | Promise<void>
|
||||
resolver?: ZodType<any>
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
submit: [values: any]
|
||||
}>()
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: props.initialValues || {},
|
||||
onSubmit: async ({ value }) => {
|
||||
if (props.onSubmit) {
|
||||
await props.onSubmit(value as Record<string, any>)
|
||||
}
|
||||
emit('submit', value)
|
||||
},
|
||||
validators: props.resolver
|
||||
? {
|
||||
onChange: ({ value }) => {
|
||||
const result = props.resolver!.safeParse(value)
|
||||
if (result.success) {
|
||||
return undefined
|
||||
}
|
||||
return result.error.issues.map(issue => ({
|
||||
path: issue.path.join('.'),
|
||||
message: issue.message
|
||||
}))
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
|
||||
const handleSubmit = (e: Event) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
:class="props.class"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<slot :form="form" />
|
||||
</form>
|
||||
</template>
|
||||
36
src/components/form/Message.vue
Normal file
36
src/components/form/Message.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
severity?: 'error' | 'success' | 'info' | 'warn'
|
||||
size?: 'sm' | 'md'
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
severity: 'error',
|
||||
size: 'sm'
|
||||
})
|
||||
|
||||
const severityClasses = {
|
||||
error: 'text-red-600',
|
||||
success: 'text-green-600',
|
||||
info: 'text-blue-600',
|
||||
warn: 'text-yellow-600'
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'text-xs',
|
||||
md: 'text-sm'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
severityClasses[severity],
|
||||
sizeClasses[size],
|
||||
props.class
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
36
src/components/table/Column.ts
Normal file
36
src/components/table/Column.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createColumnHelper, type ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
export { createColumnHelper }
|
||||
export type { ColumnDef }
|
||||
|
||||
// Helper function to create a simple column
|
||||
export function createColumn<T>(
|
||||
accessorKey: keyof T,
|
||||
header: string,
|
||||
options?: {
|
||||
cell?: (value: any, row: T) => any
|
||||
enableSorting?: boolean
|
||||
size?: number
|
||||
}
|
||||
): ColumnDef<T, any> {
|
||||
return {
|
||||
accessorKey: accessorKey as string,
|
||||
header,
|
||||
enableSorting: options?.enableSorting ?? true,
|
||||
size: options?.size,
|
||||
cell: options?.cell
|
||||
? ({ getValue, row }) => options.cell!(getValue(), row.original)
|
||||
: ({ getValue }) => getValue()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for selection column
|
||||
export function createSelectionColumn<T>(): ColumnDef<T, any> {
|
||||
return {
|
||||
id: 'select',
|
||||
header: ({ table }) => null,
|
||||
cell: () => null,
|
||||
size: 50,
|
||||
enableSorting: false
|
||||
}
|
||||
}
|
||||
116
src/components/table/DataTable.vue
Normal file
116
src/components/table/DataTable.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FlexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
useVueTable,
|
||||
type ColumnDef,
|
||||
type SortingState
|
||||
} from '@tanstack/vue-table'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props<T> {
|
||||
data: T[]
|
||||
columns: ColumnDef<T, any>[]
|
||||
sorting?: SortingState
|
||||
enableSorting?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props<any>>(), {
|
||||
sorting: () => [],
|
||||
enableSorting: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:sorting': [value: SortingState]
|
||||
}>()
|
||||
|
||||
const sortingState = ref<SortingState>(props.sorting)
|
||||
|
||||
const table = useVueTable({
|
||||
get data() {
|
||||
return props.data
|
||||
},
|
||||
get columns() {
|
||||
return props.columns
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: props.enableSorting ? getSortedRowModel() : undefined,
|
||||
onSortingChange: (updater) => {
|
||||
if (typeof updater === 'function') {
|
||||
sortingState.value = updater(sortingState.value)
|
||||
} else {
|
||||
sortingState.value = updater
|
||||
}
|
||||
emit('update:sorting', sortingState.value)
|
||||
},
|
||||
state: {
|
||||
get sorting() {
|
||||
return sortingState.value
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['overflow-x-auto', props.class]">
|
||||
<table class="w-full text-sm text-left">
|
||||
<thead class="text-xs text-gray-500 uppercase bg-gray-50 border-b border-gray-200">
|
||||
<tr
|
||||
v-for="headerGroup in table.getHeaderGroups()"
|
||||
:key="headerGroup.id"
|
||||
>
|
||||
<th
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
:colSpan="header.colSpan"
|
||||
:class="[
|
||||
'px-6 py-3 font-medium',
|
||||
header.column.getCanSort() ? 'cursor-pointer select-none hover:bg-gray-100' : ''
|
||||
]"
|
||||
@click="header.column.getToggleSortingHandler()?.($event)"
|
||||
>
|
||||
<FlexRender
|
||||
v-if="!header.isPlaceholder"
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
<span
|
||||
v-if="header.column.getIsSorted()"
|
||||
class="ml-1"
|
||||
>
|
||||
{{ header.column.getIsSorted() === 'asc' ? '↑' : '↓' }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
<tr
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
class="hover:bg-gray-50"
|
||||
>
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
class="px-6 py-4"
|
||||
>
|
||||
<FlexRender
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="table.getRowModel().rows.length === 0">
|
||||
<td
|
||||
:colSpan="table.getAllColumns().length"
|
||||
class="px-6 py-8 text-center text-gray-500"
|
||||
>
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
71
src/components/ui/Avatar.vue
Normal file
71
src/components/ui/Avatar.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
image?: string
|
||||
label?: string
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
shape?: 'circle' | 'square'
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'md',
|
||||
shape: 'circle'
|
||||
})
|
||||
|
||||
const sizeClasses = {
|
||||
xs: 'w-6 h-6 text-xs',
|
||||
sm: 'w-8 h-8 text-sm',
|
||||
md: 'w-10 h-10 text-base',
|
||||
lg: 'w-12 h-12 text-lg',
|
||||
xl: 'w-16 h-16 text-xl'
|
||||
}
|
||||
|
||||
const initials = computed(() => {
|
||||
if (!props.label) return ''
|
||||
return props.label
|
||||
.split(' ')
|
||||
.map(n => n[0])
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2)
|
||||
})
|
||||
|
||||
const bgColor = computed(() => {
|
||||
const colors = [
|
||||
'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500',
|
||||
'bg-lime-500', 'bg-green-500', 'bg-emerald-500', 'bg-teal-500',
|
||||
'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500',
|
||||
'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500',
|
||||
'bg-rose-500'
|
||||
]
|
||||
if (!props.label) return 'bg-gray-400'
|
||||
let hash = 0
|
||||
for (let i = 0; i < props.label.length; i++) {
|
||||
hash = props.label.charCodeAt(i) + ((hash << 5) - hash)
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'inline-flex items-center justify-center overflow-hidden font-medium text-white',
|
||||
sizeClasses[size],
|
||||
shape === 'circle' ? 'rounded-full' : 'rounded-lg',
|
||||
!image ? bgColor : '',
|
||||
props.class
|
||||
]"
|
||||
>
|
||||
<img
|
||||
v-if="image"
|
||||
:src="image"
|
||||
:alt="label || 'Avatar'"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<span v-else-if="initials">{{ initials }}</span>
|
||||
<span v-else class="i-heroicons-user w-1/2 h-1/2" />
|
||||
</div>
|
||||
</template>
|
||||
59
src/components/ui/Button.vue
Normal file
59
src/components/ui/Button.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
fluid?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'button',
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
fluid: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const variantClasses = {
|
||||
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
|
||||
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
|
||||
outline: 'border-2 border-gray-300 bg-transparent text-gray-700 hover:bg-gray-50 focus:ring-gray-500',
|
||||
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500'
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
:disabled="disabled || loading"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
fluid ? 'w-full' : '',
|
||||
props.class
|
||||
]"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<span
|
||||
v-if="loading"
|
||||
class="i-heroicons-arrow-path mr-2 animate-spin w-4 h-4"
|
||||
/>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
28
src/components/ui/Card.vue
Normal file
28
src/components/ui/Card.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden" :class="props.class">
|
||||
<!-- Header slot -->
|
||||
<div v-if="$slots.header" class="border-b border-gray-100">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div>
|
||||
<slot name="content">
|
||||
<slot />
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<!-- Footer slot -->
|
||||
<div v-if="$slots.footer" class="border-t border-gray-100">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
87
src/components/ui/Checkbox.vue
Normal file
87
src/components/ui/Checkbox.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: any[] | boolean | undefined
|
||||
value?: any
|
||||
name?: string
|
||||
disabled?: boolean
|
||||
size?: 'sm' | 'md'
|
||||
binary?: boolean
|
||||
inputId?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
size: 'md',
|
||||
binary: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: any[] | boolean]
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5'
|
||||
}
|
||||
|
||||
const isChecked = (): boolean => {
|
||||
if (props.binary) {
|
||||
return !!(props.modelValue as boolean)
|
||||
}
|
||||
return Array.isArray(props.modelValue) && props.value !== undefined
|
||||
? props.modelValue.includes(props.value)
|
||||
: false
|
||||
}
|
||||
|
||||
const toggle = (event?: MouseEvent) => {
|
||||
if (props.binary) {
|
||||
emit('update:modelValue', !props.modelValue)
|
||||
} else {
|
||||
const currentValue = Array.isArray(props.modelValue) ? props.modelValue : []
|
||||
if (props.value !== undefined) {
|
||||
if (currentValue.includes(props.value)) {
|
||||
emit('update:modelValue', currentValue.filter(v => v !== props.value))
|
||||
} else {
|
||||
emit('update:modelValue', [...currentValue, props.value])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event) {
|
||||
emit('click', event)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center"
|
||||
:class="disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'"
|
||||
@click="!disabled && toggle($event)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
sizeClasses[size],
|
||||
'rounded border-2 flex items-center justify-center transition-colors',
|
||||
isChecked()
|
||||
? 'bg-blue-600 border-blue-600'
|
||||
: 'bg-white border-gray-300 hover:border-gray-400'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
v-if="isChecked()"
|
||||
class="i-heroicons-check text-white"
|
||||
:class="size === 'sm' ? 'w-3 h-3' : 'w-4 h-4'"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
:name="name"
|
||||
:id="inputId"
|
||||
:checked="isChecked()"
|
||||
:disabled="disabled"
|
||||
class="sr-only"
|
||||
@change="toggle()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
124
src/components/ui/Dialog.vue
Normal file
124
src/components/ui/Dialog.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
header?: string
|
||||
width?: string
|
||||
closable?: boolean
|
||||
draggable?: boolean
|
||||
modal?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
header: '',
|
||||
width: '28rem',
|
||||
closable: true,
|
||||
draggable: false,
|
||||
modal: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
}>()
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleBackdropClick = () => {
|
||||
if (props.closable) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && props.visible && props.closable) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
document.body.style.overflow = 'hidden'
|
||||
} else {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 z-50"
|
||||
:class="[modal ? 'bg-black/50' : '']"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-150"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="relative bg-white rounded-xl shadow-xl"
|
||||
:style="{ width, maxWidth: 'calc(100vw - 2rem)' }"
|
||||
:class="props.class"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
v-if="header || $slots.header || closable"
|
||||
class="flex items-center justify-between px-6 py-4 border-b border-gray-200"
|
||||
>
|
||||
<slot name="header">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ header }}</h3>
|
||||
</slot>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
@click="handleClose"
|
||||
>
|
||||
<span class="i-heroicons-x-mark w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="px-6 py-4">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div v-if="$slots.footer" class="px-6 py-4 border-t border-gray-200">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
75
src/components/ui/Input.vue
Normal file
75
src/components/ui/Input.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | number
|
||||
name?: string
|
||||
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url'
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
fluid?: boolean
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
invalid?: boolean
|
||||
class?: string | Record<string, boolean>
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'text',
|
||||
disabled: false,
|
||||
fluid: false,
|
||||
size: 'md',
|
||||
invalid: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number]
|
||||
blur: [event: FocusEvent]
|
||||
focus: [event: FocusEvent]
|
||||
input: [event: Event]
|
||||
}>()
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-4 py-3 text-base'
|
||||
}
|
||||
|
||||
const baseClasses = computed(() => [
|
||||
'block w-full rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||
'disabled:bg-gray-100 disabled:cursor-not-allowed',
|
||||
props.invalid ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : '',
|
||||
sizeClasses[props.size]
|
||||
])
|
||||
|
||||
const inputClasses = computed(() => {
|
||||
if (typeof props.class === 'string') {
|
||||
return twMerge(baseClasses.value.join(' '), props.class)
|
||||
}
|
||||
return twMerge(baseClasses.value.join(' '))
|
||||
})
|
||||
|
||||
const handleInput = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = props.type === 'number'
|
||||
? (target.valueAsNumber || 0)
|
||||
: target.value
|
||||
emit('update:modelValue', value)
|
||||
emit('input', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
:name="name"
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="inputClasses"
|
||||
@input="handleInput"
|
||||
@blur="emit('blur', $event)"
|
||||
@focus="emit('focus', $event)"
|
||||
/>
|
||||
</template>
|
||||
105
src/components/ui/InputPassword.vue
Normal file
105
src/components/ui/InputPassword.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
name?: string
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
fluid?: boolean
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
invalid?: boolean
|
||||
feedback?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabled: false,
|
||||
fluid: false,
|
||||
size: 'md',
|
||||
invalid: false,
|
||||
feedback: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
blur: [event: FocusEvent]
|
||||
focus: [event: FocusEvent]
|
||||
}>()
|
||||
|
||||
const showPassword = ref(false)
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-4 py-3 text-base'
|
||||
}
|
||||
|
||||
const inputClasses = computed(() => [
|
||||
'block w-full rounded-lg border border-gray-300 bg-white text-gray-900 placeholder-gray-400',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||
'disabled:bg-gray-100 disabled:cursor-not-allowed pr-10',
|
||||
props.invalid ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : '',
|
||||
sizeClasses[props.size]
|
||||
])
|
||||
|
||||
const passwordStrength = computed(() => {
|
||||
if (!props.modelValue) return 0
|
||||
let strength = 0
|
||||
if (props.modelValue.length >= 8) strength++
|
||||
if (/[A-Z]/.test(props.modelValue)) strength++
|
||||
if (/[0-9]/.test(props.modelValue)) strength++
|
||||
if (/[^A-Za-z0-9]/.test(props.modelValue)) strength++
|
||||
return strength
|
||||
})
|
||||
|
||||
const strengthText = computed(() => {
|
||||
const texts = ['Very Weak', 'Weak', 'Fair', 'Good', 'Strong']
|
||||
return texts[passwordStrength.value]
|
||||
})
|
||||
|
||||
const strengthColor = computed(() => {
|
||||
const colors = ['bg-red-500', 'bg-red-400', 'bg-yellow-400', 'bg-blue-400', 'bg-green-500']
|
||||
return colors[passwordStrength.value]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="[fluid ? 'w-full' : '', props.class]">
|
||||
<div class="relative">
|
||||
<input
|
||||
:name="name"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:class="inputClasses"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
@blur="emit('blur', $event)"
|
||||
@focus="emit('focus', $event)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
<span
|
||||
:class="showPassword ? 'i-heroicons-eye-slash' : 'i-heroicons-eye'"
|
||||
class="w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="feedback && modelValue" class="mt-2">
|
||||
<div class="flex gap-1 h-1 mb-1">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
class="flex-1 rounded-full transition-colors"
|
||||
:class="i <= passwordStrength ? strengthColor : 'bg-gray-200'"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">{{ strengthText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
53
src/components/ui/ProgressBar.vue
Normal file
53
src/components/ui/ProgressBar.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value?: number
|
||||
showValue?: boolean
|
||||
unit?: string
|
||||
mode?: 'determinate' | 'indeterminate'
|
||||
color?: 'primary' | 'success' | 'warning' | 'danger'
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
value: 0,
|
||||
showValue: true,
|
||||
unit: '%',
|
||||
mode: 'determinate',
|
||||
color: 'primary'
|
||||
})
|
||||
|
||||
const normalizedValue = computed(() => {
|
||||
return Math.max(0, Math.min(100, props.value))
|
||||
})
|
||||
|
||||
const colorClasses = {
|
||||
primary: 'bg-blue-600',
|
||||
success: 'bg-green-500',
|
||||
warning: 'bg-yellow-500',
|
||||
danger: 'bg-red-500'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['w-full', props.class]">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
v-if="mode === 'determinate'"
|
||||
:class="['h-full rounded-full transition-all duration-300 ease-out', colorClasses[color]]"
|
||||
:style="{ width: `${normalizedValue}%` }"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
:class="['h-full rounded-full animate-pulse', colorClasses[color]]"
|
||||
style="width: 50%"
|
||||
/>
|
||||
</div>
|
||||
<span v-if="showValue && mode === 'determinate'" class="text-xs font-medium text-gray-600 min-w-[3rem] text-right">
|
||||
{{ normalizedValue }}{{ unit }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
27
src/components/ui/Skeleton.vue
Normal file
27
src/components/ui/Skeleton.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
width?: string
|
||||
height?: string
|
||||
borderRadius?: string
|
||||
circle?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: '100%',
|
||||
height: '1rem',
|
||||
borderRadius: '0.375rem',
|
||||
circle: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="['animate-pulse bg-gray-200', props.class]"
|
||||
:style="{
|
||||
width,
|
||||
height,
|
||||
borderRadius: circle ? '50%' : borderRadius
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
34
src/components/ui/Tag.vue
Normal file
34
src/components/ui/Tag.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
value?: string
|
||||
severity?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger'
|
||||
icon?: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
severity: 'primary'
|
||||
})
|
||||
|
||||
const severityClasses = {
|
||||
primary: 'bg-blue-100 text-blue-800 border-blue-200',
|
||||
secondary: 'bg-gray-100 text-gray-800 border-gray-200',
|
||||
success: 'bg-green-100 text-green-800 border-green-200',
|
||||
info: 'bg-cyan-100 text-cyan-800 border-cyan-200',
|
||||
warning: 'bg-yellow-100 text-yellow-800 border-yellow-200',
|
||||
danger: 'bg-red-100 text-red-800 border-red-200'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium border',
|
||||
severityClasses[severity],
|
||||
props.class
|
||||
]"
|
||||
>
|
||||
<span v-if="icon" :class="[icon, 'w-3 h-3']" />
|
||||
<slot>{{ value }}</slot>
|
||||
</span>
|
||||
</template>
|
||||
73
src/components/ui/Toast.vue
Normal file
73
src/components/ui/Toast.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast, type ToastSeverity } from '@/composables/useToast'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const severityIcons: Record<ToastSeverity, string> = {
|
||||
success: 'i-heroicons-check-circle',
|
||||
error: 'i-heroicons-x-circle',
|
||||
info: 'i-heroicons-information-circle',
|
||||
warn: 'i-heroicons-exclamation-triangle'
|
||||
}
|
||||
|
||||
const severityClasses: Record<ToastSeverity, string> = {
|
||||
success: 'bg-green-50 border-green-200 text-green-800',
|
||||
error: 'bg-red-50 border-red-200 text-red-800',
|
||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
||||
warn: 'bg-yellow-50 border-yellow-200 text-yellow-800'
|
||||
}
|
||||
|
||||
const severityIconColors: Record<ToastSeverity, string> = {
|
||||
success: 'text-green-500',
|
||||
error: 'text-red-500',
|
||||
info: 'text-blue-500',
|
||||
warn: 'text-yellow-500'
|
||||
}
|
||||
|
||||
const handleClose = (id: string) => {
|
||||
toast.remove(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="fixed top-4 right-4 z-50 flex flex-col gap-2 max-w-sm">
|
||||
<TransitionGroup
|
||||
enter-active-class="transition ease-out duration-300"
|
||||
enter-from-class="translate-x-full opacity-0"
|
||||
enter-to-class="translate-x-0 opacity-100"
|
||||
leave-active-class="transition ease-in duration-200"
|
||||
leave-from-class="translate-x-0 opacity-100"
|
||||
leave-to-class="translate-x-full opacity-0"
|
||||
>
|
||||
<div
|
||||
v-for="message in toast.messages"
|
||||
:key="message.id"
|
||||
:class="[
|
||||
'flex items-start gap-3 p-4 rounded-lg border shadow-lg min-w-[300px]',
|
||||
severityClasses[message.severity]
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
severityIcons[message.severity],
|
||||
severityIconColors[message.severity],
|
||||
'w-5 h-5 flex-shrink-0 mt-0.5'
|
||||
]"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-medium text-sm">{{ message.summary }}</p>
|
||||
<p v-if="message.detail" class="text-sm opacity-90 mt-0.5">{{ message.detail }}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-shrink-0 opacity-60 hover:opacity-100 transition-opacity"
|
||||
@click="handleClose(message.id)"
|
||||
>
|
||||
<span class="i-heroicons-x-mark w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
174
src/composables/useDataLoader.ts
Normal file
174
src/composables/useDataLoader.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
interface DataLoaderOptions<T> {
|
||||
key: string
|
||||
fetcher: () => Promise<T>
|
||||
revalidateOnFocus?: boolean
|
||||
revalidateOnReconnect?: boolean
|
||||
refreshInterval?: number
|
||||
dedupingInterval?: number
|
||||
fallbackData?: T
|
||||
}
|
||||
|
||||
interface DataLoaderState<T> {
|
||||
data: T | undefined
|
||||
error: Error | null
|
||||
isLoading: boolean
|
||||
isValidating: boolean
|
||||
}
|
||||
|
||||
// Global cache
|
||||
const cache = new Map<string, { data: any; timestamp: number }>()
|
||||
const dedupeTimers = new Map<string, number>()
|
||||
const DEDUPING_INTERVAL = 2000
|
||||
|
||||
export function useDataLoader<T>(options: DataLoaderOptions<T>) {
|
||||
const route = useRoute()
|
||||
const {
|
||||
key,
|
||||
fetcher,
|
||||
revalidateOnFocus = false,
|
||||
revalidateOnReconnect = true,
|
||||
refreshInterval,
|
||||
fallbackData
|
||||
} = options
|
||||
|
||||
const data = ref<T | undefined>(fallbackData)
|
||||
const error = ref<Error | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const isValidating = ref(false)
|
||||
|
||||
let refreshTimer: number | null = null
|
||||
let isMounted = false
|
||||
|
||||
const mutate = async (newData?: T): Promise<T | undefined> => {
|
||||
if (newData !== undefined) {
|
||||
data.value = newData
|
||||
cache.set(key, { data: newData, timestamp: Date.now() })
|
||||
return newData
|
||||
}
|
||||
|
||||
// Dedupe requests
|
||||
if (dedupeTimers.has(key)) {
|
||||
return data.value
|
||||
}
|
||||
|
||||
const dedupeKey = key
|
||||
dedupeTimers.set(dedupeKey, window.setTimeout(() => {
|
||||
dedupeTimers.delete(dedupeKey)
|
||||
}, DEDUPING_INTERVAL))
|
||||
|
||||
isValidating.value = true
|
||||
if (!data.value) {
|
||||
isLoading.value = true
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fetcher()
|
||||
data.value = result
|
||||
error.value = null
|
||||
cache.set(key, { data: result, timestamp: Date.now() })
|
||||
return result
|
||||
} catch (err) {
|
||||
error.value = err as Error
|
||||
throw err
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
isValidating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
const load = async () => {
|
||||
const cached = cache.get(key)
|
||||
if (cached && Date.now() - cached.timestamp < DEDUPING_INTERVAL) {
|
||||
data.value = cached.data
|
||||
return
|
||||
}
|
||||
await mutate()
|
||||
}
|
||||
|
||||
// Revalidate on focus
|
||||
const handleFocus = () => {
|
||||
if (revalidateOnFocus && document.visibilityState === 'visible') {
|
||||
mutate()
|
||||
}
|
||||
}
|
||||
|
||||
// Revalidate on reconnect
|
||||
const handleOnline = () => {
|
||||
if (revalidateOnReconnect) {
|
||||
mutate()
|
||||
}
|
||||
}
|
||||
|
||||
// Setup refresh interval
|
||||
const setupRefreshInterval = () => {
|
||||
if (refreshInterval && refreshInterval > 0) {
|
||||
refreshTimer = window.setInterval(() => {
|
||||
mutate()
|
||||
}, refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup refresh interval
|
||||
const cleanupRefreshInterval = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
isMounted = true
|
||||
load()
|
||||
|
||||
if (revalidateOnFocus) {
|
||||
document.addEventListener('visibilitychange', handleFocus)
|
||||
}
|
||||
|
||||
if (revalidateOnReconnect) {
|
||||
window.addEventListener('online', handleOnline)
|
||||
}
|
||||
|
||||
setupRefreshInterval()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
isMounted = false
|
||||
cleanupRefreshInterval()
|
||||
|
||||
if (revalidateOnFocus) {
|
||||
document.removeEventListener('visibilitychange', handleFocus)
|
||||
}
|
||||
|
||||
if (revalidateOnReconnect) {
|
||||
window.removeEventListener('online', handleOnline)
|
||||
}
|
||||
})
|
||||
|
||||
// Revalidate when key changes
|
||||
watch(() => key, () => {
|
||||
if (isMounted) {
|
||||
load()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
data: computed(() => data.value),
|
||||
error: computed(() => error.value),
|
||||
isLoading: computed(() => isLoading.value),
|
||||
isValidating: computed(() => isValidating.value),
|
||||
mutate
|
||||
}
|
||||
}
|
||||
|
||||
// Helper for SSR compatibility
|
||||
export const useSWRV = <T>(key: string, fetcher: () => Promise<T>) => {
|
||||
return useDataLoader<T>({
|
||||
key,
|
||||
fetcher,
|
||||
revalidateOnFocus: false
|
||||
})
|
||||
}
|
||||
90
src/composables/useToast.ts
Normal file
90
src/composables/useToast.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export type ToastSeverity = 'success' | 'error' | 'info' | 'warn'
|
||||
|
||||
export interface ToastMessage {
|
||||
id: string
|
||||
severity: ToastSeverity
|
||||
summary: string
|
||||
detail?: string
|
||||
life?: number
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
messages: ToastMessage[]
|
||||
}
|
||||
|
||||
const state = reactive<ToastState>({
|
||||
messages: []
|
||||
})
|
||||
|
||||
let toastIdCounter = 0
|
||||
|
||||
export const useToast = () => {
|
||||
const add = (message: Omit<ToastMessage, 'id'>) => {
|
||||
const id = `toast-${++toastIdCounter}`
|
||||
const newMessage: ToastMessage = {
|
||||
id,
|
||||
life: 3000,
|
||||
...message
|
||||
}
|
||||
|
||||
state.messages.push(newMessage)
|
||||
|
||||
if (newMessage.life && newMessage.life > 0) {
|
||||
setTimeout(() => {
|
||||
remove(id)
|
||||
}, newMessage.life)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
const remove = (id: string) => {
|
||||
const index = state.messages.findIndex(m => m.id === id)
|
||||
if (index > -1) {
|
||||
state.messages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
state.messages.length = 0
|
||||
}
|
||||
|
||||
const success = (detail: string, summary: string = 'Success') => {
|
||||
return add({ severity: 'success', summary, detail })
|
||||
}
|
||||
|
||||
const error = (detail: string, summary: string = 'Error') => {
|
||||
return add({ severity: 'error', summary, detail })
|
||||
}
|
||||
|
||||
const info = (detail: string, summary: string = 'Info') => {
|
||||
return add({ severity: 'info', summary, detail })
|
||||
}
|
||||
|
||||
const warn = (detail: string, summary: string = 'Warning') => {
|
||||
return add({ severity: 'warn', summary, detail })
|
||||
}
|
||||
|
||||
return {
|
||||
messages: state.messages,
|
||||
add,
|
||||
remove,
|
||||
clear,
|
||||
success,
|
||||
error,
|
||||
info,
|
||||
warn
|
||||
}
|
||||
}
|
||||
|
||||
// Global toast instance for use outside of components
|
||||
let globalToastInstance: ReturnType<typeof useToast> | null = null
|
||||
|
||||
export const getGlobalToast = () => {
|
||||
if (!globalToastInstance) {
|
||||
globalToastInstance = useToast()
|
||||
}
|
||||
return globalToastInstance
|
||||
}
|
||||
@@ -6,15 +6,12 @@ import { streamText } from 'hono/streaming';
|
||||
import isMobile from 'is-mobile';
|
||||
import { renderToWebStream } from 'vue/server-renderer';
|
||||
import { buildBootstrapScript } from './lib/manifest';
|
||||
import { styleTags } from './lib/primePassthrough';
|
||||
import { createTextTransformStreamClass } from './lib/replateStreamText';
|
||||
import { createApp } from './main';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
// @ts-ignore
|
||||
import Base from '@primevue/core/base';
|
||||
import { createTextTransformStreamClass } from './lib/replateStreamText';
|
||||
|
||||
const app = new Hono()
|
||||
const defaultNames = ['primitive', 'semantic', 'global', 'base', 'ripple-directive']
|
||||
// app.use(renderer)
|
||||
|
||||
app.use('*', contextStorage());
|
||||
app.use(cors(), async (c, next) => {
|
||||
c.set("fetch", app.request.bind(app));
|
||||
@@ -35,8 +32,7 @@ app.use(cors(), async (c, next) => {
|
||||
url.protocol = 'https:'
|
||||
url.pathname = path.replace(/^\/r/, '') || '/'
|
||||
url.port = ''
|
||||
// console.log("url", url.toString())
|
||||
// console.log("c.req.raw", c.req.raw)
|
||||
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
@@ -50,9 +46,11 @@ app.use(cors(), async (c, next) => {
|
||||
credentials: 'include'
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/.well-known/*", (c) => {
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get("*", async (c) => {
|
||||
const nonce = crypto.randomUUID();
|
||||
const url = new URL(c.req.url);
|
||||
@@ -60,33 +58,28 @@ app.get("*", async (c) => {
|
||||
app.provide("honoContext", c);
|
||||
const auth = useAuthStore();
|
||||
auth.$reset();
|
||||
// auth.initialized = false;
|
||||
await auth.init();
|
||||
await router.push(url.pathname);
|
||||
await router.isReady();
|
||||
let usedStyles = new Set<String>();
|
||||
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name)
|
||||
|
||||
return streamText(c, async (stream) => {
|
||||
c.header("Content-Type", "text/html; charset=utf-8");
|
||||
c.header("Content-Encoding", "Identity");
|
||||
const ctx: Record<string, any> = {};
|
||||
const appStream = renderToWebStream(app, ctx);
|
||||
// console.log("ctx: ", );
|
||||
|
||||
await stream.write("<!DOCTYPE html><html lang='en'><head>");
|
||||
await stream.write("<base href='" + url.origin + "'/>");
|
||||
|
||||
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, "")));
|
||||
// await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
|
||||
await stream.write(`<link rel="preconnect" href="https://fonts.googleapis.com">`);
|
||||
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Google+Sans:ital,opsz,wght@0,17..18,400..700;1,17..18,400..700&display=swap" rel="stylesheet">`);
|
||||
await stream.write('<link rel="icon" href="/favicon.ico" />');
|
||||
await stream.write(buildBootstrapScript());
|
||||
if (usedStyles.size > 0) {
|
||||
defaultNames.forEach(name => usedStyles.add(name));
|
||||
}
|
||||
await Promise.all(styleTags.filter(tag => usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))).map(tag => stream.write(`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`)));
|
||||
|
||||
await stream.write(`</head><body class='${bodyClass}'>`);
|
||||
await stream.pipe(createTextTransformStreamClass(appStream, (text) => text.replace('<div id="anchor-header" class="p-4"></div>', `<div id="anchor-header" class="p-4">${ctx.teleports["#anchor-header"] || ""}</div>`).replace('<div id="anchor-top"></div>', `<div id="anchor-top">${ctx.teleports["#anchor-top"] || ""}</div>`)));
|
||||
|
||||
delete ctx.teleports
|
||||
delete ctx.__teleportBuffers
|
||||
delete ctx.modules;
|
||||
@@ -95,6 +88,7 @@ app.get("*", async (c) => {
|
||||
await stream.write("</body></html>");
|
||||
});
|
||||
})
|
||||
|
||||
const ESCAPE_LOOKUP: { [match: string]: string } = {
|
||||
"&": "\\u0026",
|
||||
">": "\\u003e",
|
||||
@@ -108,4 +102,5 @@ const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
|
||||
function htmlEscape(str: string): string {
|
||||
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
|
||||
}
|
||||
|
||||
export default app
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
77
src/lib/swr/cache/adapters/localStorage.ts
vendored
77
src/lib/swr/cache/adapters/localStorage.ts
vendored
@@ -1,77 +0,0 @@
|
||||
import SWRVCache, { type ICacheItem } from '..'
|
||||
import type { IKey } from '../../types'
|
||||
|
||||
/**
|
||||
* LocalStorage cache adapter for swrv data cache.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
|
||||
*/
|
||||
export default class LocalStorageCache extends SWRVCache<any> {
|
||||
private STORAGE_KEY
|
||||
|
||||
constructor (key = 'swrv', ttl = 0) {
|
||||
super(ttl)
|
||||
this.STORAGE_KEY = key
|
||||
}
|
||||
|
||||
private encode (storage: any) { return JSON.stringify(storage) }
|
||||
private decode (storage: any) { return JSON.parse(storage) }
|
||||
|
||||
get (k: IKey): ICacheItem<IKey> {
|
||||
const item = localStorage.getItem(this.STORAGE_KEY)
|
||||
if (item) {
|
||||
const _key = this.serializeKey(k)
|
||||
const itemParsed: ICacheItem<any> = JSON.parse(item)[_key]
|
||||
|
||||
if (itemParsed?.expiresAt === null) {
|
||||
itemParsed.expiresAt = Infinity // localStorage sets Infinity to 'null'
|
||||
}
|
||||
|
||||
return itemParsed
|
||||
}
|
||||
|
||||
return undefined as any
|
||||
}
|
||||
|
||||
set (k: string, v: any, ttl: number) {
|
||||
let payload = {}
|
||||
const _key = this.serializeKey(k)
|
||||
const timeToLive = ttl || this.ttl
|
||||
const storage = localStorage.getItem(this.STORAGE_KEY)
|
||||
const now = Date.now()
|
||||
const item = {
|
||||
data: v,
|
||||
createdAt: now,
|
||||
expiresAt: timeToLive ? now + timeToLive : Infinity
|
||||
}
|
||||
|
||||
if (storage) {
|
||||
payload = this.decode(storage)
|
||||
(payload as any)[_key] = item
|
||||
} else {
|
||||
payload = { [_key]: item }
|
||||
}
|
||||
|
||||
this.dispatchExpire(timeToLive, item, _key)
|
||||
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
|
||||
}
|
||||
|
||||
dispatchExpire (ttl: number, item: any, serializedKey: string) {
|
||||
ttl && setTimeout(() => {
|
||||
const current = Date.now()
|
||||
const hasExpired = current >= item.expiresAt
|
||||
if (hasExpired) this.delete(serializedKey)
|
||||
}, ttl)
|
||||
}
|
||||
|
||||
delete (serializedKey: string) {
|
||||
const storage = localStorage.getItem(this.STORAGE_KEY)
|
||||
let payload = {} as Record<string, any>
|
||||
|
||||
if (storage) {
|
||||
payload = this.decode(storage)
|
||||
delete payload[serializedKey]
|
||||
}
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, this.encode(payload))
|
||||
}
|
||||
}
|
||||
72
src/lib/swr/cache/index.ts
vendored
72
src/lib/swr/cache/index.ts
vendored
@@ -1,72 +0,0 @@
|
||||
import type { IKey } from '../types'
|
||||
import hash from '../lib/hash'
|
||||
export interface ICacheItem<Data> {
|
||||
data: Data,
|
||||
createdAt: number,
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
function serializeKeyDefault (key: IKey): string {
|
||||
if (typeof key === 'function') {
|
||||
try {
|
||||
key = key()
|
||||
} catch (err) {
|
||||
// dependencies not ready
|
||||
key = ''
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
key = hash(key)
|
||||
} else {
|
||||
// convert null to ''
|
||||
key = String(key || '')
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
export default class SWRVCache<CacheData> {
|
||||
protected ttl: number
|
||||
private items?: Map<string, ICacheItem<CacheData>>
|
||||
|
||||
constructor (ttl = 0) {
|
||||
this.items = new Map()
|
||||
this.ttl = ttl
|
||||
}
|
||||
|
||||
serializeKey (key: IKey): string {
|
||||
return serializeKeyDefault(key)
|
||||
}
|
||||
|
||||
get (k: string): ICacheItem<CacheData> {
|
||||
const _key = this.serializeKey(k)
|
||||
return this.items!.get(_key)!
|
||||
}
|
||||
|
||||
set (k: string, v: any, ttl: number) {
|
||||
const _key = this.serializeKey(k)
|
||||
const timeToLive = ttl || this.ttl
|
||||
const now = Date.now()
|
||||
const item = {
|
||||
data: v,
|
||||
createdAt: now,
|
||||
expiresAt: timeToLive ? now + timeToLive : Infinity
|
||||
}
|
||||
|
||||
this.dispatchExpire(timeToLive, item, _key)
|
||||
this.items!.set(_key, item)
|
||||
}
|
||||
|
||||
dispatchExpire (ttl: number, item: any, serializedKey: string) {
|
||||
ttl && setTimeout(() => {
|
||||
const current = Date.now()
|
||||
const hasExpired = current >= item.expiresAt
|
||||
if (hasExpired) this.delete(serializedKey)
|
||||
}, ttl)
|
||||
}
|
||||
|
||||
delete (serializedKey: string) {
|
||||
this.items!.delete(serializedKey)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import SWRVCache from './cache'
|
||||
import useSWRV, { mutate } from './use-swrv'
|
||||
|
||||
export {
|
||||
type IConfig
|
||||
} from './types'
|
||||
export { mutate, SWRVCache }
|
||||
export default useSWRV
|
||||
@@ -1,44 +0,0 @@
|
||||
// From https://github.com/vercel/swr/blob/master/src/libs/hash.ts
|
||||
// use WeakMap to store the object->key mapping
|
||||
// so the objects can be garbage collected.
|
||||
// WeakMap uses a hashtable under the hood, so the lookup
|
||||
// complexity is almost O(1).
|
||||
const table = new WeakMap()
|
||||
|
||||
// counter of the key
|
||||
let counter = 0
|
||||
|
||||
// hashes an array of objects and returns a string
|
||||
export default function hash (args: any[]): string {
|
||||
if (!args.length) return ''
|
||||
let key = 'arg'
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
let _hash
|
||||
if (
|
||||
args[i] === null ||
|
||||
(typeof args[i] !== 'object' && typeof args[i] !== 'function')
|
||||
) {
|
||||
// need to consider the case that args[i] is a string:
|
||||
// args[i] _hash
|
||||
// "undefined" -> '"undefined"'
|
||||
// undefined -> 'undefined'
|
||||
// 123 -> '123'
|
||||
// null -> 'null'
|
||||
// "null" -> '"null"'
|
||||
if (typeof args[i] === 'string') {
|
||||
_hash = '"' + args[i] + '"'
|
||||
} else {
|
||||
_hash = String(args[i])
|
||||
}
|
||||
} else {
|
||||
if (!table.has(args[i])) {
|
||||
_hash = counter
|
||||
table.set(args[i], counter++)
|
||||
} else {
|
||||
_hash = table.get(args[i])
|
||||
}
|
||||
}
|
||||
key += '@' + _hash
|
||||
}
|
||||
return key
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
function isOnline (): boolean {
|
||||
if (typeof navigator.onLine !== 'undefined') {
|
||||
return navigator.onLine
|
||||
}
|
||||
// always assume it's online
|
||||
return true
|
||||
}
|
||||
|
||||
function isDocumentVisible (): boolean {
|
||||
if (
|
||||
typeof document !== 'undefined' &&
|
||||
typeof document.visibilityState !== 'undefined'
|
||||
) {
|
||||
return document.visibilityState !== 'hidden'
|
||||
}
|
||||
// always assume it's visible
|
||||
return true
|
||||
}
|
||||
|
||||
const fetcher = (url: string | Request) => fetch(url).then(res => res.json())
|
||||
|
||||
export default {
|
||||
isOnline,
|
||||
isDocumentVisible,
|
||||
fetcher
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Ref, WatchSource } from 'vue'
|
||||
import SWRVCache from './cache'
|
||||
import LocalStorageCache from './cache/adapters/localStorage'
|
||||
|
||||
export type fetcherFn<Data> = (...args: any) => Data | Promise<Data>
|
||||
|
||||
export interface IConfig<
|
||||
Data = any,
|
||||
Fn extends fetcherFn<Data> = fetcherFn<Data>
|
||||
> {
|
||||
refreshInterval?: number
|
||||
cache?: LocalStorageCache | SWRVCache<any>
|
||||
dedupingInterval?: number
|
||||
ttl?: number
|
||||
serverTTL?: number
|
||||
revalidateOnFocus?: boolean
|
||||
revalidateDebounce?: number
|
||||
shouldRetryOnError?: boolean
|
||||
errorRetryInterval?: number
|
||||
errorRetryCount?: number
|
||||
fetcher?: Fn,
|
||||
isOnline?: () => boolean
|
||||
isDocumentVisible?: () => boolean
|
||||
}
|
||||
|
||||
export interface revalidateOptions {
|
||||
shouldRetryOnError?: boolean,
|
||||
errorRetryCount?: number,
|
||||
forceRevalidate?: boolean,
|
||||
}
|
||||
|
||||
export interface IResponse<Data = any, Error = any> {
|
||||
data: Ref<Data | undefined>
|
||||
error: Ref<Error | undefined>
|
||||
isValidating: Ref<boolean>
|
||||
isLoading: Ref<boolean>
|
||||
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => Promise<void>
|
||||
}
|
||||
|
||||
export type keyType = string | any[] | null | undefined
|
||||
|
||||
export type IKey = keyType | WatchSource<keyType>
|
||||
@@ -1,470 +0,0 @@
|
||||
/** ____
|
||||
*--------------/ \.------------------/
|
||||
* / swrv \. / //
|
||||
* / / /\. / //
|
||||
* / _____/ / \. /
|
||||
* / / ____/ . \. /
|
||||
* / \ \_____ \. /
|
||||
* / . \_____ \ \ / //
|
||||
* \ _____/ / ./ / //
|
||||
* \ / _____/ ./ /
|
||||
* \ / / . ./ /
|
||||
* \ / / ./ /
|
||||
* . \/ ./ / //
|
||||
* \ ./ / //
|
||||
* \.. / /
|
||||
* . ||| /
|
||||
* ||| /
|
||||
* . ||| / //
|
||||
* ||| / //
|
||||
* ||| /
|
||||
*/
|
||||
import { tinyassert } from "@hiogawa/utils";
|
||||
import {
|
||||
getCurrentInstance,
|
||||
inject,
|
||||
isReadonly,
|
||||
isRef,
|
||||
// isRef,
|
||||
onMounted,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRefs,
|
||||
useSSRContext,
|
||||
watch,
|
||||
type FunctionPlugin
|
||||
} from 'vue';
|
||||
import SWRVCache from './cache';
|
||||
import webPreset from './lib/web-preset';
|
||||
import type { IConfig, IKey, IResponse, fetcherFn, revalidateOptions } from './types';
|
||||
|
||||
type StateRef<Data, Error> = {
|
||||
data: Data, error: Error, isValidating: boolean, isLoading: boolean, revalidate: Function, key: any
|
||||
};
|
||||
|
||||
const DATA_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
|
||||
const REF_CACHE = new SWRVCache<StateRef<any, any>[]>()
|
||||
const PROMISES_CACHE = new SWRVCache<Omit<IResponse, 'mutate'>>()
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
cache: DATA_CACHE,
|
||||
refreshInterval: 0,
|
||||
ttl: 0,
|
||||
serverTTL: 1000,
|
||||
dedupingInterval: 2000,
|
||||
revalidateOnFocus: true,
|
||||
revalidateDebounce: 0,
|
||||
shouldRetryOnError: true,
|
||||
errorRetryInterval: 5000,
|
||||
errorRetryCount: 5,
|
||||
fetcher: webPreset.fetcher,
|
||||
isOnline: webPreset.isOnline,
|
||||
isDocumentVisible: webPreset.isDocumentVisible
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the refs for later revalidation
|
||||
*/
|
||||
function setRefCache(key: string, theRef: StateRef<any, any>, ttl: number) {
|
||||
const refCacheItem = REF_CACHE.get(key)
|
||||
if (refCacheItem) {
|
||||
refCacheItem.data.push(theRef)
|
||||
} else {
|
||||
// #51 ensures ref cache does not evict too soon
|
||||
const gracePeriod = 5000
|
||||
REF_CACHE.set(key, [theRef], ttl > 0 ? ttl + gracePeriod : ttl)
|
||||
}
|
||||
}
|
||||
|
||||
function onErrorRetry(revalidate: (any: any, opts: revalidateOptions) => void, errorRetryCount: number, config: IConfig): void {
|
||||
if (!(config as any).isDocumentVisible()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.errorRetryCount !== undefined && errorRetryCount > config.errorRetryCount) {
|
||||
return
|
||||
}
|
||||
|
||||
const count = Math.min(errorRetryCount || 0, (config as any).errorRetryCount)
|
||||
const timeout = count * (config as any).errorRetryInterval
|
||||
setTimeout(() => {
|
||||
revalidate(null, { errorRetryCount: count + 1, shouldRetryOnError: true })
|
||||
}, timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Main mutation function for receiving data from promises to change state and
|
||||
* set data cache
|
||||
*/
|
||||
const mutate = async <Data>(key: string, res: Promise<Data> | Data, cache = DATA_CACHE, ttl = defaultConfig.ttl) => {
|
||||
let data, error, isValidating
|
||||
|
||||
if (isPromise(res)) {
|
||||
try {
|
||||
data = await res
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
} else {
|
||||
data = res
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
isValidating = false
|
||||
|
||||
const newData = { data, error, isValidating }
|
||||
if (typeof data !== 'undefined') {
|
||||
try {
|
||||
cache.set(key, newData, Number(ttl))
|
||||
} catch (err) {
|
||||
console.error('swrv(mutate): failed to set cache', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate all swrv instances with new data
|
||||
*/
|
||||
const stateRef = REF_CACHE.get(key)
|
||||
if (stateRef && stateRef.data.length) {
|
||||
// This filter fixes #24 race conditions to only update ref data of current
|
||||
// key, while data cache will continue to be updated if revalidation is
|
||||
// fired
|
||||
let refs = stateRef.data.filter(r => r.key === key)
|
||||
|
||||
refs.forEach((r, idx) => {
|
||||
if (typeof newData.data !== 'undefined') {
|
||||
r.data = newData.data
|
||||
}
|
||||
r.error = newData.error
|
||||
r.isValidating = newData.isValidating
|
||||
r.isLoading = newData.isValidating
|
||||
|
||||
const isLast = idx === refs.length - 1
|
||||
if (!isLast) {
|
||||
// Clean up refs that belonged to old keys
|
||||
delete refs[idx]
|
||||
}
|
||||
})
|
||||
|
||||
refs = refs.filter(Boolean)
|
||||
}
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
/* Stale-While-Revalidate hook to handle fetching, caching, validation, and more... */
|
||||
function useSWRV<Data = any, Error = any>(
|
||||
key: IKey
|
||||
): IResponse<Data, Error>
|
||||
function useSWRV<Data = any, Error = any>(
|
||||
key: IKey,
|
||||
fn: fetcherFn<Data> | undefined | null,
|
||||
config?: IConfig
|
||||
): IResponse<Data, Error>
|
||||
function useSWRV<Data = any, Error = any>(...args: any[]): IResponse<Data, Error> {
|
||||
const injectedConfig = inject<Partial<IConfig> | null>('swrv-config', null)
|
||||
tinyassert(injectedConfig, 'Injected swrv-config must be an object')
|
||||
let key: IKey
|
||||
let fn: fetcherFn<Data> | undefined | null
|
||||
let config: IConfig = { ...defaultConfig, ...injectedConfig }
|
||||
let unmounted = false
|
||||
let isHydrated = false
|
||||
|
||||
const instance = getCurrentInstance() as any
|
||||
const vm = instance?.proxy || instance // https://github.com/vuejs/composition-api/pull/520
|
||||
if (!vm) {
|
||||
console.error('Could not get current instance, check to make sure that `useSwrv` is declared in the top level of the setup function.')
|
||||
throw new Error('Could not get current instance')
|
||||
}
|
||||
|
||||
const IS_SERVER = typeof window === 'undefined' || false
|
||||
// #region ssr
|
||||
const isSsrHydration = Boolean(
|
||||
!IS_SERVER &&
|
||||
window !== undefined && (window as any).window.swrv)
|
||||
// #endregion
|
||||
if (args.length >= 1) {
|
||||
key = args[0]
|
||||
}
|
||||
if (args.length >= 2) {
|
||||
fn = args[1]
|
||||
}
|
||||
if (args.length > 2) {
|
||||
config = {
|
||||
...config,
|
||||
...args[2]
|
||||
}
|
||||
}
|
||||
|
||||
const ttl = IS_SERVER ? config.serverTTL : config.ttl
|
||||
const keyRef = typeof key === 'function' ? (key as any) : ref(key)
|
||||
|
||||
if (typeof fn === 'undefined') {
|
||||
// use the global fetcher
|
||||
fn = config.fetcher
|
||||
}
|
||||
|
||||
let stateRef: StateRef<Data, Error> | null = null
|
||||
// #region ssr
|
||||
if (isSsrHydration) {
|
||||
// component was ssrHydrated, so make the ssr reactive as the initial data
|
||||
|
||||
const swrvState = (window as any).window.swrv || []
|
||||
const swrvKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
|
||||
if (swrvKey !== undefined && swrvKey !== null) {
|
||||
const nodeState = swrvState[swrvKey] || []
|
||||
const instanceState = nodeState[nanoHex(isRef(keyRef) ? keyRef.value : keyRef())]
|
||||
|
||||
if (instanceState) {
|
||||
stateRef = reactive(instanceState)
|
||||
isHydrated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
|
||||
if (!stateRef) {
|
||||
stateRef = reactive({
|
||||
data: undefined,
|
||||
error: undefined,
|
||||
isValidating: true,
|
||||
isLoading: true,
|
||||
key: null
|
||||
}) as StateRef<Data, Error>
|
||||
}
|
||||
|
||||
/**
|
||||
* Revalidate the cache, mutate data
|
||||
*/
|
||||
const revalidate = async (data?: fetcherFn<Data>, opts?: revalidateOptions) => {
|
||||
const isFirstFetch = stateRef.data === undefined
|
||||
const keyVal = keyRef.value
|
||||
if (!keyVal) { return }
|
||||
|
||||
const cacheItem = config.cache!.get(keyVal)
|
||||
const newData = cacheItem && cacheItem.data
|
||||
|
||||
stateRef.isValidating = true
|
||||
stateRef.isLoading = !newData
|
||||
if (newData) {
|
||||
stateRef.data = newData.data
|
||||
stateRef.error = newData.error
|
||||
}
|
||||
|
||||
const fetcher = data || fn
|
||||
if (
|
||||
!fetcher ||
|
||||
(!IS_SERVER && !(config as any).isDocumentVisible() && !isFirstFetch) ||
|
||||
(opts?.forceRevalidate !== undefined && !opts?.forceRevalidate)
|
||||
) {
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
// Dedupe items that were created in the last interval #76
|
||||
if (cacheItem) {
|
||||
const shouldRevalidate = Boolean(
|
||||
((Date.now() - cacheItem.createdAt) >= (config as any).dedupingInterval) || opts?.forceRevalidate
|
||||
)
|
||||
|
||||
if (!shouldRevalidate) {
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const trigger = async () => {
|
||||
const promiseFromCache = PROMISES_CACHE.get(keyVal)
|
||||
if (!promiseFromCache) {
|
||||
const fetcherArgs = Array.isArray(keyVal) ? keyVal : [keyVal]
|
||||
const newPromise = fetcher(...fetcherArgs)
|
||||
PROMISES_CACHE.set(keyVal, newPromise, (config as any).dedupingInterval)
|
||||
await mutate(keyVal, newPromise, (config as any).cache, ttl)
|
||||
} else {
|
||||
await mutate(keyVal, promiseFromCache.data, (config as any).cache, ttl)
|
||||
}
|
||||
stateRef.isValidating = false
|
||||
stateRef.isLoading = false
|
||||
PROMISES_CACHE.delete(keyVal)
|
||||
if (stateRef.error !== undefined) {
|
||||
const shouldRetryOnError = !unmounted && config.shouldRetryOnError && (opts ? opts.shouldRetryOnError : true)
|
||||
if (shouldRetryOnError) {
|
||||
onErrorRetry(revalidate, opts ? Number(opts.errorRetryCount) : 1, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newData && config.revalidateDebounce) {
|
||||
setTimeout(async () => {
|
||||
if (!unmounted) {
|
||||
await trigger()
|
||||
}
|
||||
}, config.revalidateDebounce)
|
||||
} else {
|
||||
await trigger()
|
||||
}
|
||||
}
|
||||
|
||||
const revalidateCall = async () => revalidate(null as any, { shouldRetryOnError: false })
|
||||
let timer: any = null
|
||||
/**
|
||||
* Setup polling
|
||||
*/
|
||||
onMounted(() => {
|
||||
const tick = async () => {
|
||||
// component might un-mount during revalidate, so do not set a new timeout
|
||||
// if this is the case, but continue to revalidate since promises can't
|
||||
// be cancelled and new hook instances might rely on promise/data cache or
|
||||
// from pre-fetch
|
||||
if (!stateRef.error && (config as any).isOnline()) {
|
||||
// if API request errored, we stop polling in this round
|
||||
// and let the error retry function handle it
|
||||
await revalidate()
|
||||
} else {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
if (config.refreshInterval && !unmounted) {
|
||||
timer = setTimeout(tick, config.refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
if (config.refreshInterval) {
|
||||
timer = setTimeout(tick, config.refreshInterval)
|
||||
}
|
||||
if (config.revalidateOnFocus) {
|
||||
document.addEventListener('visibilitychange', revalidateCall, false)
|
||||
window.addEventListener('focus', revalidateCall, false)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Teardown
|
||||
*/
|
||||
onUnmounted(() => {
|
||||
unmounted = true
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
if (config.revalidateOnFocus) {
|
||||
document.removeEventListener('visibilitychange', revalidateCall, false)
|
||||
window.removeEventListener('focus', revalidateCall, false)
|
||||
}
|
||||
const refCacheItem = REF_CACHE.get(keyRef.value)
|
||||
if (refCacheItem) {
|
||||
refCacheItem.data = refCacheItem.data.filter((ref) => ref !== stateRef)
|
||||
}
|
||||
})
|
||||
|
||||
// #region ssr
|
||||
if (IS_SERVER) {
|
||||
const ssrContext = useSSRContext()
|
||||
// make sure srwv exists in ssrContext
|
||||
let swrvRes: Record<string, any> = {}
|
||||
if (ssrContext) {
|
||||
swrvRes = ssrContext.swrv = ssrContext.swrv || swrvRes
|
||||
}
|
||||
|
||||
const ssrKey = nanoHex(vm.$.type.__name ?? vm.$.type.name)
|
||||
// if (!vm.$vnode || (vm.$node && !vm.$node.data)) {
|
||||
// vm.$vnode = {
|
||||
// data: { attrs: { 'data-swrv-key': ssrKey } }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const attrs = (vm.$vnode.data.attrs = vm.$vnode.data.attrs || {})
|
||||
// attrs['data-swrv-key'] = ssrKey
|
||||
// // Nuxt compatibility
|
||||
// if (vm.$ssrContext && vm.$ssrContext.nuxt) {
|
||||
// vm.$ssrContext.nuxt.swrv = swrvRes
|
||||
// }
|
||||
if (ssrContext) {
|
||||
ssrContext.swrv = swrvRes
|
||||
}
|
||||
onServerPrefetch(async () => {
|
||||
await revalidate()
|
||||
if (!swrvRes[ssrKey]) swrvRes[ssrKey] = {}
|
||||
|
||||
swrvRes[ssrKey][nanoHex(keyRef.value)] = {
|
||||
data: stateRef.data,
|
||||
error: stateRef.error,
|
||||
isValidating: stateRef.isValidating
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endregion
|
||||
|
||||
/**
|
||||
* Revalidate when key dependencies change
|
||||
*/
|
||||
try {
|
||||
watch(keyRef, (val) => {
|
||||
if (!isReadonly(keyRef)) {
|
||||
keyRef.value = val
|
||||
}
|
||||
stateRef.key = val
|
||||
stateRef.isValidating = Boolean(val)
|
||||
setRefCache(keyRef.value, stateRef, Number(ttl))
|
||||
|
||||
if (!IS_SERVER && !isHydrated && keyRef.value) {
|
||||
revalidate()
|
||||
}
|
||||
isHydrated = false
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const res: IResponse = {
|
||||
...toRefs(stateRef),
|
||||
mutate: (data?: fetcherFn<Data>, opts?: revalidateOptions) => revalidate(data, {
|
||||
...opts,
|
||||
forceRevalidate: true
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function isPromise<T>(p: any): p is Promise<T> {
|
||||
return p !== null && typeof p === 'object' && typeof p.then === 'function'
|
||||
}
|
||||
|
||||
/**
|
||||
* string to hex 8 chars
|
||||
* @param name string
|
||||
* @returns string
|
||||
*/
|
||||
function nanoHex(name: string): string {
|
||||
try {
|
||||
let hash = 0
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
const chr = name.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + chr
|
||||
hash |= 0 // Convert to 32bit integer
|
||||
}
|
||||
let hex = (hash >>> 0).toString(16)
|
||||
while (hex.length < 8) {
|
||||
hex = '0' + hex
|
||||
}
|
||||
return hex
|
||||
} catch {
|
||||
console.error("err name: ", name)
|
||||
return '0000'
|
||||
}
|
||||
}
|
||||
export const vueSWR = (swrvConfig: Partial<IConfig> = defaultConfig): FunctionPlugin => (app) => {
|
||||
app.config.globalProperties.$swrv = useSWRV
|
||||
// app.provide('swrv', useSWRV)
|
||||
app.provide('swrv-config', swrvConfig)
|
||||
}
|
||||
export { mutate };
|
||||
export default useSWRV
|
||||
35
src/main.ts
35
src/main.ts
@@ -1,44 +1,28 @@
|
||||
import { createHead as CSRHead } from "@unhead/vue/client";
|
||||
import { createHead as SSRHead } from "@unhead/vue/server";
|
||||
import { createPinia } from "pinia";
|
||||
import { createSSRApp } from 'vue';
|
||||
import { RouterView } from 'vue-router';
|
||||
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
||||
import { vueSWR } from './lib/swr/use-swrv';
|
||||
import createAppRouter from './routes';
|
||||
import PrimeVue from 'primevue/config';
|
||||
import Aura from '@primeuix/themes/aura';
|
||||
import { createPinia } from "pinia";
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import ToastService from 'primevue/toastservice';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
|
||||
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen"
|
||||
|
||||
export function createApp() {
|
||||
const pinia = createPinia();
|
||||
const app = createSSRApp(withErrorBoundary(RouterView));
|
||||
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
|
||||
|
||||
app.use(head);
|
||||
app.use(PrimeVue, {
|
||||
// unstyled: true,
|
||||
theme: {
|
||||
preset: Aura,
|
||||
options: {
|
||||
darkModeSelector: '.my-app-dark',
|
||||
cssLayer: false,
|
||||
// cssLayer: {
|
||||
// name: 'primevue',
|
||||
// order: 'theme, base, primevue'
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
app.use(ToastService);
|
||||
|
||||
// Directive để skip hydration cho các phần tử không cần thiết
|
||||
app.directive('nh', {
|
||||
created(el) {
|
||||
el.__v_skip = true;
|
||||
}
|
||||
});
|
||||
app.directive("tooltip", Tooltip)
|
||||
|
||||
// Restore state từ SSR
|
||||
if (!import.meta.env.SSR) {
|
||||
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
|
||||
(window as any)[key] = value;
|
||||
@@ -47,10 +31,11 @@ export function createApp() {
|
||||
pinia.state.value = (window as any).$p;
|
||||
}
|
||||
}
|
||||
|
||||
app.use(pinia);
|
||||
app.use(vueSWR({ revalidateOnFocus: false }));
|
||||
|
||||
const router = createAppRouter();
|
||||
app.use(router);
|
||||
|
||||
return { app, router, head, pinia, bodyClass };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import StatsCard from '@/components/dashboard/StatsCard.vue';
|
||||
import { client, type ModelVideo } from '@/api/client';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
const recentVideos = ref<ModelVideo[]>([]);
|
||||
|
||||
// Mock stats data (in real app, fetch from API)
|
||||
const stats = ref({
|
||||
totalVideos: 0,
|
||||
totalViews: 0,
|
||||
storageUsed: 0,
|
||||
storageLimit: 10737418240, // 10GB in bytes
|
||||
uploadsThisMonth: 0
|
||||
});
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
title: 'Upload Video',
|
||||
description: 'Upload a new video to your library',
|
||||
icon: 'i-heroicons-cloud-arrow-up',
|
||||
color: 'bg-gradient-to-br from-primary/20 to-primary/5',
|
||||
iconColor: 'text-primary',
|
||||
onClick: () => router.push('/upload')
|
||||
},
|
||||
{
|
||||
title: 'Video Library',
|
||||
description: 'Browse all your videos',
|
||||
icon: 'i-heroicons-film',
|
||||
color: 'bg-gradient-to-br from-blue-100 to-blue-50',
|
||||
iconColor: 'text-blue-600',
|
||||
onClick: () => router.push('/video')
|
||||
},
|
||||
{
|
||||
title: 'Analytics',
|
||||
description: 'Track performance & insights',
|
||||
icon: 'i-heroicons-chart-bar',
|
||||
color: 'bg-gradient-to-br from-purple-100 to-purple-50',
|
||||
iconColor: 'text-purple-600',
|
||||
onClick: () => {}
|
||||
},
|
||||
{
|
||||
title: 'Manage Plan',
|
||||
description: 'Upgrade or change your plan',
|
||||
icon: 'i-heroicons-credit-card',
|
||||
color: 'bg-gradient-to-br from-orange-100 to-orange-50',
|
||||
iconColor: 'text-orange-600',
|
||||
onClick: () => router.push('/plans')
|
||||
},
|
||||
];
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Fetch recent videos
|
||||
const response = await client.videos.videosList({ page: 1, limit: 5 });
|
||||
const body = response.data as any;
|
||||
|
||||
if (body.data && Array.isArray(body.data)) {
|
||||
recentVideos.value = body.data;
|
||||
stats.value.totalVideos = body.data.length;
|
||||
} else if (Array.isArray(body)) {
|
||||
recentVideos.value = body;
|
||||
stats.value.totalVideos = body.length;
|
||||
}
|
||||
|
||||
// Calculate mock stats
|
||||
stats.value.totalViews = recentVideos.value.reduce((sum, v: any) => sum + (v.views || 0), 0);
|
||||
stats.value.storageUsed = recentVideos.value.reduce((sum, v) => sum + (v.size || 0), 0);
|
||||
stats.value.uploadsThisMonth = recentVideos.value.filter(v => {
|
||||
const uploadDate = new Date(v.created_at || '');
|
||||
const now = new Date();
|
||||
return uploadDate.getMonth() === now.getMonth() && uploadDate.getFullYear() === now.getFullYear();
|
||||
}).length;
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch dashboard data:', err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatDuration = (seconds?: number) => {
|
||||
if (!seconds) return '0:00';
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string) => {
|
||||
if (!dateString) return '';
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusClass = (status?: string) => {
|
||||
switch(status?.toLowerCase()) {
|
||||
case 'ready': return 'bg-green-100 text-green-700';
|
||||
case 'processing': return 'bg-yellow-100 text-yellow-700';
|
||||
case 'failed': return 'bg-red-100 text-red-700';
|
||||
default: return 'bg-gray-100 text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
const storagePercentage = computed(() => {
|
||||
return Math.round((stats.value.storageUsed / stats.value.storageLimit) * 100);
|
||||
});
|
||||
|
||||
const storageBreakdown = computed(() => {
|
||||
const videoSize = stats.value.storageUsed;
|
||||
const thumbSize = stats.value.totalVideos * 300 * 1024; // ~300KB per thumbnail
|
||||
const otherSize = stats.value.totalVideos * 100 * 1024; // ~100KB other files
|
||||
const total = videoSize + thumbSize + otherSize;
|
||||
|
||||
return [
|
||||
{ label: 'Videos', size: videoSize, percentage: (videoSize / total) * 100, color: 'bg-primary' },
|
||||
{ label: 'Thumbnails & Assets', size: thumbSize, percentage: (thumbSize / total) * 100, color: 'bg-blue-500' },
|
||||
{ label: 'Other Files', size: otherSize, percentage: (otherSize / total) * 100, color: 'bg-gray-400' },
|
||||
];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchDashboardData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-overview">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
description="Welcome back! Here's what's happening with your videos."
|
||||
:breadcrumbs="[
|
||||
{ label: 'Dashboard' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="animate-pulse">
|
||||
<!-- Stats Grid Skeleton -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div v-for="i in 4" :key="i" class="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="space-y-2">
|
||||
<Skeleton width="5rem" height="1rem" class="mb-2"></Skeleton>
|
||||
<Skeleton width="8rem" height="2rem"></Skeleton>
|
||||
</div>
|
||||
<Skeleton shape="circle" size="3rem"></Skeleton>
|
||||
</div>
|
||||
<Skeleton width="4rem" height="1rem"></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions Skeleton -->
|
||||
<div class="mb-8">
|
||||
<Skeleton width="10rem" height="1.5rem" class="mb-4"></Skeleton>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div v-for="i in 4" :key="i" class="p-6 rounded-xl border border-gray-200">
|
||||
<Skeleton shape="circle" size="3rem" class="mb-4"></Skeleton>
|
||||
<Skeleton width="8rem" height="1.25rem" class="mb-2"></Skeleton>
|
||||
<Skeleton width="100%" height="1rem"></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Videos Skeleton -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<Skeleton width="8rem" height="1.5rem"></Skeleton>
|
||||
<Skeleton width="5rem" height="1rem"></Skeleton>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
|
||||
<div class="flex gap-4">
|
||||
<Skeleton width="4rem" height="2.5rem" class="rounded"></Skeleton>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton width="30%" height="1rem"></Skeleton>
|
||||
<Skeleton width="20%" height="0.8rem"></Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<StatsCard
|
||||
title="Total Videos"
|
||||
:value="stats.totalVideos"
|
||||
icon="i-heroicons-film"
|
||||
color="primary"
|
||||
:trend="{ value: 12, isPositive: true }"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Total Views"
|
||||
:value="stats.totalViews.toLocaleString()"
|
||||
icon="i-heroicons-eye"
|
||||
color="info"
|
||||
:trend="{ value: 8, isPositive: true }"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Storage Used"
|
||||
:value="`${formatBytes(stats.storageUsed)} / ${formatBytes(stats.storageLimit)}`"
|
||||
icon="i-heroicons-server"
|
||||
color="warning"
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title="Uploads This Month"
|
||||
:value="stats.uploadsThisMonth"
|
||||
icon="i-heroicons-arrow-up-tray"
|
||||
color="success"
|
||||
:trend="{ value: 25, isPositive: true }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4">Quick Actions</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<button
|
||||
v-for="action in quickActions"
|
||||
:key="action.title"
|
||||
@click="action.onClick"
|
||||
:class="[
|
||||
'p-6 rounded-xl text-left transition-all duration-200',
|
||||
'border border-gray-200 hover:border-primary hover:shadow-lg',
|
||||
'group press-animated',
|
||||
action.color
|
||||
]"
|
||||
>
|
||||
<div :class="['w-12 h-12 rounded-lg flex items-center justify-center mb-4 bg-white/80', action.iconColor]">
|
||||
<span :class="[action.icon, 'w-6 h-6']" />
|
||||
</div>
|
||||
<h3 class="font-semibold mb-1 group-hover:text-primary transition-colors">{{ action.title }}</h3>
|
||||
<p class="text-sm text-gray-600">{{ action.description }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Videos -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold">Recent Videos</h2>
|
||||
<router-link
|
||||
to="/video"
|
||||
class="text-sm text-primary hover:underline font-medium flex items-center gap-1"
|
||||
>
|
||||
View all
|
||||
<span class="i-heroicons-arrow-right w-4 h-4" />
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-if="recentVideos.length === 0" class="bg-white rounded-xl border border-gray-200 p-8 text-center">
|
||||
<div class="w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mx-auto mb-4">
|
||||
<span class="i-heroicons-film w-8 h-8 text-gray-400" />
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">No videos yet</p>
|
||||
<router-link
|
||||
to="/upload"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary-600 text-white rounded-lg font-medium transition-colors"
|
||||
>
|
||||
<span class="i-heroicons-plus w-5 h-5" />
|
||||
Upload your first video
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div v-else class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Video</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Duration</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Upload Date</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="video in recentVideos" :key="video.id" class="hover:bg-gray-50 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-16 h-10 bg-gray-200 rounded overflow-hidden flex-shrink-0">
|
||||
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title" class="w-full h-full object-cover" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<span class="i-heroicons-film text-gray-400 text-xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="font-medium text-gray-900 truncate">{{ video.title }}</p>
|
||||
<p class="text-sm text-gray-500 truncate">{{ video.description || 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span :class="['px-2 py-1 text-xs font-medium rounded-full', getStatusClass(video.status)]">
|
||||
{{ video.status || 'Unknown' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
{{ formatDuration(video.duration) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
{{ formatDate(video.created_at) }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Edit">
|
||||
<span class="i-heroicons-pencil w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-gray-100 rounded transition-colors" title="Share">
|
||||
<span class="i-heroicons-share w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-red-100 rounded transition-colors" title="Delete">
|
||||
<span class="i-heroicons-trash w-4 h-4 text-red-600" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Storage Usage -->
|
||||
<div class="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h2 class="text-xl font-semibold mb-4">Storage Usage</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-gray-700">
|
||||
{{ formatBytes(stats.storageUsed) }} of {{ formatBytes(stats.storageLimit) }} used
|
||||
</span>
|
||||
<span class="text-sm font-medium" :class="storagePercentage > 80 ? 'text-danger' : 'text-gray-700'">
|
||||
{{ storagePercentage }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="h-3 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full transition-all duration-500 rounded-full"
|
||||
:class="storagePercentage > 80 ? 'bg-danger' : 'bg-primary'"
|
||||
:style="{ width: `${storagePercentage}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="item in storageBreakdown"
|
||||
:key="item.label"
|
||||
class="flex items-center justify-between text-sm"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['w-3 h-3 rounded-sm', item.color]" />
|
||||
<span class="text-gray-700">{{ item.label }}</span>
|
||||
</div>
|
||||
<span class="text-gray-500">{{ formatBytes(item.size) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="storagePercentage > 80" class="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<div class="flex gap-2">
|
||||
<span class="i-heroicons-exclamation-triangle w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-yellow-800">Storage running low</p>
|
||||
<p class="text-sm text-yellow-700 mt-1">
|
||||
Consider upgrading your plan to get more storage.
|
||||
<router-link to="/plans" class="underline font-medium">View plans</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,73 +1,87 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Toast />
|
||||
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
|
||||
class="flex flex-col gap-4 w-full">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Toast />
|
||||
<Form
|
||||
:initial-values="initialValues"
|
||||
:resolver="forgotSchema"
|
||||
class="flex flex-col gap-4 w-full"
|
||||
@submit="onFormSubmit"
|
||||
>
|
||||
<template #default="{ form }">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
||||
<InputText size="small" name="email" type="email" placeholder="you@example.com" fluid />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
fluid
|
||||
/>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="small" label="Send Reset Link" fluid />
|
||||
<Button type="submit" size="sm" fluid>
|
||||
Send Reset Link
|
||||
</Button>
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<router-link to="/login" replace
|
||||
class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
Back to Sign in
|
||||
</router-link>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<router-link
|
||||
to="/login"
|
||||
replace
|
||||
class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
></path>
|
||||
</svg>
|
||||
Back to Sign in
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Form, type FormSubmitEvent } from '@primevue/forms';
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||
import Toast from 'primevue/toast';
|
||||
import { reactive } from 'vue';
|
||||
import { z } from 'zod';
|
||||
import { client } from '@/api/client'
|
||||
import Form from '@/components/form/Form.vue'
|
||||
import Message from '@/components/form/Message.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import Toast from '@/components/ui/Toast.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { reactive } from 'vue'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { client } from '@/api/client';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
const toast = useToast()
|
||||
|
||||
const auth = useAuthStore();
|
||||
const toast = useToast();
|
||||
const forgotSchema = z.object({
|
||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' })
|
||||
})
|
||||
|
||||
const initialValues = reactive({
|
||||
email: ''
|
||||
});
|
||||
email: ''
|
||||
})
|
||||
|
||||
const resolver = zodResolver(
|
||||
z.object({
|
||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' })
|
||||
})
|
||||
);
|
||||
|
||||
const onFormSubmit = ({ valid, values }: FormSubmitEvent) => {
|
||||
if (valid) {
|
||||
client.auth.forgotPasswordCreate({ email: values.email })
|
||||
.then(() => {
|
||||
toast.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: error.message || 'An error occurred', life: 3000 });
|
||||
});
|
||||
// forgotPassword(values.email).then(() => {
|
||||
// toast.add({ severity: 'success', summary: 'Success', detail: 'Reset link sent', life: 3000 });
|
||||
// }).catch(() => {
|
||||
// toast.add({ severity: 'error', summary: 'Error', detail: auth.error, life: 3000 });
|
||||
// });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
const onFormSubmit = async (values: any) => {
|
||||
try {
|
||||
await client.auth.forgotPasswordCreate({ email: values.email })
|
||||
toast.success('Reset link sent', 'Success')
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'An error occurred', 'Error')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,102 +1,157 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Toast />
|
||||
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
|
||||
class="flex flex-col gap-4 w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
|
||||
<InputText size="small" name="email" type="text" placeholder="Enter your email" fluid
|
||||
:disabled="auth.loading" />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Toast />
|
||||
<Form
|
||||
:initial-values="initialValues"
|
||||
:resolver="loginSchema"
|
||||
class="flex flex-col gap-4 w-full"
|
||||
@submit="onFormSubmit"
|
||||
>
|
||||
<template #default="{ form }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
type="text"
|
||||
placeholder="Enter your email"
|
||||
fluid
|
||||
:disabled="auth.loading"
|
||||
/>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
||||
<Password name="password" size="small" placeholder="Enter your password" :feedback="false" toggleMask
|
||||
fluid :inputStyle="{ width: '100%' }" :disabled="auth.loading" />
|
||||
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.password.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
||||
<InputPassword
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
:feedback="false"
|
||||
fluid
|
||||
:disabled="auth.loading"
|
||||
/>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('password')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('password')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox inputId="remember-me" size="small" name="rememberMe" binary :disabled="auth.loading" />
|
||||
<label for="remember-me" class="text-sm text-gray-900">Remember me</label>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<router-link to="/forgot"
|
||||
class="text-blue-600 hover:text-blue-500 hover:underline">Forgot
|
||||
password?</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
:model-value="form.getFieldValue('rememberMe')"
|
||||
binary
|
||||
:disabled="auth.loading"
|
||||
@update:model-value="form.setFieldValue('rememberMe', $event)"
|
||||
/>
|
||||
<label for="remember-me" class="text-sm text-gray-900">Remember me</label>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<router-link
|
||||
to="/forgot"
|
||||
class="text-blue-600 hover:text-blue-500 hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="small" :label="auth.loading ? 'Signing in...' : 'Sign in'" fluid
|
||||
:loading="auth.loading" />
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
:loading="auth.loading"
|
||||
fluid
|
||||
>
|
||||
{{ auth.loading ? 'Signing in...' : 'Sign in' }}
|
||||
</Button>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center">
|
||||
<div class="w-full border-t border-gray-300"></div>
|
||||
</div>
|
||||
<div class="relative flex justify-center text-sm">
|
||||
<span class="px-2 bg-white text-gray-500">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="small" type="button" variant="outlined" severity="secondary"
|
||||
class="w-full flex items-center justify-center gap-2" @click="loginWithGoogle" :disabled="auth.loading">
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z" />
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Don't have an account?
|
||||
<router-link to="/sign-up" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up</router-link>
|
||||
</p>
|
||||
<!-- <router-link to="/forgot" class="text-blue-600 hover:text-blue-500 hover:underline">Forgot password?</router-link> -->
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full flex items-center justify-center gap-2"
|
||||
:disabled="auth.loading"
|
||||
@click="loginWithGoogle"
|
||||
>
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"
|
||||
/>
|
||||
</svg>
|
||||
Google
|
||||
</Button>
|
||||
|
||||
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600">
|
||||
<p class="text-center text-sm text-gray-600">
|
||||
Don't have an account?
|
||||
<router-link
|
||||
to="/sign-up"
|
||||
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
|
||||
>
|
||||
Sign up
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { Form, type FormSubmitEvent } from '@primevue/forms';
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { reactive } from 'vue';
|
||||
import { z } from 'zod';
|
||||
const t = useToast();
|
||||
const auth = useAuthStore();
|
||||
// const $form = Form.useFormContext();
|
||||
watch(() => auth.error, (newError) => {
|
||||
if (newError) {
|
||||
t.add({ severity: 'error', summary: String(auth.error), detail: newError, life: 5000 });
|
||||
}
|
||||
});
|
||||
import Form from '@/components/form/Form.vue'
|
||||
import Message from '@/components/form/Message.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Checkbox from '@/components/ui/Checkbox.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import InputPassword from '@/components/ui/InputPassword.vue'
|
||||
import Toast from '@/components/ui/Toast.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { z } from 'zod'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
const loginSchema = z.object({
|
||||
email: z.string().min(1, { message: 'Email or username is required.' }),
|
||||
password: z.string().min(1, { message: 'Password is required.' })
|
||||
})
|
||||
|
||||
const initialValues = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
});
|
||||
email: '',
|
||||
password: '',
|
||||
rememberMe: false
|
||||
})
|
||||
|
||||
const resolver = zodResolver(
|
||||
z.object({
|
||||
email: z.string().min(1, { message: 'Email or username is required.' }),
|
||||
password: z.string().min(1, { message: 'Password is required.' })
|
||||
})
|
||||
);
|
||||
watch(() => auth.error, (newError) => {
|
||||
if (newError) {
|
||||
toast.error(String(auth.error), 'Error')
|
||||
}
|
||||
})
|
||||
|
||||
const onFormSubmit = async ({ valid, values }: FormSubmitEvent) => {
|
||||
if (valid) auth.login(values.email, values.password);
|
||||
};
|
||||
const onFormSubmit = async (values: any) => {
|
||||
await auth.login(values.email, values.password)
|
||||
}
|
||||
|
||||
const loginWithGoogle = () => {
|
||||
auth.loginWithGoogle();
|
||||
};
|
||||
</script>
|
||||
auth.loginWithGoogle()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,70 +1,106 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
|
||||
class="flex flex-col gap-4 w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-sm font-medium text-gray-700">Full Name</label>
|
||||
<InputText size="small" name="name" placeholder="John Doe" fluid />
|
||||
<Message v-if="$form.name?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.name.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<Form
|
||||
:initial-values="initialValues"
|
||||
:resolver="signupSchema"
|
||||
class="flex flex-col gap-4 w-full"
|
||||
@submit="onFormSubmit"
|
||||
>
|
||||
<template #default="{ form }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-sm font-medium text-gray-700">Full Name</label>
|
||||
<Input
|
||||
name="name"
|
||||
placeholder="John Doe"
|
||||
fluid
|
||||
/>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('name')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('name')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
||||
<InputText size="small" name="email" type="email" placeholder="you@example.com" fluid />
|
||||
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.email.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email address</label>
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
fluid
|
||||
/>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('email')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('email')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
||||
<Password name="password" size="small" placeholder="Create a password" :feedback="true" toggleMask fluid
|
||||
:inputStyle="{ width: '100%' }" />
|
||||
<small class="text-gray-500">Must be at least 8 characters.</small>
|
||||
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{
|
||||
$form.password.error?.message }}</Message>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="password" class="text-sm font-medium text-gray-700">Password</label>
|
||||
<InputPassword
|
||||
name="password"
|
||||
placeholder="Create a password"
|
||||
:feedback="true"
|
||||
fluid
|
||||
/>
|
||||
<small class="text-gray-500">Must be at least 8 characters.</small>
|
||||
<Message
|
||||
v-if="form.getFieldMeta('password')?.errorMap?.onChange"
|
||||
severity="error"
|
||||
size="sm"
|
||||
>
|
||||
{{ form.getFieldMeta('password')?.errorMap?.onChange }}
|
||||
</Message>
|
||||
</div>
|
||||
|
||||
<Button type="submit" size="small" label="Create Account" fluid />
|
||||
<Button type="submit" size="sm" fluid>
|
||||
Create Account
|
||||
</Button>
|
||||
|
||||
<p class="mt-4 text-center text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<router-link to="/login" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign
|
||||
in</router-link>
|
||||
</p>
|
||||
</Form>
|
||||
</div>
|
||||
<p class="mt-4 text-center text-sm text-gray-600">
|
||||
Already have an account?
|
||||
<router-link
|
||||
to="/login"
|
||||
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
|
||||
>
|
||||
Sign in
|
||||
</router-link>
|
||||
</p>
|
||||
</template>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Form from '@/components/form/Form.vue'
|
||||
import Message from '@/components/form/Message.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import InputPassword from '@/components/ui/InputPassword.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { reactive } from 'vue'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Form, type FormSubmitEvent } from '@primevue/forms';
|
||||
import { zodResolver } from '@primevue/forms/resolvers/zod';
|
||||
import { reactive } from 'vue';
|
||||
import { z } from 'zod';
|
||||
const auth = useAuthStore()
|
||||
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
|
||||
const auth = useAuthStore();
|
||||
const signupSchema = z.object({
|
||||
name: z.string().min(1, { message: 'Name is required.' }),
|
||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }),
|
||||
password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
|
||||
})
|
||||
|
||||
const initialValues = reactive({
|
||||
name: '',
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
name: '',
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const resolver = zodResolver(
|
||||
z.object({
|
||||
name: z.string().min(1, { message: 'Name is required.' }),
|
||||
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }),
|
||||
password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
|
||||
})
|
||||
);
|
||||
|
||||
const onFormSubmit = ({ valid, values }: FormSubmitEvent) => {
|
||||
if (valid) {
|
||||
auth.register(values.name, values.email, values.password);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
const onFormSubmit = (values: any) => {
|
||||
auth.register(values.name, values.email, values.password)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Chart from '@/components/icons/Chart.vue';
|
||||
import Credit from '@/components/icons/Credit.vue';
|
||||
import Upload from '@/components/icons/Upload.vue';
|
||||
import Video from '@/components/icons/Video.vue';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
import Skeleton from '@/components/ui/Skeleton.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Referral from './Referral.vue';
|
||||
|
||||
@@ -49,7 +49,7 @@ const quickActions = [
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div v-for="i in 4" :key="i" class="p-6 rounded-xl border border-gray-200">
|
||||
<Skeleton shape="circle" size="3rem" class="mb-4"></Skeleton>
|
||||
<Skeleton circle width="3rem" height="3rem" class="mb-4"></Skeleton>
|
||||
<Skeleton width="8rem" height="1.25rem" class="mb-2"></Skeleton>
|
||||
<Skeleton width="100%" height="1rem"></Skeleton>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ModelVideo } from '@/api/client';
|
||||
import EmptyState from '@/components/dashboard/EmptyState.vue';
|
||||
import { formatBytes, formatDate, formatDuration } from '@/lib/utils';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
import Skeleton from '@/components/ui/Skeleton.vue';
|
||||
import { formatDate, formatDuration } from '@/lib/utils';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
interface Props {
|
||||
@@ -34,7 +34,7 @@ const getStatusClass = (status?: string) => {
|
||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div class="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
|
||||
<div class="flex gap-4">
|
||||
<Skeleton width="4rem" height="2.5rem" class="rounded"></Skeleton>
|
||||
<Skeleton width="4rem" height="2.5rem" border-radius="0.25rem"></Skeleton>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Skeleton width="30%" height="1rem"></Skeleton>
|
||||
<Skeleton width="20%" height="0.8rem"></Skeleton>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from
|
||||
referred users!</p>
|
||||
<div class="flex gap-2">
|
||||
<InputText class="w-full" readonly type="text" :value="url" @click="copyToClipboard" />
|
||||
<Input class="w-full" readonly type="text" :value="url" @click="copyToClipboard" />
|
||||
<button class="btn btn-primary" @click="copyToClipboard" :disabled="isCopied">
|
||||
<svg v-if="!isCopied" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
@@ -27,6 +27,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import Input from '@/components/ui/Input.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { ref } from 'vue';
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import StatsCard from '@/components/dashboard/StatsCard.vue';
|
||||
import Skeleton from '@/components/ui/Skeleton.vue';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
@@ -25,7 +25,7 @@ defineProps<Props>();
|
||||
<Skeleton width="5rem" height="1rem" class="mb-2"></Skeleton>
|
||||
<Skeleton width="8rem" height="2rem"></Skeleton>
|
||||
</div>
|
||||
<!-- <Skeleton shape="circle" size="3rem"></Skeleton> -->
|
||||
<!-- <Skeleton circle width="3rem" height="3rem"></Skeleton> -->
|
||||
</div>
|
||||
<Skeleton width="4rem" height="1rem"></Skeleton>
|
||||
</div>
|
||||
|
||||
@@ -1,198 +1,177 @@
|
||||
<script setup lang="ts">
|
||||
import { client, type ModelPlan } from '@/api/client';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import useSWRV from '@/lib/swr';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import CurrentPlanCard from './components/CurrentPlanCard.vue';
|
||||
import UsageStatsCard from './components/UsageStatsCard.vue';
|
||||
import PlanList from './components/PlanList.vue';
|
||||
import PlanPaymentHistory from './components/PlanPaymentHistory.vue';
|
||||
import EditPlanDialog from './components/EditPlanDialog.vue';
|
||||
import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue';
|
||||
import { client, type ModelPlan } from '@/api/client'
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue'
|
||||
import { useSWRV } from '@/composables/useDataLoader'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { computed, ref } from 'vue'
|
||||
import CurrentPlanCard from './components/CurrentPlanCard.vue'
|
||||
import EditPlanDialog from './components/EditPlanDialog.vue'
|
||||
import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue'
|
||||
import PlanList from './components/PlanList.vue'
|
||||
import PlanPaymentHistory from './components/PlanPaymentHistory.vue'
|
||||
import UsageStatsCard from './components/UsageStatsCard.vue'
|
||||
|
||||
const auth = useAuthStore();
|
||||
// const plans = ref<ModelPlan[]>([]);
|
||||
const subscribing = ref<string | null>(null);
|
||||
const showManageDialog = ref(false);
|
||||
const cancelling = ref(false);
|
||||
const auth = useAuthStore()
|
||||
const subscribing = ref<string | null>(null)
|
||||
const showManageDialog = ref(false)
|
||||
const cancelling = ref(false)
|
||||
|
||||
// Mock Payment History Data
|
||||
const paymentHistory = ref([
|
||||
{ id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' },
|
||||
{ id: 'inv_002', date: 'Nov 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-002' },
|
||||
{ id: 'inv_003', date: 'Dec 24, 2025', amount: 19.99, plan: 'Pro Plan', status: 'failed', invoiceId: 'INV-2025-003' },
|
||||
{ id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' },
|
||||
]);
|
||||
const { data, isLoading, mutate: mutatePlans } = useSWRV("r/plans", client.plans.plansList)
|
||||
{ id: 'inv_001', date: 'Oct 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-001' },
|
||||
{ id: 'inv_002', date: 'Nov 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-002' },
|
||||
{ id: 'inv_003', date: 'Dec 24, 2025', amount: 19.99, plan: 'Pro Plan', status: 'failed', invoiceId: 'INV-2025-003' },
|
||||
{ id: 'inv_004', date: 'Jan 24, 2026', amount: 19.99, plan: 'Pro Plan', status: 'pending', invoiceId: 'INV-2026-001' },
|
||||
])
|
||||
|
||||
const { data, isLoading, mutate: mutatePlans } = useSWRV('r/plans', () => client.plans.plansList())
|
||||
|
||||
// Computed Usage (Mock if not in store)
|
||||
const storageUsed = computed(() => auth.user?.storage_used || 0); // bytes
|
||||
// Default limit 10GB if no plan
|
||||
const storageLimit = computed(() => 10737418240);
|
||||
const uploadsUsed = ref(12);
|
||||
const uploadsLimit = ref(50);
|
||||
const storageUsed = computed(() => auth.user?.storage_used || 0) // bytes
|
||||
const storageLimit = computed(() => 10737418240)
|
||||
const uploadsUsed = ref(12)
|
||||
const uploadsLimit = ref(50)
|
||||
|
||||
const currentPlanId = computed(() => {
|
||||
if (auth.user?.plan_id) return auth.user.plan_id;
|
||||
if (Array.isArray(data?.value?.data?.data.plans) && data?.value?.data?.data.plans.length > 0) return data.value.data.data.plans[0].id; // Fallback to first plan
|
||||
return undefined;
|
||||
});
|
||||
if (auth.user?.plan_id) return auth.user.plan_id
|
||||
if (Array.isArray(data.value?.data?.data?.plans) && data.value?.data?.data?.plans.length > 0) return data.value.data.data.plans[0].id
|
||||
return undefined
|
||||
})
|
||||
|
||||
const currentPlan = computed(() => {
|
||||
if (!Array.isArray(data?.value?.data?.data.plans)) return undefined;
|
||||
return data.value.data.data.plans.find(p => p.id === currentPlanId.value);
|
||||
});
|
||||
if (!Array.isArray(data.value?.data?.data?.plans)) return undefined
|
||||
return data.value.data.data.plans.find((p: ModelPlan) => p.id === currentPlanId.value)
|
||||
})
|
||||
|
||||
|
||||
// watch(data, (newValue) => {
|
||||
// if (newValue) {
|
||||
// // Handle potentially different response structures
|
||||
// // Safe access to avoid SSR crash if data is null/undefined
|
||||
// const plansList = newValue?.data?.data?.plans;
|
||||
// if (Array.isArray(plansList)) {
|
||||
// plans.value = plansList;
|
||||
// }
|
||||
// }
|
||||
// }, { immediate: true });
|
||||
|
||||
const showEditDialog = ref(false);
|
||||
const editingPlan = ref<ModelPlan>({});
|
||||
const isSaving = ref(false);
|
||||
const showEditDialog = ref(false)
|
||||
const editingPlan = ref<ModelPlan>({} as ModelPlan)
|
||||
const isSaving = ref(false)
|
||||
|
||||
const openEditPlan = (plan: ModelPlan) => {
|
||||
editingPlan.value = { ...plan };
|
||||
showEditDialog.value = true;
|
||||
};
|
||||
editingPlan.value = { ...plan }
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
const savePlan = async (updatedPlan: ModelPlan) => {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
if (!updatedPlan.id) return;
|
||||
|
||||
// Optimistic update or API call
|
||||
await client.request({
|
||||
path: `/plans/${updatedPlan.id}`,
|
||||
method: 'PUT',
|
||||
body: updatedPlan
|
||||
});
|
||||
|
||||
// Refresh plans
|
||||
await mutatePlans();
|
||||
|
||||
showEditDialog.value = false;
|
||||
alert('Plan updated successfully');
|
||||
} catch (e: any) {
|
||||
console.error('Failed to update plan', e);
|
||||
// Fallback: update local state if API is mocked/missing
|
||||
const idx = data.value!.data.data.plans.findIndex(p => p.id === updatedPlan.id);
|
||||
if (idx !== -1) {
|
||||
data.value!.data.data.plans[idx] = { ...updatedPlan };
|
||||
}
|
||||
showEditDialog.value = false;
|
||||
// alert('Note: API update failed, updated locally. ' + e.message);
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
isSaving.value = true
|
||||
try {
|
||||
if (!updatedPlan.id) return
|
||||
|
||||
await client.request({
|
||||
path: `/plans/${updatedPlan.id}`,
|
||||
method: 'PUT',
|
||||
body: updatedPlan
|
||||
})
|
||||
|
||||
await mutatePlans()
|
||||
|
||||
showEditDialog.value = false
|
||||
alert('Plan updated successfully')
|
||||
} catch (e: any) {
|
||||
console.error('Failed to update plan', e)
|
||||
const idx = data.value!.data.data.plans.findIndex((p: ModelPlan) => p.id === updatedPlan.id)
|
||||
if (idx !== -1) {
|
||||
data.value!.data.data.plans[idx] = { ...updatedPlan }
|
||||
}
|
||||
};
|
||||
showEditDialog.value = false
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const subscribe = async (plan: ModelPlan) => {
|
||||
if (!plan.id) return;
|
||||
subscribing.value = plan.id;
|
||||
if (!plan.id) return
|
||||
subscribing.value = plan.id
|
||||
try {
|
||||
await client.payments.paymentsCreate({
|
||||
amount: plan.price || 0,
|
||||
plan_id: plan.id
|
||||
});
|
||||
// Update local state mock
|
||||
// In real app, we would re-fetch user profile
|
||||
alert(`Successfully subscribed to ${plan.name}`);
|
||||
|
||||
paymentHistory.value.unshift({
|
||||
id: `inv_${Date.now()}`,
|
||||
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
|
||||
amount: plan.price || 0,
|
||||
plan: plan.name || 'Unknown',
|
||||
status: 'success',
|
||||
invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}`
|
||||
});
|
||||
await client.payments.paymentsCreate({
|
||||
amount: plan.price || 0,
|
||||
plan_id: plan.id
|
||||
})
|
||||
alert(`Successfully subscribed to ${plan.name}`)
|
||||
|
||||
paymentHistory.value.unshift({
|
||||
id: `inv_${Date.now()}`,
|
||||
date: new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
|
||||
amount: plan.price || 0,
|
||||
plan: plan.name || 'Unknown',
|
||||
status: 'success',
|
||||
invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}`
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
alert('Failed to subscribe: ' + (err.message || 'Unknown error'));
|
||||
console.error(err)
|
||||
alert('Failed to subscribe: ' + (err.message || 'Unknown error'))
|
||||
} finally {
|
||||
subscribing.value = null;
|
||||
subscribing.value = null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const cancelSubscription = async () => {
|
||||
cancelling.value = true;
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
alert('Subscription has been canceled.');
|
||||
showManageDialog.value = false;
|
||||
} catch (e) {
|
||||
alert('Failed to cancel subscription.');
|
||||
} finally {
|
||||
cancelling.value = false;
|
||||
}
|
||||
};
|
||||
cancelling.value = true
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
alert('Subscription has been canceled.')
|
||||
showManageDialog.value = false
|
||||
} catch (e) {
|
||||
alert('Failed to cancel subscription.')
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="plans-page">
|
||||
<PageHeader
|
||||
title="Subscription"
|
||||
description="Manage your workspace plan and usage"
|
||||
:breadcrumbs="[
|
||||
{ label: 'Dashboard', to: '/' },
|
||||
{ label: 'Subscription' }
|
||||
]"
|
||||
title="Subscription"
|
||||
description="Manage your workspace plan and usage"
|
||||
:breadcrumbs="[
|
||||
{ label: 'Dashboard', to: '/' },
|
||||
{ label: 'Subscription' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="content max-w-7xl mx-auto space-y-12 pb-12">
|
||||
|
||||
<!-- Hero Section: Current Plan & Usage -->
|
||||
<div v-if="!isLoading" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<CurrentPlanCard
|
||||
:current-plan="currentPlan"
|
||||
@manage="showManageDialog = true"
|
||||
/>
|
||||
|
||||
<UsageStatsCard
|
||||
:storage-used="storageUsed"
|
||||
:storage-limit="storageLimit"
|
||||
:uploads-used="uploadsUsed"
|
||||
:uploads-limit="uploadsLimit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PlanList
|
||||
:plans="data?.data?.data.plans || []"
|
||||
:is-loading="!!isLoading"
|
||||
:current-plan-id="currentPlanId"
|
||||
:subscribing-plan-id="subscribing"
|
||||
:is-admin="auth.user?.role === 'admin'"
|
||||
@subscribe="subscribe"
|
||||
@edit="openEditPlan"
|
||||
|
||||
<!-- Hero Section: Current Plan & Usage -->
|
||||
<div v-if="!isLoading" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<CurrentPlanCard
|
||||
:current-plan="currentPlan"
|
||||
@manage="showManageDialog = true"
|
||||
/>
|
||||
|
||||
<PlanPaymentHistory :history="paymentHistory" />
|
||||
|
||||
<ManageSubscriptionDialog
|
||||
v-model:visible="showManageDialog"
|
||||
:current-plan="currentPlan"
|
||||
:cancelling="cancelling"
|
||||
@cancel-subscription="cancelSubscription"
|
||||
<UsageStatsCard
|
||||
:storage-used="storageUsed"
|
||||
:storage-limit="storageLimit"
|
||||
:uploads-used="uploadsUsed"
|
||||
:uploads-limit="uploadsLimit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PlanList
|
||||
:plans="data?.data?.data?.plans || []"
|
||||
:is-loading="!!isLoading"
|
||||
:current-plan-id="currentPlanId"
|
||||
:subscribing-plan-id="subscribing"
|
||||
:is-admin="auth.user?.role === 'admin'"
|
||||
@subscribe="subscribe"
|
||||
@edit="openEditPlan"
|
||||
/>
|
||||
|
||||
<PlanPaymentHistory :history="paymentHistory" />
|
||||
|
||||
<ManageSubscriptionDialog
|
||||
v-model:visible="showManageDialog"
|
||||
:current-plan="currentPlan"
|
||||
:cancelling="cancelling"
|
||||
@cancel-subscription="cancelSubscription"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<EditPlanDialog
|
||||
v-model:visible="showEditDialog"
|
||||
:plan="editingPlan"
|
||||
:loading="isSaving"
|
||||
@save="savePlan"
|
||||
v-model:visible="showEditDialog"
|
||||
:plan="editingPlan"
|
||||
:loading="isSaving"
|
||||
@save="savePlan"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,39 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { type ModelPlan } from '@/api/client';
|
||||
import Button from 'primevue/button';
|
||||
import Tag from 'primevue/tag';
|
||||
import type { ModelPlan } from '@/api/client';
|
||||
import Button from '@/components/ui/Button.vue';
|
||||
import Tag from '@/components/ui/Tag.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
currentPlan?: ModelPlan;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
currentPlan?: ModelPlan
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'manage'): void;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'manage'): void
|
||||
}>()
|
||||
|
||||
const planName = computed(() => props.currentPlan?.name || 'Free Plan')
|
||||
const planPrice = computed(() => props.currentPlan?.price || 0)
|
||||
const planCycle = computed(() => props.currentPlan?.cycle || 'month')
|
||||
const isActive = computed(() => props.currentPlan?.is_active !== false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=":uno: lg:col-span-2 relative overflow-hidden rounded-2xl bg-gradient-to-br from-gray-900 to-gray-800 text-white p-8">
|
||||
<!-- Background decorations -->
|
||||
<div class="absolute top-0 right-0 -mt-16 -mr-16 w-64 h-64 bg-primary-500 rounded-full blur-3xl opacity-20"></div>
|
||||
<div class="absolute bottom-0 left-0 -mb-16 -ml-16 w-64 h-64 bg-purple-500 rounded-full blur-3xl opacity-20"></div>
|
||||
|
||||
<div class="relative z-10 flex flex-col h-full justify-between">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-gray-400 uppercase tracking-wider mb-1">Current Plan</h2>
|
||||
<h3 class="text-4xl font-bold text-white mb-2">{{ currentPlan?.name || 'Standard Plan' }}</h3>
|
||||
<Tag value="Active" severity="success" class="px-3" rounded></Tag>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-3xl font-bold text-white">${{ currentPlan?.price || 0 }}<span class="text-lg text-gray-400 font-normal">/mo</span></div>
|
||||
<p class="text-gray-400 text-sm mt-1">Next billing on Feb 24, 2026</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-8 border-t border-gray-700/50 flex gap-4">
|
||||
<Button label="Manage Subscription" severity="secondary" class="bg-white/10 border-white/10 text-white hover:bg-white/20" @click="$emit('manage')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gradient-to-br from-blue-600 to-purple-700 rounded-2xl p-8 text-white">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<p class="text-blue-100 text-sm font-medium mb-1">Current Plan</p>
|
||||
<h3 class="text-3xl font-bold">{{ planName }}</h3>
|
||||
</div>
|
||||
<Tag
|
||||
:value="isActive ? 'Active' : 'Inactive'"
|
||||
:severity="isActive ? 'success' : 'danger'"
|
||||
class="!bg-white/20 !text-white !border-white/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-baseline gap-1 mb-6">
|
||||
<span class="text-4xl font-bold">${{ planPrice }}</span>
|
||||
<span class="text-blue-100">/{{ planCycle }}</span>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" class="!text-white !border-white/50 hover:!bg-white/20" @click="emit('manage')">
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,90 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { type ModelPlan } from '@/api/client';
|
||||
import Button from 'primevue/button';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { type ModelPlan } from '@/api/client'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Checkbox from '@/components/ui/Checkbox.vue'
|
||||
import Dialog from '@/components/ui/Dialog.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
plan: ModelPlan;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
visible: boolean
|
||||
plan: ModelPlan
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'save', plan: ModelPlan): void;
|
||||
}>();
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'save', plan: ModelPlan): void
|
||||
}>()
|
||||
|
||||
// Create a local copy to edit
|
||||
const localPlan = ref<ModelPlan>({});
|
||||
const localPlan = ref<ModelPlan>({} as ModelPlan)
|
||||
|
||||
// Sync when dialog opens or plan changes
|
||||
watch(() => props.plan, (newPlan) => {
|
||||
localPlan.value = { ...newPlan };
|
||||
}, { immediate: true });
|
||||
localPlan.value = { ...newPlan }
|
||||
}, { immediate: true })
|
||||
|
||||
const onSave = () => {
|
||||
emit('save', localPlan.value);
|
||||
};
|
||||
emit('save', localPlan.value)
|
||||
}
|
||||
|
||||
const visibleModel = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const isActive = computed({
|
||||
get: () => localPlan.value.is_active ?? false,
|
||||
set: (val: boolean) => {
|
||||
localPlan.value.is_active = val
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="visibleModel" modal header="Edit Plan" :style="{ width: '40rem' }">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-name" class="text-sm font-medium text-gray-700">Name</label>
|
||||
<InputText id="plan-name" v-model="localPlan.name" placeholder="Plan Name" />
|
||||
</div>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
header="Edit Plan"
|
||||
width="40rem"
|
||||
:closable="true"
|
||||
@update:visible="handleClose"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-name" class="text-sm font-medium text-gray-700">Name</label>
|
||||
<Input id="plan-name" v-model="localPlan.name" placeholder="Plan Name" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-price" class="text-sm font-medium text-gray-700">Price ($)</label>
|
||||
<InputNumber id="plan-price" v-model="localPlan.price" mode="currency" currency="USD" locale="en-US" :minFractionDigits="2" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-cycle" class="text-sm font-medium text-gray-700">Billing Cycle</label>
|
||||
<InputText id="plan-cycle" v-model="localPlan.cycle" placeholder="e.g. month, year" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-desc" class="text-sm font-medium text-gray-700">Description</label>
|
||||
<Textarea id="plan-desc" v-model="localPlan.description" rows="2" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-storage" class="text-sm font-medium text-gray-700">Storage Limit (bytes)</label>
|
||||
<InputNumber id="plan-storage" v-model="localPlan.storage_limit" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-uploads" class="text-sm font-medium text-gray-700">Upload Limit (per day)</label>
|
||||
<InputNumber id="plan-uploads" v-model="localPlan.upload_limit" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-duration" class="text-sm font-medium text-gray-700">Duration Limit (sec)</label>
|
||||
<InputNumber id="plan-duration" v-model="localPlan.duration_limit" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<Checkbox v-model="localPlan.is_active" :binary="true" inputId="plan-active" />
|
||||
<label for="plan-active" class="text-sm font-medium text-gray-700">Active</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-price" class="text-sm font-medium text-gray-700">Price ($)</label>
|
||||
<Input id="plan-price" :model-value="localPlan.price ?? ''" type="number" placeholder="0.00" @update:model-value="localPlan.price = Number($event)" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-cycle" class="text-sm font-medium text-gray-700">Billing Cycle</label>
|
||||
<Input id="plan-cycle" v-model="localPlan.cycle" placeholder="e.g. month, year" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancel" text severity="secondary" @click="visibleModel = false" />
|
||||
<Button label="Save Changes" icon="i-heroicons-check" @click="onSave" :loading="loading" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-desc" class="text-sm font-medium text-gray-700">Description</label>
|
||||
<textarea
|
||||
id="plan-desc"
|
||||
v-model="localPlan.description"
|
||||
rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-storage" class="text-sm font-medium text-gray-700">Storage Limit (bytes)</label>
|
||||
<Input id="plan-storage" :model-value="localPlan.storage_limit ?? ''" type="number" @update:model-value="localPlan.storage_limit = Number($event)" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-uploads" class="text-sm font-medium text-gray-700">Upload Limit (per day)</label>
|
||||
<Input id="plan-uploads" :model-value="localPlan.upload_limit ?? ''" type="number" @update:model-value="localPlan.upload_limit = Number($event)" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="plan-duration" class="text-sm font-medium text-gray-700">Duration Limit (sec)</label>
|
||||
<Input id="plan-duration" :model-value="localPlan.duration_limit ?? ''" type="number" @update:model-value="localPlan.duration_limit = Number($event)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 pt-2">
|
||||
<Checkbox v-model="isActive" :binary="true" />
|
||||
<label class="text-sm font-medium text-gray-700">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" @click="handleClose">Cancel</Button>
|
||||
<Button :loading="loading" @click="onSave">Save Changes</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import { type ModelPlan } from '@/api/client';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import { computed } from 'vue';
|
||||
import type { ModelPlan } from '@/api/client';
|
||||
import Button from '@/components/ui/Button.vue';
|
||||
import Dialog from '@/components/ui/Dialog.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
currentPlan?: ModelPlan;
|
||||
cancelling?: boolean;
|
||||
}>();
|
||||
visible: boolean
|
||||
currentPlan?: ModelPlan
|
||||
cancelling?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', value: boolean): void;
|
||||
(e: 'cancel-subscription'): void;
|
||||
}>();
|
||||
(e: 'update:visible', value: boolean): void
|
||||
(e: 'cancel-subscription'): void
|
||||
}>()
|
||||
|
||||
const visibleModel = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
});
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:visible="visibleModel" modal header="Manage Subscription" :style="{ width: '30rem' }">
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-600 mb-4">You are currently subscribed to <span class="font-bold text-gray-900">{{ currentPlan?.name }}</span>.</p>
|
||||
<div class="bg-gray-50 p-4 rounded-lg space-y-2 border border-gray-200">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Status</span>
|
||||
<span class="text-sm font-medium text-green-600">Active</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Renewal Date</span>
|
||||
<span class="text-sm font-medium text-gray-900">Feb 24, 2026</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Amount</span>
|
||||
<span class="text-sm font-medium text-gray-900">${{ currentPlan?.price || 0 }}/mo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 mb-6">
|
||||
Canceling your subscription will downgrade you to the Free plan at the end of your current billing period.
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
header="Manage Subscription"
|
||||
width="28rem"
|
||||
:closable="true"
|
||||
@update:visible="handleClose"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div v-if="currentPlan" class="bg-gray-50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-900">{{ currentPlan.name }}</h4>
|
||||
<p class="text-sm text-gray-500">${{ currentPlan.price }}/{{ currentPlan.cycle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Cancel Subscription</h4>
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
If you cancel, you'll lose access to premium features at the end of your billing period.
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button label="Close" text severity="secondary" @click="visibleModel = false" />
|
||||
<Button
|
||||
label="Cancel Subscription"
|
||||
severity="danger"
|
||||
:icon="cancelling ? 'i-svg-spinners-180-ring-with-bg' : 'i-heroicons-x-circle'"
|
||||
@click="emit('cancel-subscription')"
|
||||
:disabled="cancelling"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="danger"
|
||||
:loading="cancelling"
|
||||
@click="emit('cancel-subscription')"
|
||||
>
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button variant="outline" @click="handleClose">Close</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,107 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
import { type ModelPlan } from '@/api/client';
|
||||
import Button from 'primevue/button';
|
||||
import Skeleton from 'primevue/skeleton';
|
||||
import { formatBytes } from '@/lib/utils'; // Using utils formatBytes
|
||||
import type { ModelPlan } from '@/api/client';
|
||||
import Button from '@/components/ui/Button.vue';
|
||||
import Skeleton from '@/components/ui/Skeleton.vue';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
|
||||
defineProps<{
|
||||
plans: ModelPlan[];
|
||||
isLoading: boolean;
|
||||
currentPlanId?: string;
|
||||
subscribingPlanId?: string | null;
|
||||
isAdmin?: boolean;
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
plans: ModelPlan[]
|
||||
isLoading: boolean
|
||||
currentPlanId?: string
|
||||
subscribingPlanId?: string | null
|
||||
isAdmin?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'subscribe', plan: ModelPlan): void;
|
||||
(e: 'edit', plan: ModelPlan): void;
|
||||
}>();
|
||||
|
||||
const formatDuration = (seconds?: number) => {
|
||||
if (!seconds) return '0 mins';
|
||||
return `${Math.floor(seconds / 60)} mins`;
|
||||
};
|
||||
|
||||
const isPopular = (plan: ModelPlan) => {
|
||||
return plan.name?.toLowerCase().includes('pro') || plan.name?.toLowerCase().includes('premium');
|
||||
};
|
||||
|
||||
const isCurrentComp = (plan: ModelPlan, currentId?: string) => {
|
||||
return plan.id === currentId;
|
||||
}
|
||||
(e: 'subscribe', plan: ModelPlan): void
|
||||
(e: 'edit', plan: ModelPlan): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900">Upgrade your workspace</h2>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div v-for="i in 3" :key="i" class="h-full">
|
||||
<Skeleton height="300px" borderRadius="16px"></Skeleton>
|
||||
</div>
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Available Plans</h2>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div v-for="i in 3" :key="i" class="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<Skeleton height="1.5rem" width="60%" class="mb-4" />
|
||||
<Skeleton height="2rem" width="40%" class="mb-6" />
|
||||
<Skeleton height="1rem" class="mb-2" />
|
||||
<Skeleton height="1rem" class="mb-2" />
|
||||
<Skeleton height="1rem" width="80%" class="mb-6" />
|
||||
<Skeleton height="2.5rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plans Grid -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div
|
||||
v-for="plan in plans"
|
||||
:key="plan.id"
|
||||
class="bg-white border rounded-xl p-6 transition-all hover:shadow-lg"
|
||||
:class="plan.id === currentPlanId ? 'border-blue-500 ring-2 ring-blue-500/20' : 'border-gray-200'"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-900">{{ plan.name }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ plan.cycle }}</p>
|
||||
</div>
|
||||
<div v-if="plan.id === currentPlanId" class="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
|
||||
Current
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-8 items-start">
|
||||
<div v-for="plan in plans" :key="plan.id" class="relative group h-full">
|
||||
<div v-if="isPopular(plan) && !isCurrentComp(plan, currentPlanId)" class="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-white text-xs font-bold px-3 py-1 rounded-full z-10 shadow-md uppercase tracking-wide">
|
||||
Recommended
|
||||
</div>
|
||||
|
||||
<!-- Admin Edit Button -->
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
icon="i-heroicons-pencil-square"
|
||||
class="absolute top-2 right-2 z-20 !p-2 !w-8 !h-8"
|
||||
severity="secondary"
|
||||
text
|
||||
rounded
|
||||
@click.stop="emit('edit', plan)"
|
||||
/>
|
||||
|
||||
<div :class="[
|
||||
'relative bg-white rounded-2xl p-6 h-full border transition-all duration-200 flex flex-col',
|
||||
isCurrentComp(plan, currentPlanId) ? 'border-primary ring-1 ring-primary/50 bg-primary-50/10' : 'border-gray-200 hover:border-gray-300 hover:shadow-lg',
|
||||
isPopular(plan) && !isCurrentComp(plan, currentPlanId) ? 'shadow-md border-primary/20' : ''
|
||||
]">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-bold text-gray-900">{{ plan.name }}</h3>
|
||||
<p class="text-gray-500 text-sm min-h-[2.5rem] mt-2">{{ plan.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<span class="text-4xl font-bold text-gray-900">${{ plan.price }}</span>
|
||||
<span class="text-gray-500 text-sm">/{{ plan.cycle }}</span>
|
||||
</div>
|
||||
|
||||
<ul class="space-y-3 mb-8 flex-grow">
|
||||
<li class="flex items-center gap-3 text-sm text-gray-700">
|
||||
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
|
||||
{{ formatBytes(plan.storage_limit || 0) }} Storage
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-sm text-gray-700">
|
||||
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
|
||||
{{ formatDuration(plan.duration_limit) }} Max Duration
|
||||
</li>
|
||||
<li class="flex items-center gap-3 text-sm text-gray-700">
|
||||
<span class="i-heroicons-check-circle text-green-500 text-lg flex-shrink-0"></span>
|
||||
{{ plan.upload_limit }} Uploads / day
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Button
|
||||
:label="isCurrentComp(plan, currentPlanId) ? 'Current Plan' : (subscribingPlanId === plan.id ? 'Processing...' : 'Upgrade')"
|
||||
:icon="subscribingPlanId === plan.id ? 'i-svg-spinners-180-ring-with-bg' : ''"
|
||||
class="w-full"
|
||||
:severity="isCurrentComp(plan, currentPlanId) ? 'secondary' : 'primary'"
|
||||
:outlined="isCurrentComp(plan, currentPlanId)"
|
||||
:disabled="!!subscribingPlanId || isCurrentComp(plan, currentPlanId)"
|
||||
@click="emit('subscribe', plan)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<span class="text-3xl font-bold text-gray-900">${{ plan.price }}</span>
|
||||
<span class="text-gray-500">/{{ plan.cycle }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-6">{{ plan.description }}</p>
|
||||
|
||||
<div class="space-y-2 mb-6 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
|
||||
<span>{{ formatBytes(plan.storage_limit || 0) }} storage</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
|
||||
<span>{{ plan.upload_limit }} uploads/day</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="i-heroicons-check-circle text-green-500 w-4 h-4" />
|
||||
<span>{{ plan.duration_limit }}s video duration</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
v-if="isAdmin"
|
||||
variant="outline"
|
||||
class="flex-1"
|
||||
@click="emit('edit', plan)"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
class="flex-1"
|
||||
:variant="plan.id === currentPlanId ? 'outline' : 'primary'"
|
||||
:loading="subscribingPlanId === plan.id"
|
||||
:disabled="plan.id === currentPlanId"
|
||||
@click="emit('subscribe', plan)"
|
||||
>
|
||||
{{ plan.id === currentPlanId ? 'Current Plan' : 'Subscribe' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,93 +1,78 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Tag from 'primevue/tag';
|
||||
import { createColumnHelper } from '@/components/table/Column'
|
||||
import DataTable from '@/components/table/DataTable.vue'
|
||||
import Tag from '@/components/ui/Tag.vue'
|
||||
import Toast from '@/components/ui/Toast.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { h } from 'vue'
|
||||
|
||||
interface PaymentHistoryItem {
|
||||
id: string;
|
||||
date: string;
|
||||
amount: number;
|
||||
plan: string;
|
||||
status: string;
|
||||
invoiceId: string;
|
||||
id: string
|
||||
date: string
|
||||
amount: number
|
||||
plan: string
|
||||
status: string
|
||||
invoiceId: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
history: PaymentHistoryItem[];
|
||||
}>();
|
||||
const props = defineProps<{
|
||||
history: PaymentHistoryItem[]
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const getStatusSeverity = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'failed':
|
||||
return 'danger';
|
||||
case 'pending':
|
||||
return 'warn';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import ArrowDownTray from '@/components/icons/ArrowDownTray.vue';
|
||||
|
||||
const toast = useToast();
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'success' as const
|
||||
case 'failed':
|
||||
return 'danger' as const
|
||||
case 'pending':
|
||||
return 'warning' as const
|
||||
default:
|
||||
return 'info' as const
|
||||
}
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<PaymentHistoryItem>()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('date', {
|
||||
header: 'Date',
|
||||
cell: ({ getValue }) => h('span', { class: 'font-medium' }, getValue()),
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.accessor('amount', {
|
||||
header: 'Amount',
|
||||
cell: ({ getValue }) => h('span', {}, `$${getValue()}`)
|
||||
}),
|
||||
columnHelper.accessor('plan', {
|
||||
header: 'Plan'
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: ({ getValue }) => h(Tag, {
|
||||
value: getValue(),
|
||||
severity: getStatusSeverity(getValue())
|
||||
})
|
||||
})
|
||||
]
|
||||
|
||||
const downloadInvoice = (item: PaymentHistoryItem) => {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Downloading',
|
||||
detail: `Downloading invoice #${item.invoiceId}...`,
|
||||
life: 2000
|
||||
});
|
||||
toast.info(`Downloading invoice #${item.invoiceId}...`, 'Downloading')
|
||||
|
||||
// Simulate download delay
|
||||
setTimeout(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Downloaded',
|
||||
detail: `Invoice #${item.invoiceId} downloaded successfully`,
|
||||
life: 3000
|
||||
});
|
||||
}, 1500);
|
||||
};
|
||||
setTimeout(() => {
|
||||
toast.success(`Invoice #${item.invoiceId} downloaded successfully`, 'Downloaded')
|
||||
}, 1500)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Billing History</h2>
|
||||
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<DataTable :value="history" responsiveLayout="scroll" class="w-full">
|
||||
<template #empty>
|
||||
<div class="text-center py-8 text-gray-500">No payment history found.</div>
|
||||
</template>
|
||||
<Column field="date" header="Date" class="font-medium"></Column>
|
||||
<Column field="amount" header="Amount">
|
||||
<template #body="slotProps">
|
||||
${{ slotProps.data.amount }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="plan" header="Plan"></Column>
|
||||
<Column field="status" header="Status">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.status" :severity="getStatusSeverity(slotProps.data.status)"
|
||||
class="capitalize px-2 py-0.5 text-xs" :rounded="true" />
|
||||
</template>
|
||||
</Column>
|
||||
<!-- <Column header="" style="width: 3rem">
|
||||
<template #body="slotProps">
|
||||
<Button text rounded severity="secondary" size="small" @click="downloadInvoice(slotProps.data)"
|
||||
v-tooltip="'Download Invoice'">
|
||||
<template #icon>
|
||||
<ArrowDownTray class="w-5 h-5" />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
</Column> -->
|
||||
</DataTable>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<Toast />
|
||||
<h2 class="text-2xl font-bold mb-6 text-gray-900">Billing History</h2>
|
||||
<div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
|
||||
<DataTable :data="history" :columns="columns" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue';
|
||||
import { formatBytes } from '@/lib/utils';
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
storageUsed: number;
|
||||
storageLimit: number;
|
||||
uploadsUsed: number;
|
||||
uploadsLimit: number;
|
||||
}>();
|
||||
storageUsed: number
|
||||
storageLimit: number
|
||||
uploadsUsed: number
|
||||
uploadsLimit: number
|
||||
}>()
|
||||
|
||||
const storagePercentage = computed(() => Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100));
|
||||
const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 100), 100));
|
||||
const storagePercentage = computed(() => Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100))
|
||||
const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 100), 100))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-8 flex flex-col justify-center">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-6">Usage Statistics</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600 font-medium">Storage</span>
|
||||
<span class="text-gray-900 font-bold">{{ storagePercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="storagePercentage" :showValue="false" style="height: 8px" :class="storagePercentage > 90 ? 'p-progressbar-danger' : ''"></ProgressBar>
|
||||
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600 font-medium">Monthly Uploads</span>
|
||||
<span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="uploadsPercentage" :showValue="false" style="height: 8px"></ProgressBar>
|
||||
<p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p>
|
||||
</div>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-8 flex flex-col justify-center">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-6">Usage Statistics</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600 font-medium">Storage</span>
|
||||
<span class="text-gray-900 font-bold">{{ storagePercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
:value="storagePercentage"
|
||||
:show-value="false"
|
||||
:color="storagePercentage > 90 ? 'danger' : 'primary'"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600 font-medium">Monthly Uploads</span>
|
||||
<span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="uploadsPercentage" :show-value="false" />
|
||||
<p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,106 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue';
|
||||
import ProfileHero from './components/ProfileHero.vue';
|
||||
import ProfileInfoCard from './components/ProfileInfoCard.vue';
|
||||
import ChangePasswordDialog from './components/ChangePasswordDialog.vue';
|
||||
import AccountStatusCard from './components/AccountStatusCard.vue';
|
||||
import LinkedAccountsCard from './components/LinkedAccountsCard.vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import PageHeader from '@/components/dashboard/PageHeader.vue'
|
||||
import Toast from '@/components/ui/Toast.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { computed, ref } from 'vue'
|
||||
import AccountStatusCard from './components/AccountStatusCard.vue'
|
||||
import ChangePasswordDialog from './components/ChangePasswordDialog.vue'
|
||||
import LinkedAccountsCard from './components/LinkedAccountsCard.vue'
|
||||
import ProfileHero from './components/ProfileHero.vue'
|
||||
import ProfileInfoCard from './components/ProfileInfoCard.vue'
|
||||
|
||||
const auth = useAuthStore();
|
||||
const toast = useToast();
|
||||
const auth = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// Dialog visibility
|
||||
const showPasswordDialog = ref(false);
|
||||
const showPasswordDialog = ref(false)
|
||||
|
||||
// Refs for dialog components
|
||||
const passwordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>();
|
||||
const passwordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>()
|
||||
|
||||
// Computed storage values
|
||||
const storageUsed = computed(() => auth.user?.storage_used || 0);
|
||||
const storageLimit = computed(() => 10737418240); // 10GB default
|
||||
const storageUsed = computed(() => auth.user?.storage_used || 0)
|
||||
const storageLimit = computed(() => 10737418240) // 10GB default
|
||||
|
||||
// Handlers
|
||||
const handleEditSave = async (data: { username: string; email: string }) => {
|
||||
try {
|
||||
await auth.updateProfile(data);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Profile Updated',
|
||||
detail: 'Your profile has been updated successfully.',
|
||||
life: 3000
|
||||
});
|
||||
} catch (e) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Update Failed',
|
||||
detail: auth.error || 'Failed to update profile.',
|
||||
life: 5000
|
||||
});
|
||||
}
|
||||
};
|
||||
try {
|
||||
await auth.updateProfile(data)
|
||||
toast.success('Your profile has been updated successfully.', 'Profile Updated')
|
||||
} catch (e) {
|
||||
toast.error(auth.error || 'Failed to update profile.', 'Update Failed')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasswordSave = async (data: { currentPassword: string; newPassword: string }) => {
|
||||
try {
|
||||
await auth.changePassword(data.currentPassword, data.newPassword);
|
||||
showPasswordDialog.value = false;
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Password Changed',
|
||||
detail: 'Your password has been changed successfully.',
|
||||
life: 3000
|
||||
});
|
||||
} catch (e: any) {
|
||||
passwordDialogRef.value?.setError(e.message || 'Failed to change password');
|
||||
}
|
||||
};
|
||||
try {
|
||||
await auth.changePassword(data.currentPassword, data.newPassword)
|
||||
showPasswordDialog.value = false
|
||||
toast.success('Your password has been changed successfully.', 'Password Changed')
|
||||
} catch (e: any) {
|
||||
passwordDialogRef.value?.setError(e.message || 'Failed to change password')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<PageHeader
|
||||
title="Profile Settings"
|
||||
description="Manage your account information and preferences."
|
||||
:breadcrumbs="[
|
||||
{ label: 'Dashboard', to: '/' },
|
||||
{ label: 'Profile' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="max-w-5xl mx-auto space-y-8 pb-12">
|
||||
<!-- Hero Identity Card -->
|
||||
<ProfileHero
|
||||
:user="auth.user"
|
||||
@logout="auth.logout()"
|
||||
/>
|
||||
<div class="profile-page">
|
||||
<Toast />
|
||||
<PageHeader
|
||||
title="Profile Settings"
|
||||
description="Manage your account information and preferences."
|
||||
:breadcrumbs="[
|
||||
{ label: 'Dashboard', to: '/' },
|
||||
{ label: 'Profile' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="max-w-5xl mx-auto space-y-8 pb-12">
|
||||
<!-- Hero Identity Card -->
|
||||
<ProfileHero
|
||||
:user="auth.user"
|
||||
@logout="auth.logout()"
|
||||
/>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Personal Info -->
|
||||
<div class="md:col-span-2">
|
||||
<ProfileInfoCard
|
||||
:user="auth.user"
|
||||
@change-password="showPasswordDialog = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Stats Side -->
|
||||
<div class="md:col-span-1 space-y-6">
|
||||
<AccountStatusCard
|
||||
:storage-used="storageUsed"
|
||||
:storage-limit="storageLimit"
|
||||
/>
|
||||
<LinkedAccountsCard />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<!-- Personal Info -->
|
||||
<div class="md:col-span-2">
|
||||
<ProfileInfoCard
|
||||
:user="auth.user"
|
||||
@change-password="showPasswordDialog = true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<ChangePasswordDialog
|
||||
ref="passwordDialogRef"
|
||||
v-model:visible="showPasswordDialog"
|
||||
@save="handlePasswordSave"
|
||||
/>
|
||||
<!-- Stats Side -->
|
||||
<div class="md:col-span-1 space-y-6">
|
||||
<AccountStatusCard
|
||||
:storage-used="storageUsed"
|
||||
:storage-limit="storageLimit"
|
||||
/>
|
||||
<LinkedAccountsCard />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialogs -->
|
||||
<ChangePasswordDialog
|
||||
ref="passwordDialogRef"
|
||||
v-model:visible="showPasswordDialog"
|
||||
@save="handlePasswordSave"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import ProgressBar from 'primevue/progressbar';
|
||||
import ProgressBar from '@/components/ui/ProgressBar.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
storageUsed: number;
|
||||
storageLimit: number;
|
||||
}>();
|
||||
interface Props {
|
||||
storageUsed?: number
|
||||
storageTotal?: number
|
||||
}
|
||||
|
||||
const storagePercentage = computed(() =>
|
||||
Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100)
|
||||
);
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
storageUsed: 0,
|
||||
storageTotal: 100
|
||||
})
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
const usagePercentage = computed(() => {
|
||||
if (props.storageTotal === 0) return 0
|
||||
return Math.round((props.storageUsed / props.storageTotal) * 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">Account Status</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600">Storage Used</span>
|
||||
<span class="font-bold text-gray-900">{{ storagePercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="storagePercentage" :showValue="false" style="height: 6px"></ProgressBar>
|
||||
<p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
|
||||
</div>
|
||||
<div class="bg-green-50 rounded-lg p-4 border border-green-100 flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-green-600 mt-0.5 shrink-0" 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>
|
||||
<div>
|
||||
<h4 class="font-bold text-green-800 text-sm">Account Active</h4>
|
||||
<p class="text-green-600 text-xs mt-0.5">Your subscription is in good standing.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-6 border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Account Status</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span class="text-gray-600">Storage Usage</span>
|
||||
<span class="font-medium text-gray-900">{{ usagePercentage }}%</span>
|
||||
</div>
|
||||
<ProgressBar :value="usagePercentage" :show-value="false" />
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
{{ storageUsed }} GB of {{ storageTotal }} GB used
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,102 +1,132 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Button from 'primevue/button';
|
||||
import Message from 'primevue/message';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Button from '@/components/ui/Button.vue';
|
||||
import Dialog from '@/components/ui/Dialog.vue';
|
||||
import Input from '@/components/ui/Input.vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
}>();
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
save: [data: { currentPassword: string; newPassword: string }];
|
||||
}>();
|
||||
'update:visible': [value: boolean]
|
||||
save: [data: { currentPassword: string; newPassword: string }]
|
||||
}>()
|
||||
|
||||
const currentPassword = ref('');
|
||||
const newPassword = ref('');
|
||||
const confirmPassword = ref('');
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const currentPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
currentPassword.value = '';
|
||||
newPassword.value = '';
|
||||
confirmPassword.value = '';
|
||||
error.value = '';
|
||||
}
|
||||
});
|
||||
if (val) {
|
||||
currentPassword.value = ''
|
||||
newPassword.value = ''
|
||||
confirmPassword.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
const isValid = computed(() => {
|
||||
return currentPassword.value.length >= 1
|
||||
&& newPassword.value.length >= 6
|
||||
&& newPassword.value === confirmPassword.value;
|
||||
});
|
||||
return currentPassword.value.length >= 1
|
||||
&& newPassword.value.length >= 6
|
||||
&& newPassword.value === confirmPassword.value
|
||||
})
|
||||
|
||||
const passwordMismatch = computed(() => {
|
||||
return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value;
|
||||
});
|
||||
return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value
|
||||
})
|
||||
|
||||
const passwordTooShort = computed(() => {
|
||||
return newPassword.value.length > 0 && newPassword.value.length < 6;
|
||||
});
|
||||
return newPassword.value.length > 0 && newPassword.value.length < 6
|
||||
})
|
||||
|
||||
const newPasswordInvalidClass = computed(() => passwordTooShort.value ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')
|
||||
const confirmPasswordInvalidClass = computed(() => passwordMismatch.value ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '')
|
||||
|
||||
const handleSave = () => {
|
||||
if (!isValid.value) return;
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
emit('save', {
|
||||
currentPassword: currentPassword.value,
|
||||
newPassword: newPassword.value
|
||||
});
|
||||
};
|
||||
if (!isValid.value) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
emit('save', {
|
||||
currentPassword: currentPassword.value,
|
||||
newPassword: newPassword.value
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// Expose methods for parent to control loading state
|
||||
defineExpose({
|
||||
setLoading: (val: boolean) => { loading.value = val; },
|
||||
setError: (msg: string) => { error.value = msg; loading.value = false; }
|
||||
});
|
||||
setLoading: (val: boolean) => { loading.value = val },
|
||||
setError: (msg: string) => { error.value = msg; loading.value = false }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal header="Change Password"
|
||||
:style="{ width: '28rem' }" :closable="true" :draggable="false">
|
||||
<div class="space-y-6 pt-2">
|
||||
<Message v-if="error" severity="error" :closable="false">{{ error }}</Message>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="current-password" class="text-sm font-medium text-gray-700">Current Password</label>
|
||||
<InputText id="current-password" v-model="currentPassword" type="password" class="w-full"
|
||||
placeholder="Enter current password" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="new-password" class="text-sm font-medium text-gray-700">New Password</label>
|
||||
<InputText id="new-password" v-model="newPassword" type="password" class="w-full"
|
||||
placeholder="Enter new password (min 6 characters)"
|
||||
:class="{ 'p-invalid': passwordTooShort }" />
|
||||
<small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="confirm-password" class="text-sm font-medium text-gray-700">Confirm New Password</label>
|
||||
<InputText id="confirm-password" v-model="confirmPassword" type="password" class="w-full"
|
||||
placeholder="Confirm new password"
|
||||
:class="{ 'p-invalid': passwordMismatch }" />
|
||||
<small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<Button label="Cancel" severity="secondary" @click="handleClose" :disabled="loading" />
|
||||
<Button label="Change Password" @click="handleSave" :loading="loading" :disabled="!isValid" />
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
<Dialog
|
||||
:visible="visible"
|
||||
header="Change Password"
|
||||
width="28rem"
|
||||
:closable="true"
|
||||
:draggable="false"
|
||||
@update:visible="handleClose"
|
||||
>
|
||||
<div class="space-y-6 pt-2">
|
||||
<div
|
||||
v-if="error"
|
||||
class="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="current-password" class="text-sm font-medium text-gray-700">Current Password</label>
|
||||
<Input
|
||||
id="current-password"
|
||||
v-model="currentPassword"
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="new-password" class="text-sm font-medium text-gray-700">New Password</label>
|
||||
<Input
|
||||
id="new-password"
|
||||
v-model="newPassword"
|
||||
type="password"
|
||||
placeholder="Enter new password (min 6 characters)"
|
||||
:class="newPasswordInvalidClass"
|
||||
/>
|
||||
<small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="confirm-password" class="text-sm font-medium text-gray-700">Confirm New Password</label>
|
||||
<Input
|
||||
id="confirm-password"
|
||||
v-model="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
:class="confirmPasswordInvalidClass"
|
||||
/>
|
||||
<small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<Button variant="outline" :disabled="loading" @click="handleClose">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button :loading="loading" :disabled="!isValid" @click="handleSave">
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import Tag from 'primevue/tag';
|
||||
import Tag from '@/components/ui/Tag.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">Linked Accounts</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:border-gray-200 transition-colors">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 text-red-600" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="currentColor" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="currentColor" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="currentColor" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="font-medium text-gray-700">Google</span>
|
||||
</div>
|
||||
<Tag value="Connected" severity="success" class="text-xs px-2"></Tag>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-6 border border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Linked Accounts</h3>
|
||||
<div class="space-y-4">
|
||||
<!-- Placeholder for linked accounts -->
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<span class="i-heroicons-envelope text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-gray-900">Email</p>
|
||||
<p class="text-sm text-gray-500">Connected</p>
|
||||
</div>
|
||||
</div>
|
||||
<Tag value="Active" severity="success" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,83 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelUser } from '@/api/client';
|
||||
import Avatar from 'primevue/avatar';
|
||||
import Button from 'primevue/button';
|
||||
import Tag from 'primevue/tag';
|
||||
import { computed } from 'vue';
|
||||
import type { ModelUser } from '@/api/client'
|
||||
import Avatar from '@/components/ui/Avatar.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Tag from '@/components/ui/Tag.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
user: ModelUser | null;
|
||||
}>();
|
||||
interface Props {
|
||||
user: ModelUser | null
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
logout: [];
|
||||
changePassword: [];
|
||||
}>();
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const joinDate = computed(() => {
|
||||
return new Date(props.user?.created_at || Date.now()).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
});
|
||||
const displayName = computed(() => {
|
||||
return props.user?.username || 'User'
|
||||
})
|
||||
|
||||
const displayEmail = computed(() => {
|
||||
return props.user?.email || 'No email'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative overflow-hidden rounded-2xl bg-gradient-to-r from-gray-900 via-gray-800 to-gray-900 text-white p-8 md:p-10">
|
||||
<!-- Background decorations -->
|
||||
<div class="absolute top-0 right-0 -mt-20 -mr-20 w-80 h-80 bg-primary-500 rounded-full mix-blend-overlay filter blur-3xl"></div>
|
||||
<div class="absolute bottom-0 left-0 -mb-20 -ml-20 w-80 h-80 bg-purple-500 rounded-full mix-blend-overlay filter blur-3xl"></div>
|
||||
|
||||
<div class="relative z-10 flex flex-col md:flex-row items-center gap-8">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 bg-primary-500 rounded-full blur-lg opacity-40"></div>
|
||||
<!-- :label="user?.username?.charAt(0).toUpperCase() || 'U'" -->
|
||||
<Avatar
|
||||
class="relative border-4 border-gray-800 text-3xl font-bold bg-gradient-to-br from-primary-400 to-primary-600 text-white shadow-2xl"
|
||||
size="xlarge"
|
||||
shape="circle"
|
||||
style="width: 120px; height: 120px; font-size: 3rem;"
|
||||
image="https://picsum.photos/seed/user123/120/120.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-center md:text-left space-y-2 flex-grow">
|
||||
<div class="flex flex-col md:flex-row items-center gap-3 justify-center md:justify-start">
|
||||
<h2 class="text-3xl font-bold text-white">{{ user?.username || 'User' }}</h2>
|
||||
<Tag :value="user?.role || 'User'" severity="info" class="uppercase tracking-wider px-2 header-tag" rounded></Tag>
|
||||
</div>
|
||||
<p class="text-gray-400 text-lg">{{ user?.email }}</p>
|
||||
<p class="text-gray-500 text-sm flex items-center justify-center md:justify-start gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/>
|
||||
<line x1="16" x2="16" y1="2" y2="6"/>
|
||||
<line x1="8" x2="8" y1="2" y2="6"/>
|
||||
<line x1="3" x2="21" y1="10" y2="10"/>
|
||||
</svg>
|
||||
Member since {{ joinDate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<Button label="Logout" severity="danger" class="border-white/10 text-white hover:bg-white/10 bg-white/5" @click="emit('logout')">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
|
||||
<polyline points="16 17 21 12 16 7"/>
|
||||
<line x1="21" x2="9" y1="12" y2="12"/>
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-6 text-white">
|
||||
<div class="flex items-center gap-4">
|
||||
<Avatar
|
||||
:label="displayName"
|
||||
size="xl"
|
||||
shape="circle"
|
||||
class="border-4 border-white/30"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<h2 class="text-2xl font-bold">{{ displayName }}</h2>
|
||||
<Tag value="Active" severity="success" class="!bg-white/20 !text-white !border-white/30" />
|
||||
</div>
|
||||
<p class="text-white/80">{{ displayEmail }}</p>
|
||||
</div>
|
||||
<Button variant="ghost" class="!text-white hover:!bg-white/20">
|
||||
Edit Profile
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.header-tag) {
|
||||
background: rgba(255,255,255,0.2) !important;
|
||||
color: white !important;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelUser } from '@/api/client';
|
||||
import Button from 'primevue/button';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import type { ModelUser } from '@/api/client'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
user: ModelUser | null;
|
||||
}>();
|
||||
interface Props {
|
||||
user: ModelUser | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
edit: [];
|
||||
changePassword: [];
|
||||
}>();
|
||||
save: [data: { username: string; email: string }]
|
||||
}>()
|
||||
|
||||
const isEditing = ref(false)
|
||||
const username = ref('')
|
||||
const email = ref('')
|
||||
|
||||
watch(() => props.user, (newUser) => {
|
||||
if (newUser) {
|
||||
username.value = newUser.username || ''
|
||||
email.value = newUser.email || ''
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const handleSave = () => {
|
||||
emit('save', { username: username.value, email: email.value })
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
username.value = props.user?.username || ''
|
||||
email.value = props.user?.email || ''
|
||||
isEditing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white border border-gray-200 rounded-2xl p-8">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Personal Information</h3>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Change Password" text severity="secondary" @click="emit('changePassword')">
|
||||
<template #icon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username" class="text-sm font-medium text-gray-700">Username</label>
|
||||
<div class="relative">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
</InputIcon>
|
||||
<InputText id="username" :value="user?.username" class="w-full pl-10" readonly />
|
||||
</IconField>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email" class="text-sm font-medium text-gray-700">Email Address</label>
|
||||
<div class="relative">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" 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>
|
||||
</InputIcon>
|
||||
<InputText id="email" :value="user?.email" class="w-full pl-10" readonly />
|
||||
</IconField>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<!-- <div class="flex flex-col gap-2">
|
||||
<label for="role" class="text-sm font-medium text-gray-700">Role</label>
|
||||
<InputText id="role" :value="user?.role || 'User'" class="w-full capitalize bg-gray-50" readonly />
|
||||
</div> -->
|
||||
<!-- <div class="flex flex-col gap-2">
|
||||
<label for="id" class="text-sm font-medium text-gray-700">User ID</label>
|
||||
<InputText id="id" :value="user?.id || 'N/A'" class="w-full font-mono text-sm bg-gray-50" readonly />
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-6 border border-gray-200">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Profile Information</h3>
|
||||
<Button
|
||||
v-if="!isEditing"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="isEditing = true"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-inputtext[readonly]) {
|
||||
background-color: #f9fafb;
|
||||
border-color: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
</style>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<Input
|
||||
v-if="isEditing"
|
||||
v-model="username"
|
||||
placeholder="Enter username"
|
||||
/>
|
||||
<p v-else class="text-gray-900">{{ user?.username || 'Not set' }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<Input
|
||||
v-if="isEditing"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="Enter email"
|
||||
/>
|
||||
<p v-else class="text-gray-900">{{ user?.email || 'Not set' }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditing" class="flex gap-2 pt-2">
|
||||
<Button size="sm" variant="outline" @click="handleCancel">Cancel</Button>
|
||||
<Button size="sm" @click="handleSave">Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,81 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import type { ModelVideo } from '@/api/client';
|
||||
import { formatDuration, formatDate, getStatusClass } from '@/lib/utils';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Card from 'primevue/card';
|
||||
import type { ModelVideo } from '@/api/client'
|
||||
import Checkbox from '@/components/ui/Checkbox.vue'
|
||||
import { formatDate, formatDuration, getStatusClass } from '@/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
videos: ModelVideo[];
|
||||
selectedVideos: ModelVideo[];
|
||||
}>();
|
||||
interface Props {
|
||||
videos: ModelVideo[]
|
||||
selectedVideos: ModelVideo[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedVideos', value: ModelVideo[]): void;
|
||||
(e: 'delete', videoId: string): void;
|
||||
}>();
|
||||
(e: 'update:selectedVideos', value: ModelVideo[]): void
|
||||
(e: 'delete', videoId: string): void
|
||||
}>()
|
||||
|
||||
const isSelected = (video: ModelVideo) => {
|
||||
return props.selectedVideos.some(v => v.id === video.id)
|
||||
}
|
||||
|
||||
const toggleSelection = (video: ModelVideo) => {
|
||||
const newSelection = isSelected(video)
|
||||
? props.selectedVideos.filter(v => v.id !== video.id)
|
||||
: [...props.selectedVideos, video]
|
||||
emit('update:selectedVideos', newSelection)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
<Card v-for="video in videos" :key="video.id"
|
||||
class="overflow-hidden shadow-sm hover:shadow-md transition-shadow group relative border border-gray-200"
|
||||
:class="{ '!border-primary ring-2 ring-primary': selectedVideos.some(v => v.id === video.id) }">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
<div
|
||||
v-for="video in videos"
|
||||
:key="video.id"
|
||||
class="bg-white rounded-xl border overflow-hidden shadow-sm hover:shadow-md transition-shadow group relative"
|
||||
:class="isSelected(video) ? 'border-primary ring-2 ring-primary' : 'border-gray-200'"
|
||||
>
|
||||
<!-- Header/Thumbnail -->
|
||||
<div
|
||||
class="aspect-video bg-gray-200 relative overflow-hidden group-hover:opacity-95 transition-opacity"
|
||||
>
|
||||
<!-- Grid Selection Checkbox -->
|
||||
<div
|
||||
class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:class="{ 'opacity-100': isSelected(video) }"
|
||||
>
|
||||
<Checkbox
|
||||
:model-value="isSelected(video)"
|
||||
:binary="true"
|
||||
@click="toggleSelection(video)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #header>
|
||||
<div
|
||||
class="aspect-video bg-gray-200 relative overflow-hidden group-hover:opacity-95 transition-opacity">
|
||||
<!-- Grid Selection Checkbox -->
|
||||
<div class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
:class="{ 'opacity-100': selectedVideos.some(v => v.id === video.id) }">
|
||||
<Checkbox :modelValue="selectedVideos" :value="video"
|
||||
@update:modelValue="emit('update:selectedVideos', $event)" />
|
||||
</div>
|
||||
<img
|
||||
v-if="video.thumbnail"
|
||||
:src="video.thumbnail"
|
||||
:alt="video.title"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="w-full h-full flex items-center justify-center text-gray-400"
|
||||
>
|
||||
<span class="i-heroicons-film text-3xl" />
|
||||
</div>
|
||||
|
||||
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title"
|
||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center text-gray-400">
|
||||
<span class="i-heroicons-film text-3xl" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
||||
</div>
|
||||
<span
|
||||
class="absolute bottom-1.5 right-1.5 bg-black/70 text-white text-[10px] font-medium px-1.5 py-0.5 rounded"
|
||||
>
|
||||
{{ formatDuration(video.duration) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
class="absolute bottom-1.5 right-1.5 bg-black/70 text-white text-[10px] font-medium px-1.5 py-0.5 rounded">
|
||||
{{ formatDuration(video.duration) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Content -->
|
||||
<div class="flex flex-col h-full p-4">
|
||||
<div class="flex items-start justify-between gap-2 mb-1">
|
||||
<h3
|
||||
class="font-medium text-sm text-gray-900 line-clamp-2 leading-snug flex-1"
|
||||
:title="video.title"
|
||||
>
|
||||
{{ video.title }}
|
||||
</h3>
|
||||
<button class="text-gray-400 hover:text-gray-700">
|
||||
<span class="i-heroicons-ellipsis-vertical w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex items-start justify-between gap-2 mb-1">
|
||||
<h3 class="font-medium text-sm text-gray-900 line-clamp-2 leading-snug flex-1"
|
||||
:title="video.title">
|
||||
{{ video.title }}
|
||||
</h3>
|
||||
<button class="text-gray-400 hover:text-gray-700">
|
||||
<span class="i-heroicons-ellipsis-vertical w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">
|
||||
{{ video.description || 'No description' }}
|
||||
</p>
|
||||
|
||||
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">{{ video.description || 'No description' }}
|
||||
</p>
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<span
|
||||
:class="[
|
||||
'px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider',
|
||||
getStatusClass(video.status)
|
||||
]"
|
||||
>
|
||||
{{ video.status }}
|
||||
</span>
|
||||
|
||||
<div class="mt-auto flex items-center justify-between">
|
||||
<span
|
||||
:class="['px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider', getStatusClass(video.status)]">
|
||||
{{ video.status }}
|
||||
</span>
|
||||
|
||||
<div class="text-[10px] text-gray-400">
|
||||
{{ formatDate(video.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div class="text-[10px] text-gray-400">
|
||||
{{ formatDate(video.created_at) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,99 +1,142 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import type { ModelVideo } from '@/api/client';
|
||||
import { formatDuration, formatDate, formatBytes, getStatusClass } from '@/lib/utils';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import type { ModelVideo } from '@/api/client'
|
||||
import { createColumnHelper } from '@/components/table/Column'
|
||||
import DataTable from '@/components/table/DataTable.vue'
|
||||
import Checkbox from '@/components/ui/Checkbox.vue'
|
||||
import { formatBytes, formatDate, formatDuration, getStatusClass } from '@/lib/utils'
|
||||
import { h } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
videos: ModelVideo[];
|
||||
selectedVideos: ModelVideo[];
|
||||
}>();
|
||||
interface Props {
|
||||
videos: ModelVideo[]
|
||||
selectedVideos: ModelVideo[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selectedVideos', value: ModelVideo[]): void;
|
||||
(e: 'delete', videoId: string): void;
|
||||
}>();
|
||||
(e: 'update:selectedVideos', value: ModelVideo[]): void
|
||||
(e: 'delete', videoId: string): void
|
||||
}>()
|
||||
|
||||
const columnHelper = createColumnHelper<ModelVideo>()
|
||||
|
||||
const isSelected = (video: ModelVideo) => {
|
||||
return props.selectedVideos.some(v => v.id === video.id)
|
||||
}
|
||||
|
||||
const toggleAll = () => {
|
||||
const allSelected = props.videos.length > 0 && props.videos.every(v => isSelected(v))
|
||||
const newSelection = allSelected ? [] : [...props.videos]
|
||||
emit('update:selectedVideos', newSelection)
|
||||
}
|
||||
|
||||
const allSelected = () => props.videos.length > 0 && props.videos.every(v => isSelected(v))
|
||||
const someSelected = () => props.videos.some(v => isSelected(v)) && !allSelected()
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: 'select',
|
||||
header: () => h('div', {
|
||||
class: 'flex justify-center'
|
||||
}, h(Checkbox, {
|
||||
modelValue: allSelected(),
|
||||
binary: true,
|
||||
onClick: toggleAll
|
||||
})),
|
||||
cell: ({ row }) => h('div', {
|
||||
class: 'flex justify-center'
|
||||
}, h(Checkbox, {
|
||||
modelValue: isSelected(row.original),
|
||||
binary: true,
|
||||
onClick: () => {
|
||||
const newSelection = isSelected(row.original)
|
||||
? props.selectedVideos.filter(v => v.id !== row.original.id)
|
||||
: [...props.selectedVideos, row.original]
|
||||
emit('update:selectedVideos', newSelection)
|
||||
}
|
||||
})),
|
||||
size: 50
|
||||
}),
|
||||
columnHelper.accessor('title', {
|
||||
header: 'Video',
|
||||
cell: ({ row }) => h('div', { class: 'flex items-center gap-3' }, [
|
||||
h('div', { class: 'w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0' }, [
|
||||
row.original.thumbnail
|
||||
? h('img', {
|
||||
src: row.original.thumbnail,
|
||||
alt: row.original.title,
|
||||
class: 'w-full h-full object-cover'
|
||||
})
|
||||
: h('div', { class: 'w-full h-full flex items-center justify-center' }, [
|
||||
h('span', { class: 'i-heroicons-film text-gray-400 text-xl' })
|
||||
])
|
||||
]),
|
||||
h('div', { class: 'min-w-0 flex-1' }, [
|
||||
h('p', { class: 'font-medium text-gray-900 truncate' }, row.original.title),
|
||||
h('p', { class: 'text-sm text-gray-500 truncate' }, row.original.description || 'No description')
|
||||
])
|
||||
]),
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: ({ getValue }) => {
|
||||
const status = getValue() || 'Unknown'
|
||||
return h('span', {
|
||||
class: `px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap ${getStatusClass(status)}`
|
||||
}, status)
|
||||
},
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.accessor('duration', {
|
||||
header: 'Duration',
|
||||
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatDuration(getValue())),
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.accessor('size', {
|
||||
header: 'Size',
|
||||
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatBytes(getValue())),
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.accessor('created_at', {
|
||||
header: 'Upload Date',
|
||||
cell: ({ getValue }) => h('span', { class: 'text-sm text-gray-500' }, formatDate(getValue())),
|
||||
enableSorting: true
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => h('div', { class: 'flex items-center gap-1' }, [
|
||||
h('button', {
|
||||
class: 'p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors',
|
||||
title: 'Download'
|
||||
}, h('span', { class: 'i-heroicons-arrow-down-tray w-4 h-4' })),
|
||||
h('button', {
|
||||
class: 'p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors',
|
||||
title: 'Copy Link'
|
||||
}, h('span', { class: 'i-heroicons-link w-4 h-4' })),
|
||||
h('div', { class: 'w-px h-3 bg-gray-200 mx-1' }),
|
||||
h('button', {
|
||||
class: 'p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors',
|
||||
title: 'Edit'
|
||||
}, h('span', { class: 'i-heroicons-pencil w-4 h-4' })),
|
||||
h('button', {
|
||||
class: 'p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors',
|
||||
title: 'Delete',
|
||||
onClick: () => row.original.id && emit('delete', row.original.id)
|
||||
}, h('span', { class: 'i-heroicons-trash w-4 h-4' }))
|
||||
]),
|
||||
size: 150
|
||||
})
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<DataTable :value="videos" dataKey="id" tableStyle="min-width: 50rem" :selection="selectedVideos"
|
||||
@update:selection="emit('update:selectedVideos', $event)">
|
||||
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
|
||||
|
||||
<Column header="Video">
|
||||
<template #body="{ data }">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-20 h-12 bg-gray-200 rounded overflow-hidden flex-shrink-0">
|
||||
<img v-if="data.thumbnail" :src="data.thumbnail" :alt="data.title"
|
||||
class="w-full h-full object-cover" />
|
||||
<div v-else class="w-full h-full flex items-center justify-center">
|
||||
<span class="i-heroicons-film text-gray-400 text-xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-medium text-gray-900 truncate">{{ data.title }}</p>
|
||||
<p class="text-sm text-gray-500 truncate">{{ data.description || 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Status">
|
||||
<template #body="{ data }">
|
||||
<span
|
||||
:class="['px-2 py-1 text-xs font-medium rounded-full whitespace-nowrap', getStatusClass(data.status)]">
|
||||
{{ data.status || 'Unknown' }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Duration">
|
||||
<template #body="{ data }">
|
||||
<span class="text-sm text-gray-500">{{ formatDuration(data.duration) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Size">
|
||||
<template #body="{ data }">
|
||||
<span class="text-sm text-gray-500">{{ formatBytes(data.size) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Upload Date">
|
||||
<template #body="{ data }">
|
||||
<span class="text-sm text-gray-500">{{ formatDate(data.created_at) }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Actions">
|
||||
<template #body="{ data }">
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors"
|
||||
title="Download">
|
||||
<span class="i-heroicons-arrow-down-tray w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 text-gray-400 hover:text-primary hover:bg-primary/5 rounded transition-colors"
|
||||
title="Copy Link">
|
||||
<span class="i-heroicons-link w-4 h-4" />
|
||||
</button>
|
||||
<div class="w-px h-3 bg-gray-200 mx-1"></div>
|
||||
<button
|
||||
class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||
title="Edit">
|
||||
<span class="i-heroicons-pencil w-4 h-4" />
|
||||
</button>
|
||||
<button @click="emit('delete', data.id)"
|
||||
class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||
title="Delete">
|
||||
<span class="i-heroicons-trash w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<DataTable
|
||||
:data="videos"
|
||||
:columns="columns"
|
||||
:enable-sorting="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { cloudflare } from "@cloudflare/vite-plugin";
|
||||
import { PrimeVueResolver } from "@primevue/auto-import-resolver";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||
import path from "node:path";
|
||||
import unocss from "unocss/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import ssrPlugin from "./ssrPlugin";
|
||||
|
||||
export default defineConfig((env) => {
|
||||
// console.log("env:", env, import.meta.env);
|
||||
return {
|
||||
plugins: [
|
||||
unocss(),
|
||||
vue(),
|
||||
vueJsx(),
|
||||
AutoImport({
|
||||
imports: ["vue", "vue-router", "pinia"], // Common presets
|
||||
dts: true, // Generate TypeScript declaration file
|
||||
imports: ["vue", "vue-router", "pinia"],
|
||||
dts: true,
|
||||
}),
|
||||
Components({
|
||||
dirs: ["src/components"],
|
||||
@@ -25,7 +24,6 @@ export default defineConfig((env) => {
|
||||
dts: true,
|
||||
dtsTsx: true,
|
||||
directives: false,
|
||||
resolvers: [PrimeVueResolver()],
|
||||
}),
|
||||
ssrPlugin(),
|
||||
cloudflare(),
|
||||
@@ -33,13 +31,11 @@ export default defineConfig((env) => {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
// "httpClientAdapter": path.resolve(__dirname, "./src/api/httpClientAdapter.server.ts")
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["vue"],
|
||||
},
|
||||
|
||||
ssr: {
|
||||
noExternal: ["vue"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user