1 Commits

Author SHA1 Message Date
d0176fb48b ai-vibe 2026-02-05 15:59:18 +07:00
57 changed files with 2728 additions and 3558 deletions

400
bun.lock
View File

@@ -10,19 +10,17 @@
"@aws-sdk/s3-request-presigner": "^3.971.0", "@aws-sdk/s3-request-presigner": "^3.971.0",
"@hiogawa/tiny-rpc": "^0.2.3-pre.18", "@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@hiogawa/utils": "^1.7.0", "@hiogawa/utils": "^1.7.0",
"@primeuix/themes": "^2.0.3", "@tanstack/vue-form": "^1.28.0",
"@primevue/forms": "^4.5.4", "@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.1.2", "@unhead/vue": "^2.1.2",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"firebase-admin": "^13.6.0",
"hono": "^4.11.4", "hono": "^4.11.4",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"primevue": "^4.5.4",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"vue": "^3.5.27", "vue": "^3.5.27",
"vue-router": "^4.6.4", "vue-router": "^5.0.2",
"zod": "^4.3.5", "zod": "^4.3.5",
}, },
"devDependencies": { "devDependencies": {
@@ -255,40 +253,6 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], "@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/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=="], "@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/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=="], "@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=="],
"@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=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@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=="], "@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/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=="], "@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=="], "@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=="], "@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=="], "@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/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/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=="], "@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=="], "@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=="], "@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-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=="], "@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=="], "@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=="], "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=="], "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=="],
"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=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
@@ -769,38 +651,18 @@
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "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=="], "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "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=="], "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=="], "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=="], "entities": ["entities@7.0.0", "", {}, "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ=="],
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], "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=="], "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=="], "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=="], "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=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], "fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
"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=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "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=="], "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=="], "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=="], "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=="], "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=="], "hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], "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-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=="], "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=="], "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=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="],
"stubs": ["stubs@3.0.0", "", {}, "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw=="],
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], "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=="], "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=="], "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=="], "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=="], "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=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "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=="], "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": ["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=="], "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": ["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-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=="], "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=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "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=="], "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=="], "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=="], "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=="], "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=="], "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"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=="],
"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=="], "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-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=="], "@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=="],
"@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=="],
"@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], "@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=="], "@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=="], "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=="], "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=="], "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/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="],
"unconfig-core/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=="], "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-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=="], "@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=="], "@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=="], "@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=="], "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=="], "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/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=="], "@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
View File

@@ -16,10 +16,12 @@ declare module 'vue' {
AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default'] AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.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'] 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'] 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'] CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.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'] CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
DashboardNav: typeof import('./src/components/DashboardNav.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'] 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'] GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
Home: typeof import('./src/components/icons/Home.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'] InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
InputIcon: typeof import('primevue/inputicon')['default'] Input: typeof import('./src/components/ui/Input.vue')['default']
InputText: typeof import('primevue/inputtext')['default'] InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
Layout: typeof import('./src/components/icons/Layout.vue')['default'] Layout: typeof import('./src/components/icons/Layout.vue')['default']
LinkIcon: typeof import('./src/components/icons/LinkIcon.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'] NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
PageHeader: typeof import('./src/components/dashboard/PageHeader.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'] 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'] RootLayout: typeof import('./src/components/RootLayout.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('primevue/select')['default']
SettingsIcon: typeof import('./src/components/icons/SettingsIcon.vue')['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'] 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'] 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'] TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
Upload: typeof import('./src/components/icons/Upload.vue')['default'] Upload: typeof import('./src/components/icons/Upload.vue')['default']
Video: typeof import('./src/components/icons/Video.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 AlertTriangleIcon: typeof import('./src/components/icons/AlertTriangleIcon.vue')['default']
const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default'] const ArrowDownTray: typeof import('./src/components/icons/ArrowDownTray.vue')['default']
const ArrowRightIcon: typeof import('./src/components/icons/ArrowRightIcon.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 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 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 CheckCircleIcon: typeof import('./src/components/icons/CheckCircleIcon.vue')['default']
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default'] const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.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 CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
const DashboardNav: typeof import('./src/components/DashboardNav.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 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 GlobalUploadIndicator: typeof import('./src/components/GlobalUploadIndicator.vue')['default']
const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default'] const HardDriveUpload: typeof import('./src/components/icons/HardDriveUpload.vue')['default']
const Home: typeof import('./src/components/icons/Home.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 InfoIcon: typeof import('./src/components/icons/InfoIcon.vue')['default']
const InputIcon: typeof import('primevue/inputicon')['default'] const Input: typeof import('./src/components/ui/Input.vue')['default']
const InputText: typeof import('primevue/inputtext')['default'] const InputPassword: typeof import('./src/components/ui/InputPassword.vue')['default']
const Layout: typeof import('./src/components/icons/Layout.vue')['default'] const Layout: typeof import('./src/components/icons/Layout.vue')['default']
const LinkIcon: typeof import('./src/components/icons/LinkIcon.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 NotificationDrawer: typeof import('./src/components/NotificationDrawer.vue')['default']
const PageHeader: typeof import('./src/components/dashboard/PageHeader.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 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 RootLayout: typeof import('./src/components/RootLayout.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] 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 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 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 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 TrashIcon: typeof import('./src/components/icons/TrashIcon.vue')['default']
const Upload: typeof import('./src/components/icons/Upload.vue')['default'] const Upload: typeof import('./src/components/icons/Upload.vue')['default']
const Video: typeof import('./src/components/icons/Video.vue')['default'] const Video: typeof import('./src/components/icons/Video.vue')['default']

View File

@@ -15,24 +15,21 @@
"@aws-sdk/s3-request-presigner": "^3.971.0", "@aws-sdk/s3-request-presigner": "^3.971.0",
"@hiogawa/tiny-rpc": "^0.2.3-pre.18", "@hiogawa/tiny-rpc": "^0.2.3-pre.18",
"@hiogawa/utils": "^1.7.0", "@hiogawa/utils": "^1.7.0",
"@primeuix/themes": "^2.0.3", "@tanstack/vue-form": "^1.28.0",
"@primevue/forms": "^4.5.4", "@tanstack/vue-table": "^8.21.3",
"@unhead/vue": "^2.1.2", "@unhead/vue": "^2.1.2",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"firebase-admin": "^13.6.0",
"hono": "^4.11.4", "hono": "^4.11.4",
"is-mobile": "^5.0.0", "is-mobile": "^5.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"primevue": "^4.5.4",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"vue": "^3.5.27", "vue": "^3.5.27",
"vue-router": "^4.6.4", "vue-router": "^5.0.2",
"zod": "^4.3.5" "zod": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/vite-plugin": "^1.21.0", "@cloudflare/vite-plugin": "^1.21.0",
"@primevue/auto-import-resolver": "^4.5.4",
"@types/node": "^25.0.9", "@types/node": "^25.0.9",
"@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue": "^6.0.3",
"@vitejs/plugin-vue-jsx": "^5.1.3", "@vitejs/plugin-vue-jsx": "^5.1.3",

View File

@@ -1,12 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import Bell from "@/components/icons/Bell.vue"; 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 Credit from "@/components/icons/Credit.vue";
import Home from "@/components/icons/Home.vue";
import Upload from "@/components/icons/Upload.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 { cn } from "@/lib/utils";
import { createStaticVNode, ref } from "vue"; 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 className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1); const 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"> <template v-for="i in links" :key="i.label">
<component :name="i.label" :is="i.type === 'a' ? 'router-link' : 'div'" <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( :class="cn(
i.className, i.className,
($route.path === i.href || i.isActive?.value) && 'bg-primary/15' ($route.path === i.href || i.isActive?.value) && 'bg-primary/15'

View 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>

View 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>

View 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>

View 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
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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
})
}

View 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
}

View File

@@ -6,15 +6,12 @@ import { streamText } from 'hono/streaming';
import isMobile from 'is-mobile'; import isMobile from 'is-mobile';
import { renderToWebStream } from 'vue/server-renderer'; import { renderToWebStream } from 'vue/server-renderer';
import { buildBootstrapScript } from './lib/manifest'; import { buildBootstrapScript } from './lib/manifest';
import { styleTags } from './lib/primePassthrough'; import { createTextTransformStreamClass } from './lib/replateStreamText';
import { createApp } from './main'; import { createApp } from './main';
import { useAuthStore } from './stores/auth'; import { useAuthStore } from './stores/auth';
// @ts-ignore
import Base from '@primevue/core/base';
import { createTextTransformStreamClass } from './lib/replateStreamText';
const app = new Hono() const app = new Hono()
const defaultNames = ['primitive', 'semantic', 'global', 'base', 'ripple-directive']
// app.use(renderer)
app.use('*', contextStorage()); app.use('*', contextStorage());
app.use(cors(), async (c, next) => { app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app)); c.set("fetch", app.request.bind(app));
@@ -35,8 +32,7 @@ app.use(cors(), async (c, next) => {
url.protocol = 'https:' url.protocol = 'https:'
url.pathname = path.replace(/^\/r/, '') || '/' url.pathname = path.replace(/^\/r/, '') || '/'
url.port = '' url.port = ''
// console.log("url", url.toString())
// console.log("c.req.raw", c.req.raw)
const headers = new Headers(c.req.header()); const headers = new Headers(c.req.header());
headers.delete("host"); headers.delete("host");
headers.delete("connection"); headers.delete("connection");
@@ -50,9 +46,11 @@ app.use(cors(), async (c, next) => {
credentials: 'include' credentials: 'include'
}); });
}); });
app.get("/.well-known/*", (c) => { app.get("/.well-known/*", (c) => {
return c.json({ ok: true }); return c.json({ ok: true });
}); });
app.get("*", async (c) => { app.get("*", async (c) => {
const nonce = crypto.randomUUID(); const nonce = crypto.randomUUID();
const url = new URL(c.req.url); const url = new URL(c.req.url);
@@ -60,33 +58,28 @@ app.get("*", async (c) => {
app.provide("honoContext", c); app.provide("honoContext", c);
const auth = useAuthStore(); const auth = useAuthStore();
auth.$reset(); auth.$reset();
// auth.initialized = false;
await auth.init(); await auth.init();
await router.push(url.pathname); await router.push(url.pathname);
await router.isReady(); await router.isReady();
let usedStyles = new Set<String>();
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name)
return streamText(c, async (stream) => { return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8"); c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity"); c.header("Content-Encoding", "Identity");
const ctx: Record<string, any> = {}; const ctx: Record<string, any> = {};
const appStream = renderToWebStream(app, ctx); const appStream = renderToWebStream(app, ctx);
// console.log("ctx: ", );
await stream.write("<!DOCTYPE html><html lang='en'><head>"); await stream.write("<!DOCTYPE html><html lang='en'><head>");
await stream.write("<base href='" + url.origin + "'/>"); await stream.write("<base href='" + url.origin + "'/>");
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, ""))); 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 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 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('<link rel="icon" href="/favicon.ico" />');
await stream.write(buildBootstrapScript()); 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.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>`))); 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.teleports
delete ctx.__teleportBuffers delete ctx.__teleportBuffers
delete ctx.modules; delete ctx.modules;
@@ -95,6 +88,7 @@ app.get("*", async (c) => {
await stream.write("</body></html>"); await stream.write("</body></html>");
}); });
}) })
const ESCAPE_LOOKUP: { [match: string]: string } = { const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026", "&": "\\u0026",
">": "\\u003e", ">": "\\u003e",
@@ -108,4 +102,5 @@ const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlEscape(str: string): string { function htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]); return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
} }
export default app 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

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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

View File

@@ -1,44 +1,28 @@
import { createHead as CSRHead } from "@unhead/vue/client"; import { createHead as CSRHead } from "@unhead/vue/client";
import { createHead as SSRHead } from "@unhead/vue/server"; import { createHead as SSRHead } from "@unhead/vue/server";
import { createPinia } from "pinia";
import { createSSRApp } from 'vue'; import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary'; import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import { vueSWR } from './lib/swr/use-swrv';
import createAppRouter from './routes'; 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" const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen"
export function createApp() { export function createApp() {
const pinia = createPinia(); const pinia = createPinia();
const app = createSSRApp(withErrorBoundary(RouterView)); const app = createSSRApp(withErrorBoundary(RouterView));
const head = import.meta.env.SSR ? SSRHead() : CSRHead(); const head = import.meta.env.SSR ? SSRHead() : CSRHead();
app.use(head); app.use(head);
app.use(PrimeVue, {
// unstyled: true, // Directive để skip hydration cho các phần tử không cần thiết
theme: {
preset: Aura,
options: {
darkModeSelector: '.my-app-dark',
cssLayer: false,
// cssLayer: {
// name: 'primevue',
// order: 'theme, base, primevue'
// }
}
}
});
app.use(ToastService);
app.directive('nh', { app.directive('nh', {
created(el) { created(el) {
el.__v_skip = true; el.__v_skip = true;
} }
}); });
app.directive("tooltip", Tooltip)
// Restore state từ SSR
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => { Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
(window as any)[key] = value; (window as any)[key] = value;
@@ -47,8 +31,9 @@ export function createApp() {
pinia.state.value = (window as any).$p; pinia.state.value = (window as any).$p;
} }
} }
app.use(pinia); app.use(pinia);
app.use(vueSWR({ revalidateOnFocus: false }));
const router = createAppRouter(); const router = createAppRouter();
app.use(router); app.use(router);

View File

@@ -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>

View File

@@ -1,73 +1,87 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<Toast /> <Toast />
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit" <Form
class="flex flex-col gap-4 w-full"> :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"> <div class="text-sm text-gray-600 mb-2">
Enter your email address and we'll send you a link to reset your password. Enter your email address and we'll send you a link to reset your password.
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email address</label> <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 /> <Input
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{ name="email"
$form.email.error?.message }}</Message> 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>
<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"> <div class="text-center mt-2">
<router-link to="/login" replace <router-link
class="inline-flex items-center text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"> 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"> <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" <path
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path> stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
></path>
</svg> </svg>
Back to Sign in Back to Sign in
</router-link> </router-link>
</div> </div>
</template>
</Form> </Form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Form, type FormSubmitEvent } from '@primevue/forms'; import { client } from '@/api/client'
import { zodResolver } from '@primevue/forms/resolvers/zod'; import Form from '@/components/form/Form.vue'
import Toast from 'primevue/toast'; import Message from '@/components/form/Message.vue'
import { reactive } from 'vue'; import Button from '@/components/ui/Button.vue'
import { z } from 'zod'; 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'; const toast = useToast()
import { useAuthStore } from '@/stores/auth';
import { useToast } from "primevue/usetoast";
const auth = useAuthStore(); const forgotSchema = z.object({
const toast = useToast(); email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' })
})
const initialValues = reactive({ const initialValues = reactive({
email: '' email: ''
}); })
const resolver = zodResolver( const onFormSubmit = async (values: any) => {
z.object({ try {
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }) await client.auth.forgotPasswordCreate({ email: values.email })
}) toast.success('Reset link sent', 'Success')
); } catch (error: any) {
toast.error(error.message || 'An error occurred', 'Error')
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> </script>

View File

@@ -1,38 +1,77 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<Toast /> <Toast />
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit" <Form
class="flex flex-col gap-4 w-full"> :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"> <div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email</label> <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 <Input
:disabled="auth.loading" /> name="email"
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{ type="text"
$form.email.error?.message }}</Message> 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>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="password" class="text-sm font-medium text-gray-700">Password</label> <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 <InputPassword
fluid :inputStyle="{ width: '100%' }" :disabled="auth.loading" /> name="password"
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{ placeholder="Enter your password"
$form.password.error?.message }}</Message> :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>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox inputId="remember-me" size="small" name="rememberMe" binary :disabled="auth.loading" /> <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> <label for="remember-me" class="text-sm text-gray-900">Remember me</label>
</div> </div>
<div class="text-sm"> <div class="text-sm">
<router-link to="/forgot" <router-link
class="text-blue-600 hover:text-blue-500 hover:underline">Forgot to="/forgot"
password?</router-link> class="text-blue-600 hover:text-blue-500 hover:underline"
>
Forgot password?
</router-link>
</div> </div>
</div> </div>
<Button type="submit" size="small" :label="auth.loading ? 'Signing in...' : 'Sign in'" fluid <Button
:loading="auth.loading" /> type="submit"
size="sm"
:loading="auth.loading"
fluid
>
{{ auth.loading ? 'Signing in...' : 'Sign in' }}
</Button>
<div class="relative"> <div class="relative">
<div class="absolute inset-0 flex items-center"> <div class="absolute inset-0 flex items-center">
@@ -43,60 +82,76 @@
</div> </div>
</div> </div>
<Button size="small" type="button" variant="outlined" severity="secondary" <Button
class="w-full flex items-center justify-center gap-2" @click="loginWithGoogle" :disabled="auth.loading"> 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"> <svg class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor">
<path <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" /> 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> </svg>
Google Google
</Button> </Button>
<div class="mt-2 flex flex-col items-center justify-center gap-1 text-sm text-gray-600"> <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"> <p class="text-center text-sm text-gray-600">
Don't have an account? 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> <router-link
to="/sign-up"
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
>
Sign up
</router-link>
</p> </p>
<!-- <router-link to="/forgot" class="text-blue-600 hover:text-blue-500 hover:underline">Forgot password?</router-link> -->
</div> </div>
</template>
</Form> </Form>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth'; import Form from '@/components/form/Form.vue'
import { Form, type FormSubmitEvent } from '@primevue/forms'; import Message from '@/components/form/Message.vue'
import { zodResolver } from '@primevue/forms/resolvers/zod'; import Button from '@/components/ui/Button.vue'
import Toast from 'primevue/toast'; import Checkbox from '@/components/ui/Checkbox.vue'
import { useToast } from "primevue/usetoast"; import Input from '@/components/ui/Input.vue'
import { reactive } from 'vue'; import InputPassword from '@/components/ui/InputPassword.vue'
import { z } from 'zod'; import Toast from '@/components/ui/Toast.vue'
const t = useToast(); import { useToast } from '@/composables/useToast'
const auth = useAuthStore(); import { useAuthStore } from '@/stores/auth'
// const $form = Form.useFormContext(); import { reactive, watch } from 'vue'
watch(() => auth.error, (newError) => { import { z } from 'zod'
if (newError) {
t.add({ severity: 'error', summary: String(auth.error), detail: newError, life: 5000 }); 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({ const initialValues = reactive({
email: '', email: '',
password: '', password: '',
rememberMe: false rememberMe: false
}); })
const resolver = zodResolver( watch(() => auth.error, (newError) => {
z.object({ if (newError) {
email: z.string().min(1, { message: 'Email or username is required.' }), toast.error(String(auth.error), 'Error')
password: z.string().min(1, { message: 'Password is required.' }) }
}) })
);
const onFormSubmit = async ({ valid, values }: FormSubmitEvent) => { const onFormSubmit = async (values: any) => {
if (valid) auth.login(values.email, values.password); await auth.login(values.email, values.password)
}; }
const loginWithGoogle = () => { const loginWithGoogle = () => {
auth.loginWithGoogle(); auth.loginWithGoogle()
}; }
</script> </script>

View File

@@ -1,70 +1,106 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit" <Form
class="flex flex-col gap-4 w-full"> :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"> <div class="flex flex-col gap-1">
<label for="name" class="text-sm font-medium text-gray-700">Full Name</label> <label for="name" class="text-sm font-medium text-gray-700">Full Name</label>
<InputText size="small" name="name" placeholder="John Doe" fluid /> <Input
<Message v-if="$form.name?.invalid" severity="error" size="small" variant="simple">{{ name="name"
$form.name.error?.message }}</Message> placeholder="John Doe"
fluid
/>
<Message
v-if="form.getFieldMeta('name')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('name')?.errorMap?.onChange }}
</Message>
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email address</label> <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 /> <Input
<Message v-if="$form.email?.invalid" severity="error" size="small" variant="simple">{{ name="email"
$form.email.error?.message }}</Message> 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>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="password" class="text-sm font-medium text-gray-700">Password</label> <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 <InputPassword
:inputStyle="{ width: '100%' }" /> name="password"
placeholder="Create a password"
:feedback="true"
fluid
/>
<small class="text-gray-500">Must be at least 8 characters.</small> <small class="text-gray-500">Must be at least 8 characters.</small>
<Message v-if="$form.password?.invalid" severity="error" size="small" variant="simple">{{ <Message
$form.password.error?.message }}</Message> v-if="form.getFieldMeta('password')?.errorMap?.onChange"
severity="error"
size="sm"
>
{{ form.getFieldMeta('password')?.errorMap?.onChange }}
</Message>
</div> </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"> <p class="mt-4 text-center text-sm text-gray-600">
Already have an account? Already have an account?
<router-link to="/login" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign <router-link
in</router-link> to="/login"
class="font-medium text-blue-600 hover:text-blue-500 hover:underline"
>
Sign in
</router-link>
</p> </p>
</template>
</Form> </Form>
</div> </div>
</template> </template>
<script setup lang="ts"> <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'; const auth = useAuthStore()
import { zodResolver } from '@primevue/forms/resolvers/zod';
import { reactive } from 'vue';
import { z } from 'zod';
const signupSchema = z.object({
import { useAuthStore } from '@/stores/auth'; name: z.string().min(1, { message: 'Name is required.' }),
email: z.string().min(1, { message: 'Email is required.' }).email({ message: 'Invalid email address.' }),
const auth = useAuthStore(); password: z.string().min(8, { message: 'Password must be at least 8 characters.' })
})
const initialValues = reactive({ const initialValues = reactive({
name: '', name: '',
email: '', email: '',
password: '' password: ''
}); })
const resolver = zodResolver( const onFormSubmit = (values: any) => {
z.object({ auth.register(values.name, values.email, values.password)
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> </script>

View File

@@ -3,7 +3,7 @@ import Chart from '@/components/icons/Chart.vue';
import Credit from '@/components/icons/Credit.vue'; import Credit from '@/components/icons/Credit.vue';
import Upload from '@/components/icons/Upload.vue'; import Upload from '@/components/icons/Upload.vue';
import Video from '@/components/icons/Video.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 { useRouter } from 'vue-router';
import Referral from './Referral.vue'; 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 gap-4">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 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"> <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="8rem" height="1.25rem" class="mb-2"></Skeleton>
<Skeleton width="100%" height="1rem"></Skeleton> <Skeleton width="100%" height="1rem"></Skeleton>
</div> </div>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ModelVideo } from '@/api/client'; import { ModelVideo } from '@/api/client';
import EmptyState from '@/components/dashboard/EmptyState.vue'; import EmptyState from '@/components/dashboard/EmptyState.vue';
import { formatBytes, formatDate, formatDuration } from '@/lib/utils'; import Skeleton from '@/components/ui/Skeleton.vue';
import Skeleton from 'primevue/skeleton'; import { formatDate, formatDuration } from '@/lib/utils';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
interface Props { interface Props {
@@ -34,7 +34,7 @@ const getStatusClass = (status?: string) => {
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden"> <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="p-4 border-b border-gray-200" v-for="i in 5" :key="i">
<div class="flex gap-4"> <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"> <div class="flex-1 space-y-2">
<Skeleton width="30%" height="1rem"></Skeleton> <Skeleton width="30%" height="1rem"></Skeleton>
<Skeleton width="20%" height="0.8rem"></Skeleton> <Skeleton width="20%" height="0.8rem"></Skeleton>

View File

@@ -7,7 +7,7 @@
<p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from <p class="text-sm text-gray-600 font-medium">Share your referral link and earn commissions from
referred users!</p> referred users!</p>
<div class="flex gap-2"> <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"> <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" <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" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
@@ -27,6 +27,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Input from '@/components/ui/Input.vue';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { ref } from 'vue'; import { ref } from 'vue';
const auth = useAuthStore() const auth = useAuthStore()

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import StatsCard from '@/components/dashboard/StatsCard.vue'; import StatsCard from '@/components/dashboard/StatsCard.vue';
import Skeleton from '@/components/ui/Skeleton.vue';
import { formatBytes } from '@/lib/utils'; import { formatBytes } from '@/lib/utils';
import Skeleton from 'primevue/skeleton';
interface Props { interface Props {
loading: boolean; loading: boolean;
@@ -25,7 +25,7 @@ defineProps<Props>();
<Skeleton width="5rem" height="1rem" class="mb-2"></Skeleton> <Skeleton width="5rem" height="1rem" class="mb-2"></Skeleton>
<Skeleton width="8rem" height="2rem"></Skeleton> <Skeleton width="8rem" height="2rem"></Skeleton>
</div> </div>
<!-- <Skeleton shape="circle" size="3rem"></Skeleton> --> <!-- <Skeleton circle width="3rem" height="3rem"></Skeleton> -->
</div> </div>
<Skeleton width="4rem" height="1rem"></Skeleton> <Skeleton width="4rem" height="1rem"></Skeleton>
</div> </div>

View File

@@ -1,21 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { client, type ModelPlan } from '@/api/client'; import { client, type ModelPlan } from '@/api/client'
import PageHeader from '@/components/dashboard/PageHeader.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue'
import useSWRV from '@/lib/swr'; import { useSWRV } from '@/composables/useDataLoader'
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth'
import { computed, ref, watch } from 'vue'; import { computed, ref } from 'vue'
import CurrentPlanCard from './components/CurrentPlanCard.vue'; import CurrentPlanCard from './components/CurrentPlanCard.vue'
import UsageStatsCard from './components/UsageStatsCard.vue'; import EditPlanDialog from './components/EditPlanDialog.vue'
import PlanList from './components/PlanList.vue'; import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue'
import PlanPaymentHistory from './components/PlanPaymentHistory.vue'; import PlanList from './components/PlanList.vue'
import EditPlanDialog from './components/EditPlanDialog.vue'; import PlanPaymentHistory from './components/PlanPaymentHistory.vue'
import ManageSubscriptionDialog from './components/ManageSubscriptionDialog.vue'; import UsageStatsCard from './components/UsageStatsCard.vue'
const auth = useAuthStore(); const auth = useAuthStore()
// const plans = ref<ModelPlan[]>([]); const subscribing = ref<string | null>(null)
const subscribing = ref<string | null>(null); const showManageDialog = ref(false)
const showManageDialog = ref(false); const cancelling = ref(false)
const cancelling = ref(false);
// Mock Payment History Data // Mock Payment History Data
const paymentHistory = ref([ const paymentHistory = ref([
@@ -23,90 +22,72 @@ const paymentHistory = ref([
{ id: 'inv_002', date: 'Nov 24, 2025', amount: 9.99, plan: 'Basic Plan', status: 'success', invoiceId: 'INV-2025-002' }, { 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_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' }, { 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)
const { data, isLoading, mutate: mutatePlans } = useSWRV('r/plans', () => client.plans.plansList())
// Computed Usage (Mock if not in store) // Computed Usage (Mock if not in store)
const storageUsed = computed(() => auth.user?.storage_used || 0); // bytes const storageUsed = computed(() => auth.user?.storage_used || 0) // bytes
// Default limit 10GB if no plan const storageLimit = computed(() => 10737418240)
const storageLimit = computed(() => 10737418240); const uploadsUsed = ref(12)
const uploadsUsed = ref(12); const uploadsLimit = ref(50)
const uploadsLimit = ref(50);
const currentPlanId = computed(() => { const currentPlanId = computed(() => {
if (auth.user?.plan_id) return auth.user.plan_id; 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 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; return undefined
}); })
const currentPlan = computed(() => { const currentPlan = computed(() => {
if (!Array.isArray(data?.value?.data?.data.plans)) return undefined; if (!Array.isArray(data.value?.data?.data?.plans)) return undefined
return data.value.data.data.plans.find(p => p.id === currentPlanId.value); return data.value.data.data.plans.find((p: ModelPlan) => p.id === currentPlanId.value)
}); })
const showEditDialog = ref(false)
// watch(data, (newValue) => { const editingPlan = ref<ModelPlan>({} as ModelPlan)
// if (newValue) { const isSaving = ref(false)
// // 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 openEditPlan = (plan: ModelPlan) => { const openEditPlan = (plan: ModelPlan) => {
editingPlan.value = { ...plan }; editingPlan.value = { ...plan }
showEditDialog.value = true; showEditDialog.value = true
}; }
const savePlan = async (updatedPlan: ModelPlan) => { const savePlan = async (updatedPlan: ModelPlan) => {
isSaving.value = true; isSaving.value = true
try { try {
if (!updatedPlan.id) return; if (!updatedPlan.id) return
// Optimistic update or API call
await client.request({ await client.request({
path: `/plans/${updatedPlan.id}`, path: `/plans/${updatedPlan.id}`,
method: 'PUT', method: 'PUT',
body: updatedPlan body: updatedPlan
}); })
// Refresh plans await mutatePlans()
await mutatePlans();
showEditDialog.value = false; showEditDialog.value = false
alert('Plan updated successfully'); alert('Plan updated successfully')
} catch (e: any) { } catch (e: any) {
console.error('Failed to update plan', e); 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: ModelPlan) => p.id === updatedPlan.id)
const idx = data.value!.data.data.plans.findIndex(p => p.id === updatedPlan.id);
if (idx !== -1) { if (idx !== -1) {
data.value!.data.data.plans[idx] = { ...updatedPlan }; data.value!.data.data.plans[idx] = { ...updatedPlan }
} }
showEditDialog.value = false; showEditDialog.value = false
// alert('Note: API update failed, updated locally. ' + e.message);
} finally { } finally {
isSaving.value = false; isSaving.value = false
} }
}; }
const subscribe = async (plan: ModelPlan) => { const subscribe = async (plan: ModelPlan) => {
if (!plan.id) return; if (!plan.id) return
subscribing.value = plan.id; subscribing.value = plan.id
try { try {
await client.payments.paymentsCreate({ await client.payments.paymentsCreate({
amount: plan.price || 0, amount: plan.price || 0,
plan_id: plan.id plan_id: plan.id
}); })
// Update local state mock alert(`Successfully subscribed to ${plan.name}`)
// In real app, we would re-fetch user profile
alert(`Successfully subscribed to ${plan.name}`);
paymentHistory.value.unshift({ paymentHistory.value.unshift({
id: `inv_${Date.now()}`, id: `inv_${Date.now()}`,
@@ -115,28 +96,27 @@ const subscribe = async (plan: ModelPlan) => {
plan: plan.name || 'Unknown', plan: plan.name || 'Unknown',
status: 'success', status: 'success',
invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}` invoiceId: `INV-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)}`
}); })
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err)
alert('Failed to subscribe: ' + (err.message || 'Unknown error')); alert('Failed to subscribe: ' + (err.message || 'Unknown error'))
} finally { } finally {
subscribing.value = null; subscribing.value = null
} }
}; }
const cancelSubscription = async () => { const cancelSubscription = async () => {
cancelling.value = true; cancelling.value = true
try { try {
// Simulate API call await new Promise(resolve => setTimeout(resolve, 1500))
await new Promise(resolve => setTimeout(resolve, 1500)); alert('Subscription has been canceled.')
alert('Subscription has been canceled.'); showManageDialog.value = false
showManageDialog.value = false;
} catch (e) { } catch (e) {
alert('Failed to cancel subscription.'); alert('Failed to cancel subscription.')
} finally { } finally {
cancelling.value = false; cancelling.value = false
} }
}; }
</script> </script>
<template> <template>
@@ -168,7 +148,7 @@ const cancelSubscription = async () => {
</div> </div>
<PlanList <PlanList
:plans="data?.data?.data.plans || []" :plans="data?.data?.data?.plans || []"
:is-loading="!!isLoading" :is-loading="!!isLoading"
:current-plan-id="currentPlanId" :current-plan-id="currentPlanId"
:subscribing-plan-id="subscribing" :subscribing-plan-id="subscribing"
@@ -195,4 +175,3 @@ const cancelSubscription = async () => {
/> />
</div> </div>
</template> </template>

View File

@@ -1,39 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import { type ModelPlan } from '@/api/client'; import type { ModelPlan } from '@/api/client';
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue';
import Tag from 'primevue/tag'; import Tag from '@/components/ui/Tag.vue';
import { computed } from 'vue';
defineProps<{ const props = defineProps<{
currentPlan?: ModelPlan; currentPlan?: ModelPlan
}>(); }>()
defineEmits<{ const emit = defineEmits<{
(e: 'manage'): void; (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> </script>
<template> <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"> <div class="bg-gradient-to-br from-blue-600 to-purple-700 rounded-2xl p-8 text-white">
<!-- Background decorations --> <div class="flex items-start justify-between mb-4">
<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> <div>
<h2 class="text-sm font-medium text-gray-400 uppercase tracking-wider mb-1">Current Plan</h2> <p class="text-blue-100 text-sm font-medium mb-1">Current Plan</p>
<h3 class="text-4xl font-bold text-white mb-2">{{ currentPlan?.name || 'Standard Plan' }}</h3> <h3 class="text-3xl font-bold">{{ planName }}</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>
<Tag
:value="isActive ? 'Active' : 'Inactive'"
:severity="isActive ? 'success' : 'danger'"
class="!bg-white/20 !text-white !border-white/30"
/>
</div> </div>
<div class="mt-8 pt-8 border-t border-gray-700/50 flex gap-4"> <div class="flex items-baseline gap-1 mb-6">
<Button label="Manage Subscription" severity="secondary" class="bg-white/10 border-white/10 text-white hover:bg-white/20" @click="$emit('manage')" /> <span class="text-4xl font-bold">${{ planPrice }}</span>
</div> <span class="text-blue-100">/{{ planCycle }}</span>
</div> </div>
<Button variant="outline" class="!text-white !border-white/50 hover:!bg-white/20" @click="emit('manage')">
Manage Subscription
</Button>
</div> </div>
</template> </template>

View File

@@ -1,90 +1,107 @@
<script setup lang="ts"> <script setup lang="ts">
import { type ModelPlan } from '@/api/client'; import { type ModelPlan } from '@/api/client'
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue'
import Checkbox from 'primevue/checkbox'; import Checkbox from '@/components/ui/Checkbox.vue'
import Dialog from 'primevue/dialog'; import Dialog from '@/components/ui/Dialog.vue'
import InputNumber from 'primevue/inputnumber'; import Input from '@/components/ui/Input.vue'
import InputText from 'primevue/inputtext'; import { computed, ref, watch } from 'vue'
import Textarea from 'primevue/textarea';
import { computed, ref, watch } from 'vue';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean
plan: ModelPlan; plan: ModelPlan
loading?: boolean; loading?: boolean
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', value: boolean): void; (e: 'update:visible', value: boolean): void
(e: 'save', plan: ModelPlan): void; (e: 'save', plan: ModelPlan): void
}>(); }>()
// Create a local copy to edit // Create a local copy to edit
const localPlan = ref<ModelPlan>({}); const localPlan = ref<ModelPlan>({} as ModelPlan)
// Sync when dialog opens or plan changes // Sync when dialog opens or plan changes
watch(() => props.plan, (newPlan) => { watch(() => props.plan, (newPlan) => {
localPlan.value = { ...newPlan }; localPlan.value = { ...newPlan }
}, { immediate: true }); }, { immediate: true })
const onSave = () => { const onSave = () => {
emit('save', localPlan.value); emit('save', localPlan.value)
}; }
const visibleModel = computed({ const handleClose = () => {
get: () => props.visible, emit('update:visible', false)
set: (val) => emit('update:visible', val) }
});
const isActive = computed({
get: () => localPlan.value.is_active ?? false,
set: (val: boolean) => {
localPlan.value.is_active = val
}
})
</script> </script>
<template> <template>
<Dialog v-model:visible="visibleModel" modal header="Edit Plan" :style="{ width: '40rem' }"> <Dialog
:visible="visible"
header="Edit Plan"
width="40rem"
:closable="true"
@update:visible="handleClose"
>
<div class="space-y-4"> <div class="space-y-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-name" class="text-sm font-medium text-gray-700">Name</label> <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" /> <Input id="plan-name" v-model="localPlan.name" placeholder="Plan Name" />
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-price" class="text-sm font-medium text-gray-700">Price ($)</label> <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" /> <Input id="plan-price" :model-value="localPlan.price ?? ''" type="number" placeholder="0.00" @update:model-value="localPlan.price = Number($event)" />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-cycle" class="text-sm font-medium text-gray-700">Billing Cycle</label> <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" /> <Input id="plan-cycle" v-model="localPlan.cycle" placeholder="e.g. month, year" />
</div> </div>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-desc" class="text-sm font-medium text-gray-700">Description</label> <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" /> <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>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-storage" class="text-sm font-medium text-gray-700">Storage Limit (bytes)</label> <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" /> <Input id="plan-storage" :model-value="localPlan.storage_limit ?? ''" type="number" @update:model-value="localPlan.storage_limit = Number($event)" />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-uploads" class="text-sm font-medium text-gray-700">Upload Limit (per day)</label> <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" /> <Input id="plan-uploads" :model-value="localPlan.upload_limit ?? ''" type="number" @update:model-value="localPlan.upload_limit = Number($event)" />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="plan-duration" class="text-sm font-medium text-gray-700">Duration Limit (sec)</label> <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" /> <Input id="plan-duration" :model-value="localPlan.duration_limit ?? ''" type="number" @update:model-value="localPlan.duration_limit = Number($event)" />
</div> </div>
</div> </div>
<div class="flex items-center gap-2 pt-2"> <div class="flex items-center gap-2 pt-2">
<Checkbox v-model="localPlan.is_active" :binary="true" inputId="plan-active" /> <Checkbox v-model="isActive" :binary="true" />
<label for="plan-active" class="text-sm font-medium text-gray-700">Active</label> <label class="text-sm font-medium text-gray-700">Active</label>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<Button label="Cancel" text severity="secondary" @click="visibleModel = false" /> <div class="flex justify-end gap-2">
<Button label="Save Changes" icon="i-heroicons-check" @click="onSave" :loading="loading" /> <Button variant="outline" @click="handleClose">Cancel</Button>
<Button :loading="loading" @click="onSave">Save Changes</Button>
</div>
</template> </template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -1,57 +1,55 @@
<script setup lang="ts"> <script setup lang="ts">
import { type ModelPlan } from '@/api/client'; import type { ModelPlan } from '@/api/client';
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue';
import Dialog from 'primevue/dialog'; import Dialog from '@/components/ui/Dialog.vue';
import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean
currentPlan?: ModelPlan; currentPlan?: ModelPlan
cancelling?: boolean; cancelling?: boolean
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:visible', value: boolean): void; (e: 'update:visible', value: boolean): void
(e: 'cancel-subscription'): void; (e: 'cancel-subscription'): void
}>(); }>()
const visibleModel = computed({ const handleClose = () => {
get: () => props.visible, emit('update:visible', false)
set: (val) => emit('update:visible', val) }
});
</script> </script>
<template> <template>
<Dialog v-model:visible="visibleModel" modal header="Manage Subscription" :style="{ width: '30rem' }"> <Dialog
<div class="mb-4"> :visible="visible"
<p class="text-gray-600 mb-4">You are currently subscribed to <span class="font-bold text-gray-900">{{ currentPlan?.name }}</span>.</p> header="Manage Subscription"
<div class="bg-gray-50 p-4 rounded-lg space-y-2 border border-gray-200"> width="28rem"
<div class="flex justify-between"> :closable="true"
<span class="text-sm text-gray-500">Status</span> @update:visible="handleClose"
<span class="text-sm font-medium text-green-600">Active</span> >
<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>
<div class="flex justify-between">
<span class="text-sm text-gray-500">Renewal Date</span> <div class="border-t border-gray-200 pt-4">
<span class="text-sm font-medium text-gray-900">Feb 24, 2026</span> <h4 class="font-medium text-gray-900 mb-2">Cancel Subscription</h4>
</div> <p class="text-sm text-gray-600 mb-4">
<div class="flex justify-between"> If you cancel, you'll lose access to premium features at the end of your billing period.
<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.
</p> </p>
<div class="flex justify-end gap-2">
<Button label="Close" text severity="secondary" @click="visibleModel = false" />
<Button <Button
label="Cancel Subscription" variant="danger"
severity="danger" :loading="cancelling"
:icon="cancelling ? 'i-svg-spinners-180-ring-with-bg' : 'i-heroicons-x-circle'"
@click="emit('cancel-subscription')" @click="emit('cancel-subscription')"
:disabled="cancelling" >
/> Cancel Subscription
</Button>
</div> </div>
</div>
<template #footer>
<Button variant="outline" @click="handleClose">Close</Button>
</template>
</Dialog> </Dialog>
</template> </template>

View File

@@ -1,105 +1,97 @@
<script setup lang="ts"> <script setup lang="ts">
import { type ModelPlan } from '@/api/client'; import type { ModelPlan } from '@/api/client';
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue';
import Skeleton from 'primevue/skeleton'; import Skeleton from '@/components/ui/Skeleton.vue';
import { formatBytes } from '@/lib/utils'; // Using utils formatBytes import { formatBytes } from '@/lib/utils';
defineProps<{ const props = defineProps<{
plans: ModelPlan[]; plans: ModelPlan[]
isLoading: boolean; isLoading: boolean
currentPlanId?: string; currentPlanId?: string
subscribingPlanId?: string | null; subscribingPlanId?: string | null
isAdmin?: boolean; isAdmin?: boolean
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'subscribe', plan: ModelPlan): void; (e: 'subscribe', plan: ModelPlan): void
(e: 'edit', 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;
}
</script> </script>
<template> <template>
<section> <section>
<div class="flex items-center justify-between mb-8"> <h2 class="text-2xl font-bold mb-6 text-gray-900">Available Plans</h2>
<h2 class="text-2xl font-bold text-gray-900">Upgrade your workspace</h2>
</div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="isLoading" class="grid grid-cols-1 md:grid-cols-3 gap-8"> <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="h-full"> <div v-for="i in 3" :key="i" class="bg-white border border-gray-200 rounded-xl p-6">
<Skeleton height="300px" borderRadius="16px"></Skeleton> <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>
</div> </div>
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-8 items-start"> <!-- Plans Grid -->
<div v-for="plan in plans" :key="plan.id" class="relative group h-full"> <div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<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"> <div
Recommended 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>
<!-- 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>
<div class="mb-6"> <div class="mb-6">
<span class="text-4xl font-bold text-gray-900">${{ plan.price }}</span> <span class="text-3xl font-bold text-gray-900">${{ plan.price }}</span>
<span class="text-gray-500 text-sm">/{{ plan.cycle }}</span> <span class="text-gray-500">/{{ plan.cycle }}</span>
</div> </div>
<ul class="space-y-3 mb-8 flex-grow"> <p class="text-sm text-gray-600 mb-6">{{ plan.description }}</p>
<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>
<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 <Button
:label="isCurrentComp(plan, currentPlanId) ? 'Current Plan' : (subscribingPlanId === plan.id ? 'Processing...' : 'Upgrade')" v-if="isAdmin"
:icon="subscribingPlanId === plan.id ? 'i-svg-spinners-180-ring-with-bg' : ''" variant="outline"
class="w-full" class="flex-1"
:severity="isCurrentComp(plan, currentPlanId) ? 'secondary' : 'primary'" @click="emit('edit', plan)"
:outlined="isCurrentComp(plan, currentPlanId)" >
:disabled="!!subscribingPlanId || isCurrentComp(plan, currentPlanId)" Edit
</Button>
<Button
class="flex-1"
:variant="plan.id === currentPlanId ? 'outline' : 'primary'"
:loading="subscribingPlanId === plan.id"
:disabled="plan.id === currentPlanId"
@click="emit('subscribe', plan)" @click="emit('subscribe', plan)"
/> >
{{ plan.id === currentPlanId ? 'Current Plan' : 'Subscribe' }}
</Button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,93 +1,78 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button'; import { createColumnHelper } from '@/components/table/Column'
import Column from 'primevue/column'; import DataTable from '@/components/table/DataTable.vue'
import DataTable from 'primevue/datatable'; import Tag from '@/components/ui/Tag.vue'
import Tag from 'primevue/tag'; import Toast from '@/components/ui/Toast.vue'
import { useToast } from '@/composables/useToast'
import { h } from 'vue'
interface PaymentHistoryItem { interface PaymentHistoryItem {
id: string; id: string
date: string; date: string
amount: number; amount: number
plan: string; plan: string
status: string; status: string
invoiceId: string; invoiceId: string
} }
defineProps<{ const props = defineProps<{
history: PaymentHistoryItem[]; history: PaymentHistoryItem[]
}>(); }>()
const toast = useToast()
const getStatusSeverity = (status: string) => { const getStatusSeverity = (status: string) => {
switch (status) { switch (status) {
case 'success': case 'success':
return 'success'; return 'success' as const
case 'failed': case 'failed':
return 'danger'; return 'danger' as const
case 'pending': case 'pending':
return 'warn'; return 'warning' as const
default: default:
return 'info'; return 'info' as const
} }
}; }
import { useToast } from 'primevue/usetoast';
import ArrowDownTray from '@/components/icons/ArrowDownTray.vue';
const toast = useToast();
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) => { const downloadInvoice = (item: PaymentHistoryItem) => {
toast.add({ toast.info(`Downloading invoice #${item.invoiceId}...`, 'Downloading')
severity: 'info',
summary: 'Downloading',
detail: `Downloading invoice #${item.invoiceId}...`,
life: 2000
});
// Simulate download delay
setTimeout(() => { setTimeout(() => {
toast.add({ toast.success(`Invoice #${item.invoiceId} downloaded successfully`, 'Downloaded')
severity: 'success', }, 1500)
summary: 'Downloaded', }
detail: `Invoice #${item.invoiceId} downloaded successfully`,
life: 3000
});
}, 1500);
};
</script> </script>
<template> <template>
<section> <section>
<Toast />
<h2 class="text-2xl font-bold mb-6 text-gray-900">Billing History</h2> <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"> <div class="bg-white border border-gray-200 rounded-xl overflow-hidden">
<DataTable :value="history" responsiveLayout="scroll" class="w-full"> <DataTable :data="history" :columns="columns" />
<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> </div>
</section> </section>
</template> </template>

View File

@@ -1,17 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import ProgressBar from '@/components/ui/ProgressBar.vue';
import { formatBytes } from '@/lib/utils'; import { formatBytes } from '@/lib/utils';
import ProgressBar from 'primevue/progressbar';
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
storageUsed: number; storageUsed: number
storageLimit: number; storageLimit: number
uploadsUsed: number; uploadsUsed: number
uploadsLimit: number; uploadsLimit: number
}>(); }>()
const storagePercentage = computed(() => Math.min(Math.round((props.storageUsed / props.storageLimit) * 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)); const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed / props.uploadsLimit) * 100), 100))
</script> </script>
<template> <template>
@@ -23,7 +23,11 @@ const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed
<span class="text-gray-600 font-medium">Storage</span> <span class="text-gray-600 font-medium">Storage</span>
<span class="text-gray-900 font-bold">{{ storagePercentage }}%</span> <span class="text-gray-900 font-bold">{{ storagePercentage }}%</span>
</div> </div>
<ProgressBar :value="storagePercentage" :showValue="false" style="height: 8px" :class="storagePercentage > 90 ? 'p-progressbar-danger' : ''"></ProgressBar> <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> <p class="text-xs text-gray-500 mt-2">{{ formatBytes(storageUsed) }} of {{ formatBytes(storageLimit) }} used</p>
</div> </div>
@@ -32,7 +36,7 @@ const uploadsPercentage = computed(() => Math.min(Math.round((props.uploadsUsed
<span class="text-gray-600 font-medium">Monthly Uploads</span> <span class="text-gray-600 font-medium">Monthly Uploads</span>
<span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span> <span class="text-gray-900 font-bold">{{ uploadsPercentage }}%</span>
</div> </div>
<ProgressBar :value="uploadsPercentage" :showValue="false" style="height: 8px"></ProgressBar> <ProgressBar :value="uploadsPercentage" :show-value="false" />
<p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p> <p class="text-xs text-gray-500 mt-2">{{ uploadsUsed }} of {{ uploadsLimit }} uploads</p>
</div> </div>
</div> </div>

View File

@@ -1,65 +1,52 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import PageHeader from '@/components/dashboard/PageHeader.vue'
import { useAuthStore } from '@/stores/auth'; import Toast from '@/components/ui/Toast.vue'
import PageHeader from '@/components/dashboard/PageHeader.vue'; import { useToast } from '@/composables/useToast'
import ProfileHero from './components/ProfileHero.vue'; import { useAuthStore } from '@/stores/auth'
import ProfileInfoCard from './components/ProfileInfoCard.vue'; import { computed, ref } from 'vue'
import ChangePasswordDialog from './components/ChangePasswordDialog.vue'; import AccountStatusCard from './components/AccountStatusCard.vue'
import AccountStatusCard from './components/AccountStatusCard.vue'; import ChangePasswordDialog from './components/ChangePasswordDialog.vue'
import LinkedAccountsCard from './components/LinkedAccountsCard.vue'; import LinkedAccountsCard from './components/LinkedAccountsCard.vue'
import { useToast } from 'primevue/usetoast'; import ProfileHero from './components/ProfileHero.vue'
import ProfileInfoCard from './components/ProfileInfoCard.vue'
const auth = useAuthStore(); const auth = useAuthStore()
const toast = useToast(); const toast = useToast()
// Dialog visibility // Dialog visibility
const showPasswordDialog = ref(false); const showPasswordDialog = ref(false)
// Refs for dialog components // Refs for dialog components
const passwordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>(); const passwordDialogRef = ref<InstanceType<typeof ChangePasswordDialog>>()
// Computed storage values // Computed storage values
const storageUsed = computed(() => auth.user?.storage_used || 0); const storageUsed = computed(() => auth.user?.storage_used || 0)
const storageLimit = computed(() => 10737418240); // 10GB default const storageLimit = computed(() => 10737418240) // 10GB default
// Handlers // Handlers
const handleEditSave = async (data: { username: string; email: string }) => { const handleEditSave = async (data: { username: string; email: string }) => {
try { try {
await auth.updateProfile(data); await auth.updateProfile(data)
toast.add({ toast.success('Your profile has been updated successfully.', 'Profile Updated')
severity: 'success',
summary: 'Profile Updated',
detail: 'Your profile has been updated successfully.',
life: 3000
});
} catch (e) { } catch (e) {
toast.add({ toast.error(auth.error || 'Failed to update profile.', 'Update Failed')
severity: 'error',
summary: 'Update Failed',
detail: auth.error || 'Failed to update profile.',
life: 5000
});
} }
}; }
const handlePasswordSave = async (data: { currentPassword: string; newPassword: string }) => { const handlePasswordSave = async (data: { currentPassword: string; newPassword: string }) => {
try { try {
await auth.changePassword(data.currentPassword, data.newPassword); await auth.changePassword(data.currentPassword, data.newPassword)
showPasswordDialog.value = false; showPasswordDialog.value = false
toast.add({ toast.success('Your password has been changed successfully.', 'Password Changed')
severity: 'success',
summary: 'Password Changed',
detail: 'Your password has been changed successfully.',
life: 3000
});
} catch (e: any) { } catch (e: any) {
passwordDialogRef.value?.setError(e.message || 'Failed to change password'); passwordDialogRef.value?.setError(e.message || 'Failed to change password')
} }
}; }
</script> </script>
<template> <template>
<div class="profile-page"> <div class="profile-page">
<Toast />
<PageHeader <PageHeader
title="Profile Settings" title="Profile Settings"
description="Manage your account information and preferences." description="Manage your account information and preferences."

View File

@@ -1,46 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import ProgressBar from 'primevue/progressbar'; import ProgressBar from '@/components/ui/ProgressBar.vue';
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ interface Props {
storageUsed: number; storageUsed?: number
storageLimit: number; storageTotal?: number
}>(); }
const storagePercentage = computed(() => const props = withDefaults(defineProps<Props>(), {
Math.min(Math.round((props.storageUsed / props.storageLimit) * 100), 100) storageUsed: 0,
); storageTotal: 100
})
const formatBytes = (bytes: number) => { const usagePercentage = computed(() => {
if (bytes === 0) return '0 B'; if (props.storageTotal === 0) return 0
const k = 1024; return Math.round((props.storageUsed / props.storageTotal) * 100)
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];
};
</script> </script>
<template> <template>
<div class="bg-white border border-gray-200 rounded-2xl p-6"> <div class="bg-white rounded-xl p-6 border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4">Account Status</h3> <h3 class="text-lg font-semibold text-gray-900 mb-4">Account Status</h3>
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
<div class="flex justify-between text-sm mb-2"> <div class="flex justify-between text-sm mb-2">
<span class="text-gray-600">Storage Used</span> <span class="text-gray-600">Storage Usage</span>
<span class="font-bold text-gray-900">{{ storagePercentage }}%</span> <span class="font-medium text-gray-900">{{ usagePercentage }}%</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>
<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> </div>
</div> </div>

View File

@@ -1,101 +1,131 @@
<script setup lang="ts"> <script setup lang="ts">
import Dialog from 'primevue/dialog'; import Button from '@/components/ui/Button.vue';
import InputText from 'primevue/inputtext'; import Dialog from '@/components/ui/Dialog.vue';
import Button from 'primevue/button'; import Input from '@/components/ui/Input.vue';
import Message from 'primevue/message'; import { computed, ref, watch } from 'vue';
import { ref, computed, watch } from 'vue';
const props = defineProps<{ const props = defineProps<{
visible: boolean; visible: boolean
}>(); }>()
const emit = defineEmits<{ const emit = defineEmits<{
'update:visible': [value: boolean]; 'update:visible': [value: boolean]
save: [data: { currentPassword: string; newPassword: string }]; save: [data: { currentPassword: string; newPassword: string }]
}>(); }>()
const currentPassword = ref(''); const currentPassword = ref('')
const newPassword = ref(''); const newPassword = ref('')
const confirmPassword = ref(''); const confirmPassword = ref('')
const loading = ref(false); const loading = ref(false)
const error = ref(''); const error = ref('')
watch(() => props.visible, (val) => { watch(() => props.visible, (val) => {
if (val) { if (val) {
currentPassword.value = ''; currentPassword.value = ''
newPassword.value = ''; newPassword.value = ''
confirmPassword.value = ''; confirmPassword.value = ''
error.value = ''; error.value = ''
} }
}); })
const isValid = computed(() => { const isValid = computed(() => {
return currentPassword.value.length >= 1 return currentPassword.value.length >= 1
&& newPassword.value.length >= 6 && newPassword.value.length >= 6
&& newPassword.value === confirmPassword.value; && newPassword.value === confirmPassword.value
}); })
const passwordMismatch = computed(() => { const passwordMismatch = computed(() => {
return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value; return confirmPassword.value.length > 0 && newPassword.value !== confirmPassword.value
}); })
const passwordTooShort = computed(() => { 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 = () => { const handleSave = () => {
if (!isValid.value) return; if (!isValid.value) return
loading.value = true; loading.value = true
error.value = ''; error.value = ''
emit('save', { emit('save', {
currentPassword: currentPassword.value, currentPassword: currentPassword.value,
newPassword: newPassword.value newPassword: newPassword.value
}); })
}; }
const handleClose = () => { const handleClose = () => {
emit('update:visible', false); emit('update:visible', false)
}; }
// Expose methods for parent to control loading state // Expose methods for parent to control loading state
defineExpose({ defineExpose({
setLoading: (val: boolean) => { loading.value = val; }, setLoading: (val: boolean) => { loading.value = val },
setError: (msg: string) => { error.value = msg; loading.value = false; } setError: (msg: string) => { error.value = msg; loading.value = false }
}); })
</script> </script>
<template> <template>
<Dialog :visible="visible" @update:visible="emit('update:visible', $event)" modal header="Change Password" <Dialog
:style="{ width: '28rem' }" :closable="true" :draggable="false"> :visible="visible"
header="Change Password"
width="28rem"
:closable="true"
:draggable="false"
@update:visible="handleClose"
>
<div class="space-y-6 pt-2"> <div class="space-y-6 pt-2">
<Message v-if="error" severity="error" :closable="false">{{ error }}</Message> <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"> <div class="flex flex-col gap-2">
<label for="current-password" class="text-sm font-medium text-gray-700">Current Password</label> <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" <Input
placeholder="Enter current password" /> id="current-password"
v-model="currentPassword"
type="password"
placeholder="Enter current password"
/>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="new-password" class="text-sm font-medium text-gray-700">New Password</label> <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" <Input
id="new-password"
v-model="newPassword"
type="password"
placeholder="Enter new password (min 6 characters)" placeholder="Enter new password (min 6 characters)"
:class="{ 'p-invalid': passwordTooShort }" /> :class="newPasswordInvalidClass"
/>
<small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small> <small v-if="passwordTooShort" class="text-red-500">Password must be at least 6 characters</small>
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="confirm-password" class="text-sm font-medium text-gray-700">Confirm New Password</label> <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" <Input
id="confirm-password"
v-model="confirmPassword"
type="password"
placeholder="Confirm new password" placeholder="Confirm new password"
:class="{ 'p-invalid': passwordMismatch }" /> :class="confirmPasswordInvalidClass"
/>
<small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small> <small v-if="passwordMismatch" class="text-red-500">Passwords do not match</small>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<div class="flex justify-end gap-3 pt-4"> <div class="flex justify-end gap-3 pt-4">
<Button label="Cancel" severity="secondary" @click="handleClose" :disabled="loading" /> <Button variant="outline" :disabled="loading" @click="handleClose">
<Button label="Change Password" @click="handleSave" :loading="loading" :disabled="!isValid" /> Cancel
</Button>
<Button :loading="loading" :disabled="!isValid" @click="handleSave">
Change Password
</Button>
</div> </div>
</template> </template>
</Dialog> </Dialog>

View File

@@ -1,24 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import Tag from 'primevue/tag'; import Tag from '@/components/ui/Tag.vue';
</script> </script>
<template> <template>
<div class="bg-white border border-gray-200 rounded-2xl p-6"> <div class="bg-white rounded-xl p-6 border border-gray-200">
<h3 class="text-lg font-bold text-gray-900 mb-4">Linked Accounts</h3> <h3 class="text-lg font-semibold text-gray-900 mb-4">Linked Accounts</h3>
<div class="space-y-3"> <div class="space-y-4">
<div class="flex items-center justify-between p-3 rounded-lg border border-gray-100 hover:border-gray-200 transition-colors"> <!-- Placeholder for linked accounts -->
<div class="flex items-center justify-between py-2">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center"> <div class="w-10 h-10 rounded-full bg-gray-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"> <span class="i-heroicons-envelope text-gray-600" />
<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> </div>
<span class="font-medium text-gray-700">Google</span> <div>
<p class="font-medium text-gray-900">Email</p>
<p class="text-sm text-gray-500">Connected</p>
</div> </div>
<Tag value="Connected" severity="success" class="text-xs px-2"></Tag> </div>
<Tag value="Active" severity="success" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,83 +1,44 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelUser } from '@/api/client'; import type { ModelUser } from '@/api/client'
import Avatar from 'primevue/avatar'; import Avatar from '@/components/ui/Avatar.vue'
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue'
import Tag from 'primevue/tag'; import Tag from '@/components/ui/Tag.vue'
import { computed } from 'vue'; import { computed } from 'vue'
const props = defineProps<{ interface Props {
user: ModelUser | null; user: ModelUser | null
}>(); }
const emit = defineEmits<{ const props = defineProps<Props>()
logout: [];
changePassword: [];
}>();
const joinDate = computed(() => { const displayName = computed(() => {
return new Date(props.user?.created_at || Date.now()).toLocaleDateString('en-US', { return props.user?.username || 'User'
year: 'numeric', })
month: 'long',
day: 'numeric' const displayEmail = computed(() => {
}); return props.user?.email || 'No email'
}); })
</script> </script>
<template> <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"> <div class="bg-gradient-to-r from-blue-600 to-purple-600 rounded-xl p-6 text-white">
<!-- Background decorations --> <div class="flex items-center gap-4">
<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 <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" :label="displayName"
size="xlarge" size="xl"
shape="circle" shape="circle"
style="width: 120px; height: 120px; font-size: 3rem;" class="border-4 border-white/30"
image="https://picsum.photos/seed/user123/120/120.jpg"
/> />
<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> </div>
<p class="text-white/80">{{ displayEmail }}</p>
<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> </div>
<p class="text-gray-400 text-lg">{{ user?.email }}</p> <Button variant="ghost" class="!text-white hover:!bg-white/20">
<p class="text-gray-500 text-sm flex items-center justify-center md:justify-start gap-2"> Edit Profile
<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> </Button>
</div> </div>
</div> </div>
</div>
</template> </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>

View File

@@ -1,81 +1,81 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelUser } from '@/api/client'; import type { ModelUser } from '@/api/client'
import Button from 'primevue/button'; import Button from '@/components/ui/Button.vue'
import InputText from 'primevue/inputtext'; import Input from '@/components/ui/Input.vue'
import { ref, watch } from 'vue'
defineProps<{ interface Props {
user: ModelUser | null; user: ModelUser | null
}>(); }
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
edit: []; save: [data: { username: string; email: string }]
changePassword: []; }>()
}>();
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> </script>
<template> <template>
<div class="bg-white border border-gray-200 rounded-2xl p-8"> <div class="bg-white rounded-xl p-6 border border-gray-200">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-bold text-gray-900">Personal Information</h3> <h3 class="text-lg font-semibold text-gray-900">Profile Information</h3>
<div class="flex gap-2"> <Button
<Button label="Change Password" text severity="secondary" @click="emit('changePassword')"> v-if="!isEditing"
<template #icon> size="sm"
<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"> variant="outline"
<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/> @click="isEditing = true"
<path d="M7 11V7a5 5 0 0 1 10 0v4"/> >
</svg> Edit
</template>
</Button> </Button>
</div> </div>
<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>
<div class="grid grid-cols-1 gap-6"> <div>
<div class="flex flex-col gap-2"> <label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<label for="username" class="text-sm font-medium text-gray-700">Username</label> <Input
<div class="relative"> v-if="isEditing"
<IconField> v-model="email"
<InputIcon> type="email"
<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"> placeholder="Enter email"
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/> />
<circle cx="12" cy="7" r="4"/> <p v-else class="text-gray-900">{{ user?.email || 'Not set' }}</p>
</svg>
</InputIcon>
<InputText id="username" :value="user?.username" class="w-full pl-10" readonly />
</IconField>
</div> </div>
</div>
<div class="flex flex-col gap-2"> <div v-if="isEditing" class="flex gap-2 pt-2">
<label for="email" class="text-sm font-medium text-gray-700">Email Address</label> <Button size="sm" variant="outline" @click="handleCancel">Cancel</Button>
<div class="relative"> <Button size="sm" @click="handleSave">Save</Button>
<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> </div>
</div> </div>
</template> </template>
<style scoped>
:deep(.p-inputtext[readonly]) {
background-color: #f9fafb;
border-color: #e5e7eb;
color: #374151;
}
</style>

View File

@@ -1,59 +1,87 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from 'vue'; import type { ModelVideo } from '@/api/client'
import type { ModelVideo } from '@/api/client'; import Checkbox from '@/components/ui/Checkbox.vue'
import { formatDuration, formatDate, getStatusClass } from '@/lib/utils'; import { formatDate, formatDuration, getStatusClass } from '@/lib/utils'
import Checkbox from 'primevue/checkbox';
import Card from 'primevue/card';
defineProps<{ interface Props {
videos: ModelVideo[]; videos: ModelVideo[]
selectedVideos: ModelVideo[]; selectedVideos: ModelVideo[]
}>(); }
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:selectedVideos', value: ModelVideo[]): void; (e: 'update:selectedVideos', value: ModelVideo[]): void
(e: 'delete', videoId: string): 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> </script>
<template> <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"> <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) }">
<template #header>
<div <div
class="aspect-video bg-gray-200 relative overflow-hidden group-hover:opacity-95 transition-opacity"> 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 --> <!-- Grid Selection Checkbox -->
<div class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity" <div
:class="{ 'opacity-100': selectedVideos.some(v => v.id === video.id) }"> class="absolute top-2 left-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity"
<Checkbox :modelValue="selectedVideos" :value="video" :class="{ 'opacity-100': isSelected(video) }"
@update:modelValue="emit('update:selectedVideos', $event)" /> >
<Checkbox
:model-value="isSelected(video)"
:binary="true"
@click="toggleSelection(video)"
/>
</div> </div>
<img v-if="video.thumbnail" :src="video.thumbnail" :alt="video.title" <img
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" /> v-if="video.thumbnail"
<div v-else class="w-full h-full flex items-center justify-center text-gray-400"> :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" /> <span class="i-heroicons-film text-3xl" />
</div> </div>
<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"> 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 <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"> 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) }} {{ formatDuration(video.duration) }}
</span> </span>
</div> </div>
</template>
<template #content> <!-- Content -->
<div class="flex flex-col h-full"> <div class="flex flex-col h-full p-4">
<div class="flex items-start justify-between gap-2 mb-1"> <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" <h3
:title="video.title"> class="font-medium text-sm text-gray-900 line-clamp-2 leading-snug flex-1"
:title="video.title"
>
{{ video.title }} {{ video.title }}
</h3> </h3>
<button class="text-gray-400 hover:text-gray-700"> <button class="text-gray-400 hover:text-gray-700">
@@ -61,12 +89,17 @@ const emit = defineEmits<{
</button> </button>
</div> </div>
<p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">{{ video.description || 'No description' }} <p class="text-xs text-gray-500 mb-3 line-clamp-1 h-4">
{{ video.description || 'No description' }}
</p> </p>
<div class="mt-auto flex items-center justify-between"> <div class="mt-auto flex items-center justify-between">
<span <span
:class="['px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider', getStatusClass(video.status)]"> :class="[
'px-1.5 py-0.5 text-[10px] font-medium rounded-full uppercase tracking-wider',
getStatusClass(video.status)
]"
>
{{ video.status }} {{ video.status }}
</span> </span>
@@ -75,7 +108,6 @@ const emit = defineEmits<{
</div> </div>
</div> </div>
</div> </div>
</template> </div>
</Card>
</div> </div>
</template> </template>

View File

@@ -1,99 +1,142 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineProps, defineEmits } from 'vue'; import type { ModelVideo } from '@/api/client'
import type { ModelVideo } from '@/api/client'; import { createColumnHelper } from '@/components/table/Column'
import { formatDuration, formatDate, formatBytes, getStatusClass } from '@/lib/utils'; import DataTable from '@/components/table/DataTable.vue'
import DataTable from 'primevue/datatable'; import Checkbox from '@/components/ui/Checkbox.vue'
import Column from 'primevue/column'; import { formatBytes, formatDate, formatDuration, getStatusClass } from '@/lib/utils'
import { h } from 'vue'
defineProps<{ interface Props {
videos: ModelVideo[]; videos: ModelVideo[]
selectedVideos: ModelVideo[]; selectedVideos: ModelVideo[]
}>(); }
const props = defineProps<Props>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:selectedVideos', value: ModelVideo[]): void; (e: 'update:selectedVideos', value: ModelVideo[]): void
(e: 'delete', videoId: string): 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> </script>
<template> <template>
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden"> <div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
<DataTable :value="videos" dataKey="id" tableStyle="min-width: 50rem" :selection="selectedVideos" <DataTable
@update:selection="emit('update:selectedVideos', $event)"> :data="videos"
<Column selectionMode="multiple" headerStyle="width: 3rem"></Column> :columns="columns"
:enable-sorting="true"
<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>
</template> </template>

View File

@@ -1,23 +1,22 @@
import { cloudflare } from "@cloudflare/vite-plugin"; import { cloudflare } from "@cloudflare/vite-plugin";
import { PrimeVueResolver } from "@primevue/auto-import-resolver";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx"; import vueJsx from "@vitejs/plugin-vue-jsx";
import path from "node:path"; import path from "node:path";
import unocss from "unocss/vite"; import unocss from "unocss/vite";
import Components from "unplugin-vue-components/vite";
import AutoImport from "unplugin-auto-import/vite"; import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import ssrPlugin from "./ssrPlugin"; import ssrPlugin from "./ssrPlugin";
export default defineConfig((env) => { export default defineConfig((env) => {
// console.log("env:", env, import.meta.env);
return { return {
plugins: [ plugins: [
unocss(), unocss(),
vue(), vue(),
vueJsx(), vueJsx(),
AutoImport({ AutoImport({
imports: ["vue", "vue-router", "pinia"], // Common presets imports: ["vue", "vue-router", "pinia"],
dts: true, // Generate TypeScript declaration file dts: true,
}), }),
Components({ Components({
dirs: ["src/components"], dirs: ["src/components"],
@@ -25,7 +24,6 @@ export default defineConfig((env) => {
dts: true, dts: true,
dtsTsx: true, dtsTsx: true,
directives: false, directives: false,
resolvers: [PrimeVueResolver()],
}), }),
ssrPlugin(), ssrPlugin(),
cloudflare(), cloudflare(),
@@ -33,13 +31,11 @@ export default defineConfig((env) => {
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
// "httpClientAdapter": path.resolve(__dirname, "./src/api/httpClientAdapter.server.ts")
}, },
}, },
optimizeDeps: { optimizeDeps: {
exclude: ["vue"], exclude: ["vue"],
}, },
ssr: { ssr: {
noExternal: ["vue"], noExternal: ["vue"],
}, },