feat: add username availability check API endpoint
feat: create user image retrieval API endpoint feat: enhance coin page with dynamic data fetching and improved UI feat: implement coin creation form with validation and submission logic feat: add user settings page with profile update functionality
This commit is contained in:
parent
9aa4ba157b
commit
16ad425bb5
48 changed files with 3030 additions and 326 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.github
|
||||
8
drop.sql
Normal file
8
drop.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
DROP TABLE IF EXISTS "account" CASCADE;
|
||||
DROP TABLE IF EXISTS "coin" CASCADE;
|
||||
DROP TABLE IF EXISTS "price_history" CASCADE;
|
||||
DROP TABLE IF EXISTS "session" CASCADE;
|
||||
DROP TABLE IF EXISTS "transaction" CASCADE;
|
||||
DROP TABLE IF EXISTS "user" CASCADE;
|
||||
DROP TABLE IF EXISTS "user_portfolio" CASCADE;
|
||||
DROP TABLE IF EXISTS "verification" CASCADE;
|
||||
218
website/bun.lock
218
website/bun.lock
|
|
@ -4,6 +4,8 @@
|
|||
"": {
|
||||
"name": "website",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.815.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.815.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@visx/scale": "^3.12.0",
|
||||
|
|
@ -11,6 +13,7 @@
|
|||
"drizzle-orm": "^0.33.0",
|
||||
"lightweight-charts": "^5.0.7",
|
||||
"lucide-svelte": "^0.511.0",
|
||||
"mode-watcher": "^1.0.7",
|
||||
"postgres": "^3.4.4",
|
||||
"svelte-lightweight-charts": "^2.2.0",
|
||||
},
|
||||
|
|
@ -28,6 +31,7 @@
|
|||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^4.1.7",
|
||||
|
|
@ -42,6 +46,86 @@
|
|||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/crc32c": ["@aws-crypto/crc32c@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag=="],
|
||||
|
||||
"@aws-crypto/sha1-browser": ["@aws-crypto/sha1-browser@5.2.0", "", { "dependencies": { "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg=="],
|
||||
|
||||
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-s3": ["@aws-sdk/client-s3@3.815.0", "", { "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.812.0", "@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/middleware-bucket-endpoint": "3.808.0", "@aws-sdk/middleware-expect-continue": "3.804.0", "@aws-sdk/middleware-flexible-checksums": "3.815.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-location-constraint": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/middleware-ssec": "3.804.0", "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/xml-builder": "3.804.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/eventstream-serde-browser": "^4.0.2", "@smithy/eventstream-serde-config-resolver": "^4.1.0", "@smithy/eventstream-serde-node": "^4.0.2", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-blob-browser": "^4.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/hash-stream-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/md5-js": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.6", "@smithy/middleware-retry": "^4.1.7", "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.14", "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", "@smithy/util-stream": "^4.2.0", "@smithy/util-utf8": "^4.0.0", "@smithy/util-waiter": "^4.0.3", "tslib": "^2.6.2" } }, "sha512-tpJyXuGYIPHIu8G53jXQw3mN5ZK6LdL+tcEF3kRJuQ377Vbo+BSfqaizt9Qb3JuOGcNwCp83jd2LlmcXypN5fg=="],
|
||||
|
||||
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.812.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.6", "@smithy/middleware-retry": "^4.1.7", "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.14", "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-O//smQRj1+RXELB7xX54s5pZB0V69KHXpUZmz8V+8GAYO1FKTHfbpUgK+zyMNb+lFZxG9B69yl8pWPZ/K8bvxA=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.812.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/core": "^3.3.3", "@smithy/node-config-provider": "^4.1.1", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "fast-xml-parser": "4.4.1", "tslib": "^2.6.2" } }, "sha512-myWA9oHMBVDObKrxG+puAkIGs8igcWInQ1PWCRTS/zN4BkhUMFjjh/JPV/4Vzvtvj5E36iujq2WtlrDLl1PpOw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Ge7IEu06ANurGBZx39q9CNN/ncqb1K8lpKZCY969uNWO0/7YPhnplrRJGMZYIS35nD2mBm3ortEKjY/wMZZd5g=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-stream": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Vux2U42vPGXeE407Lp6v3yVA65J7hBO9rB67LXshyGVi7VZLAYWc4mrZxNJNqabEkjcDEmMQQakLPT6zc5SvFw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/credential-provider-env": "3.812.0", "@aws-sdk/credential-provider-http": "3.812.0", "@aws-sdk/credential-provider-process": "3.812.0", "@aws-sdk/credential-provider-sso": "3.812.0", "@aws-sdk/credential-provider-web-identity": "3.812.0", "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-oltqGvQ488xtPY5wrNjbD+qQYYkuCjn30IDE1qKMxJ58EM6UVTQl3XV44Xq07xfF5gKwVJQkfIyOkRAguOVybg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.812.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.812.0", "@aws-sdk/credential-provider-http": "3.812.0", "@aws-sdk/credential-provider-ini": "3.812.0", "@aws-sdk/credential-provider-process": "3.812.0", "@aws-sdk/credential-provider-sso": "3.812.0", "@aws-sdk/credential-provider-web-identity": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/credential-provider-imds": "^4.0.4", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-SnvSWBP6cr9nqx784eETnL2Zl7ZnMB/oJgFVEG1aejAGbT1H9gTpMwuUsBXk4u/mEYe3f1lh1Wqo+HwDgNkfrg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-YI8bb153XeEOb59F9KtTZEwDAc14s2YHZz58+OFiJ2udnKsPV87mNiFhJPW6ba9nmOLXVat5XDcwtVT1b664wg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.812.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.812.0", "@aws-sdk/core": "3.812.0", "@aws-sdk/token-providers": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-ODsPcNhgiO6GOa82TVNskM97mml9rioe9Cbhemz48lkfDQPv1u06NaCR0o3FsvprX1sEhMvJTR3sE1fyEOzvJQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-E9Bmiujvm/Hp9DM/Vc1S+D0pQbx8/x4dR/zyAEZU9EoRq0duQOQ1reWYWbebYmL1OklcVpTfKV0a/VCwuAtGSg=="],
|
||||
|
||||
"@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.808.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "@smithy/util-config-provider": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wEPlNcs8dir9lXbuviEGtSzYSxG/NRKQrJk5ybOc7OpPGHovsN+QhDOdY3lcjOFdwMTiMIG9foUkPz3zBpLB1A=="],
|
||||
|
||||
"@aws-sdk/middleware-expect-continue": ["@aws-sdk/middleware-expect-continue@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-YW1hySBolALMII6C8y7Z0CRG2UX1dGJjLEBNFeefhO/xP7ZuE1dvnmfJGaEuBMnvc3wkRS63VZ3aqX6sevM1CA=="],
|
||||
|
||||
"@aws-sdk/middleware-flexible-checksums": ["@aws-sdk/middleware-flexible-checksums@3.815.0", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/is-array-buffer": "^4.0.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-stream": "^4.2.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-cv/BO7saBbHTrLMUJiClZHM/GB4xDBbJmZ70f9HwcNBP59tBB8TgF/vSyi8SdFM82TvRP+Zzi1AZ8hXcwElaCg=="],
|
||||
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-bum1hLVBrn2lJCi423Z2fMUYtsbkGI2s4N+2RI2WSjvbaVyMSv/WcejIrjkqiiMR+2Y7m5exgoKeg4/TODLDPQ=="],
|
||||
|
||||
"@aws-sdk/middleware-location-constraint": ["@aws-sdk/middleware-location-constraint@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-AMtKnllIWKgoo7hiJfphLYotEwTERfjVMO2+cKAncz9w1g+bnYhHxiVhJJoR94y047c06X4PU5MsTxvdQ73Znw=="],
|
||||
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-w/qLwL3iq0KOPQNat0Kb7sKndl9BtceigINwBU7SpkYWX9L/Lem6f8NPEKrC9Tl4wDBht3Yztub4oRTy/horJA=="],
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zqHOrvLRdsUdN/ehYfZ9Tf8svhbiLLz5VaWUz22YndFv6m9qaAcijkpAOlKexsv3nLBMJdSdJ6GUTAeIy3BZzw=="],
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0", "@smithy/core": "^3.3.3", "@smithy/node-config-provider": "^4.1.1", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.2", "@smithy/util-stream": "^4.2.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-e8AqRRIaTsunL1hqtO1hksa9oTYdsIbfezHUyVpPGugUIB1lMqPt/DlBsanI85OzUD711UfNSEcZ1mqAxpDOoA=="],
|
||||
|
||||
"@aws-sdk/middleware-ssec": ["@aws-sdk/middleware-ssec@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Tk8jK0gOIUBvEPTz/wwSlP1V70zVQ3QYqsLPAjQRMO6zfOK9ax31dln3MgKvFDJxBydS2tS3wsn53v+brxDxTA=="],
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.812.0", "", { "dependencies": { "@aws-sdk/core": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@smithy/core": "^3.3.3", "@smithy/protocol-http": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-r+HFwtSvnAs6Fydp4mijylrTX0og9p/xfxOcKsqhMuk3HpZAIcf9sSjRQI6MBusYklg7pnM4sGEnPAZIrdRotA=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.812.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.812.0", "@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-node": "3.812.0", "@smithy/config-resolver": "^4.1.2", "@smithy/core": "^3.3.3", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.6", "@smithy/middleware-retry": "^4.1.7", "@smithy/middleware-serde": "^4.0.5", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.1.1", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.14", "@smithy/util-defaults-mode-node": "^4.0.14", "@smithy/util-endpoints": "^3.0.4", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.3", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-FS/fImbEpJU3cXtBGR9fyVd+CP51eNKlvTMi3f4/6lSk3RmHjudNC9yEF/og3jtpT3O+7vsNOUW9mHco5IjdQQ=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.808.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/types": "^4.2.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.2", "tslib": "^2.6.2" } }, "sha512-9x2QWfphkARZY5OGkl9dJxZlSlYM2l5inFeo2bKntGuwg4A4YUe5h7d5yJ6sZbam9h43eBrkOdumx03DAkQF9A=="],
|
||||
|
||||
"@aws-sdk/s3-request-presigner": ["@aws-sdk/s3-request-presigner@3.815.0", "", { "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/types": "3.804.0", "@aws-sdk/util-format-url": "3.804.0", "@smithy/middleware-endpoint": "^4.1.6", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.6", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-3xpBwIVPp1xkPXpfGJ2RsgZ+otcwAfAgo5J4buqXkSfBxtJE/dPcaDQEF3AF8APrG/mbuMjkTKIVJ9hNZ34XeA=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.812.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/protocol-http": "^5.1.0", "@smithy/signature-v4": "^5.1.0", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-JTpk3ZHf7TXYbicKfOKi+VrsBTqcAszg9QR9fQmT9aCxPp39gsF3WsXq7NjepwZ5So11ixGIsPE/jtMym399QQ=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.812.0", "", { "dependencies": { "@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/property-provider": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-dbVBaKxrxE708ub5uH3w+cmKIeRQas+2Xf6rpckhohYY+IiflGOdK6aLrp3T6dOQgr/FJ37iQtcYNonAG+yVBQ=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.804.0", "", { "dependencies": { "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-A9qnsy9zQ8G89vrPPlNG9d1d8QcKRGqJKqwyGgS0dclJpwy6d1EWgQLIolKPl6vcFpLoe6avLOLxr+h8ur5wpg=="],
|
||||
|
||||
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.804.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ=="],
|
||||
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.808.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/types": "^4.2.0", "@smithy/util-endpoints": "^3.0.4", "tslib": "^2.6.2" } }, "sha512-N6Lic98uc4ADB7fLWlzx+1uVnq04VgVjngZvwHoujcRg9YDhIg9dUDiTzD5VZv13g1BrPYmvYP1HhsildpGV6w=="],
|
||||
|
||||
"@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/querystring-builder": "^4.0.2", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1nOwSg7B0bj5LFGor0udF/HSdvDuSCxP+NC0IuSOJ5RgJ2AphFo03pLtK2UwArHY5WWZaejAEz5VBND6xxOEhA=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.804.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.804.0", "", { "dependencies": { "@aws-sdk/types": "3.804.0", "@smithy/types": "^4.2.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-KfW6T6nQHHM/vZBBdGn6fMyG/MgX5lq82TDdX4HRQRRuHKLgBWGpKXqqvBwqIaCdXwWHgDrg2VQups6GqOWW2A=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.812.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/types": "3.804.0", "@smithy/node-config-provider": "^4.1.1", "@smithy/types": "^4.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-8pt+OkHhS2U0LDwnzwRnFxyKn8sjSe752OIZQCNv263odud8jQu9pYO2pKqb2kRBk9h9szynjZBDLXfnvSQ7Bg=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.804.0", "", { "dependencies": { "@smithy/types": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-JbGWp36IG9dgxtvC6+YXwt5WDZYfuamWFtVfK6fQpnmL96dx+GUPOXPKRWdw67WLKf2comHY28iX2d3z35I53Q=="],
|
||||
|
||||
"@better-auth/utils": ["@better-auth/utils@0.2.5", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ=="],
|
||||
|
||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||
|
|
@ -182,6 +266,106 @@
|
|||
|
||||
"@simplewebauthn/server": ["@simplewebauthn/server@13.1.1", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8" } }, "sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA=="],
|
||||
|
||||
"@smithy/abort-controller": ["@smithy/abort-controller@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-AqXFf6DXnuRBXy4SoK/n1mfgHaKaq36bmkphmD1KO0nHq6xK/g9KHSW4HEsPQUBCGdIEfuJifGHwxFXPIFay9Q=="],
|
||||
|
||||
"@smithy/chunked-blob-reader": ["@smithy/chunked-blob-reader@5.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw=="],
|
||||
|
||||
"@smithy/chunked-blob-reader-native": ["@smithy/chunked-blob-reader-native@4.0.0", "", { "dependencies": { "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig=="],
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.1.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.2", "@smithy/types": "^4.3.0", "@smithy/util-config-provider": "^4.0.0", "@smithy/util-middleware": "^4.0.3", "tslib": "^2.6.2" } }, "sha512-N5e7ofiyYDmHxnPnqF8L4KtsbSDwyxFRfDK9bp1d9OyPO4ytRLd0/XxCqi5xVaaqB65v4woW8uey6jND6zxzxQ=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.4.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.0.6", "@smithy/protocol-http": "^5.1.1", "@smithy/types": "^4.3.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.3", "@smithy/util-stream": "^4.2.1", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-dDYISQo7k0Ml/rXlFIjkTmTcQze/LxhtIRAEmZ6HJ/EI0inVxVEVnrUXJ7jPx6ZP0GHUhFm40iQcCgS5apXIXA=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.0.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.2", "@smithy/property-provider": "^4.0.3", "@smithy/types": "^4.3.0", "@smithy/url-parser": "^4.0.3", "tslib": "^2.6.2" } }, "sha512-saEAGwrIlkb9XxX/m5S5hOtzjoJPEK6Qw2f9pYTbIsMPOFyGSXBBTw95WbOyru8A1vIS2jVCCU1Qhz50QWG3IA=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.0", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-V22KIPXZsE2mc4zEgYGANM/7UbL9jWlOACEolyGyMuTY+jjHJ2PQ0FdopOTS1CS7u6PlAkALmypkv2oQ4aftcg=="],
|
||||
|
||||
"@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.0.3", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-oe1d/tfCGVZBMX8O6HApaM4G+fF9JNdyLP7tWXt00epuL/kLOdp/4o9VqheLFeJaXgao+9IaBgs/q/oM48hxzg=="],
|
||||
|
||||
"@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.1.1", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-XXCPGjRNwpFWHKQJMKIjGLfFKYULYckFnxGcWmBC2mBf3NsrvUKgqHax4NCqc0TfbDAimPDHOc6HOKtzsXK9Gw=="],
|
||||
|
||||
"@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.0.3", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-HOEbRmm9TrikCoFrypYu0J/gC4Lsk8gl5LtOz1G3laD2Jy44+ht2Pd2E9qjNQfhMJIzKDZ/gbuUH0s0v4kWQ0A=="],
|
||||
|
||||
"@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.0.3", "", { "dependencies": { "@smithy/eventstream-codec": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-ShOP512CZrYI9n+h64PJ84udzoNHUQtPddyh1j175KNTKsSnMEDNscOWJWyEoLQiuhWWw51lSa+k6ea9ZGXcRg=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.0.3", "", { "dependencies": { "@smithy/protocol-http": "^5.1.1", "@smithy/querystring-builder": "^4.0.3", "@smithy/types": "^4.3.0", "@smithy/util-base64": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-yBZwavI31roqTndNI7ONHqesfH01JmjJK6L3uUpZAhyAmr86LN5QiPzfyZGIxQmed8VEK2NRSQT3/JX5V1njfQ=="],
|
||||
|
||||
"@smithy/hash-blob-browser": ["@smithy/hash-blob-browser@4.0.3", "", { "dependencies": { "@smithy/chunked-blob-reader": "^5.0.0", "@smithy/chunked-blob-reader-native": "^4.0.0", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-37wZYU/XI2cOF4hgNDNMzZNAuNtJTkZFWxcpagQrnf6PYU/6sJ6y5Ey9Bp4vzi9nteex/ImxAugfsF3XGLrqWA=="],
|
||||
|
||||
"@smithy/hash-node": ["@smithy/hash-node@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-W5Uhy6v/aYrgtjh9y0YP332gIQcwccQ+EcfWhllL0B9rPae42JngTTUpb8W6wuxaNFzqps4xq5klHckSSOy5fw=="],
|
||||
|
||||
"@smithy/hash-stream-node": ["@smithy/hash-stream-node@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CAwAvztwGYHHZGGcXtbinNxytaj5FNZChz8V+o7eNUAi5BgVqnF91Z3cJSmaE9O7FYUQVrIzGAB25Aok9T5KHQ=="],
|
||||
|
||||
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-1Bo8Ur1ZGqxvwTqBmv6DZEn0rXtwJGeqiiO2/JFcCtz3nBakOqeXbJBElXJMMzd0ghe8+eB6Dkw98nMYctgizg=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="],
|
||||
|
||||
"@smithy/md5-js": ["@smithy/md5-js@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-m95Z+1UJFPq4cv/R6TPMLYkoau7cNJYA5GLuuUJjfmF+Zrad4yaupIWeGGzIinf8pD1L+CIAxjh8eowPvyL7Dw=="],
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.0.3", "", { "dependencies": { "@smithy/protocol-http": "^5.1.1", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-NE/Zph4BP5u16bzYq2csq9qD0T6UBLeg4AuNrwNJ7Gv9uLYaGEgelZUOdRndGdMGcUfSGvNlXGb2aA2hPCwJ6g=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.1.7", "", { "dependencies": { "@smithy/core": "^3.4.0", "@smithy/middleware-serde": "^4.0.6", "@smithy/node-config-provider": "^4.1.2", "@smithy/shared-ini-file-loader": "^4.0.3", "@smithy/types": "^4.3.0", "@smithy/url-parser": "^4.0.3", "@smithy/util-middleware": "^4.0.3", "tslib": "^2.6.2" } }, "sha512-KDzM7Iajo6K7eIWNNtukykRT4eWwlHjCEsULZUaSfi/SRSBK8BPRqG5FsVfp58lUxcvre8GT8AIPIqndA0ERKw=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.1.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.2", "@smithy/protocol-http": "^5.1.1", "@smithy/service-error-classification": "^4.0.4", "@smithy/smithy-client": "^4.3.0", "@smithy/types": "^4.3.0", "@smithy/util-middleware": "^4.0.3", "@smithy/util-retry": "^4.0.4", "tslib": "^2.6.2", "uuid": "^9.0.1" } }, "sha512-e2OtQgFzzlSG0uCjcJmi02QuFSRTrpT11Eh2EcqqDFy7DYriteHZJkkf+4AsxsrGDugAtPFcWBz1aq06sSX5fQ=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.0.6", "", { "dependencies": { "@smithy/protocol-http": "^5.1.1", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-YECyl7uNII+jCr/9qEmCu8xYL79cU0fqjo0qxpcVIU18dAPHam/iYwcknAu4Jiyw1uN+sAx7/SMf/Kmef/Jjsg=="],
|
||||
|
||||
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-baeV7t4jQfQtFxBADFmnhmqBmqR38dNU5cvEgHcMK/Kp3D3bEI0CouoX2Sr/rGuntR+Eg0IjXdxnGGTc6SbIkw=="],
|
||||
|
||||
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.1.2", "", { "dependencies": { "@smithy/property-provider": "^4.0.3", "@smithy/shared-ini-file-loader": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-SUvNup8iU1v7fmM8XPk+27m36udmGCfSz+VZP5Gb0aJ3Ne0X28K/25gnsrg3X1rWlhcnhzNUUysKW/Ied46ivQ=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.0.5", "", { "dependencies": { "@smithy/abort-controller": "^4.0.3", "@smithy/protocol-http": "^5.1.1", "@smithy/querystring-builder": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-T7QglZC1vS7SPT44/1qSIAQEx5bFKb3LfO6zw/o4Xzt1eC5HNoH1TkS4lMYA9cWFbacUhx4hRl/blLun4EOCkg=="],
|
||||
|
||||
"@smithy/property-provider": ["@smithy/property-provider@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-Wcn17QNdawJZcZZPBuMuzyBENVi1AXl4TdE0jvzo4vWX2x5df/oMlmr/9M5XAAC6+yae4kWZlOYIsNsgDrMU9A=="],
|
||||
|
||||
"@smithy/protocol-http": ["@smithy/protocol-http@5.1.1", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-Vsay2mzq05DwNi9jK01yCFtfvu9HimmgC7a4HTs7lhX12Sx8aWsH0mfz6q/02yspSp+lOB+Q2HJwi4IV2GKz7A=="],
|
||||
|
||||
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-UUzIWMVfPmDZcOutk2/r1vURZqavvQW0OHvgsyNV0cKupChvqg+/NKPRMaMEe+i8tP96IthMFeZOZWpV+E4RAw=="],
|
||||
|
||||
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-K5M4ZJQpFCblOJ5Oyw7diICpFg1qhhR47m2/5Ef1PhGE19RaIZf50tjYFrxa6usqcuXyTiFPGo4d1geZdH4YcQ=="],
|
||||
|
||||
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.0.4", "", { "dependencies": { "@smithy/types": "^4.3.0" } }, "sha512-W5ScbQ1bTzgH91kNEE2CvOzM4gXlDOqdow4m8vMFSIXCel2scbHwjflpVNnC60Y3F1m5i7w2gQg9lSnR+JsJAA=="],
|
||||
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-vHwlrqhZGIoLwaH8vvIjpHnloShqdJ7SUPNM2EQtEox+yEDFTVQ7E+DLZ+6OhnYEgFUwPByJyz6UZaOu2tny6A=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.1.1", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "@smithy/protocol-http": "^5.1.1", "@smithy/types": "^4.3.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-middleware": "^4.0.3", "@smithy/util-uri-escape": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-zy8Repr5zvT0ja+Tf5wjV/Ba6vRrhdiDcp/ww6cvqYbSEudIkziDe3uppNRlFoCViyJXdPnLcwyZdDLA4CHzSg=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.3.0", "", { "dependencies": { "@smithy/core": "^3.4.0", "@smithy/middleware-endpoint": "^4.1.7", "@smithy/middleware-stack": "^4.0.3", "@smithy/protocol-http": "^5.1.1", "@smithy/types": "^4.3.0", "@smithy/util-stream": "^4.2.1", "tslib": "^2.6.2" } }, "sha512-DNsRA38pN6tYHUjebmwD9e4KcgqTLldYQb2gC6K+oxXYdCTxPn6wV9+FvOa6wrU2FQEnGJoi+3GULzOTKck/tg=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.3.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+1iaIQHthDh9yaLhRzaoQxRk+l9xlk+JjMFxGRhNLz+m9vKOkjNeU8QuB4w3xvzHyVR/BVlp/4AXDHjoRIkfgQ=="],
|
||||
|
||||
"@smithy/url-parser": ["@smithy/url-parser@4.0.3", "", { "dependencies": { "@smithy/querystring-parser": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-n5/DnosDu/tweOqUUNtUbu7eRIR4J/Wz9nL7V5kFYQQVb8VYdj7a4G5NJHCw6o21ul7CvZoJkOpdTnsQDLT0tQ=="],
|
||||
|
||||
"@smithy/util-base64": ["@smithy/util-base64@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg=="],
|
||||
|
||||
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA=="],
|
||||
|
||||
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.0.15", "", { "dependencies": { "@smithy/property-provider": "^4.0.3", "@smithy/smithy-client": "^4.3.0", "@smithy/types": "^4.3.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-bJJ/B8owQbHAflatSq92f9OcV8858DJBQF1Y3GRjB8psLyUjbISywszYPFw16beREHO/C3I3taW4VGH+tOuwrQ=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.0.15", "", { "dependencies": { "@smithy/config-resolver": "^4.1.3", "@smithy/credential-provider-imds": "^4.0.5", "@smithy/node-config-provider": "^4.1.2", "@smithy/property-provider": "^4.0.3", "@smithy/smithy-client": "^4.3.0", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-8CUrEW2Ni5q+NmYkj8wsgkfqoP7l4ZquptFbq92yQE66xevc4SxqP2zH6tMtN158kgBqBDsZ+qlrRwXWOjCR8A=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.0.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.1.2", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-PjDpqLk24/vAl340tmtCA++Q01GRRNH9cwL9qh46NspAX9S+IQVcK+GOzPt0GLJ6KYGyn8uOgo2kvJhiThclJw=="],
|
||||
|
||||
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="],
|
||||
|
||||
"@smithy/util-middleware": ["@smithy/util-middleware@4.0.3", "", { "dependencies": { "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-iIsC6qZXxkD7V3BzTw3b1uK8RVC1M8WvwNxK1PKrH9FnxntCd30CSunXjL/8iJBE8Z0J14r2P69njwIpRG4FBQ=="],
|
||||
|
||||
"@smithy/util-retry": ["@smithy/util-retry@4.0.4", "", { "dependencies": { "@smithy/service-error-classification": "^4.0.4", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-Aoqr9W2jDYGrI6OxljN8VmLDQIGO4VdMAUKMf9RGqLG8hn6or+K41NEy1Y5dtum9q8F7e0obYAuKl2mt/GnpZg=="],
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.2.1", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.0.3", "@smithy/node-http-handler": "^4.0.5", "@smithy/types": "^4.3.0", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", "@smithy/util-hex-encoding": "^4.0.0", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-W3IR0x5DY6iVtjj5p902oNhD+Bz7vs5S+p6tppbPa509rV9BdeXZjGuRSCtVEad9FA0Mba+tNUtUmtnSI1nwUw=="],
|
||||
|
||||
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
|
||||
|
||||
"@smithy/util-waiter": ["@smithy/util-waiter@4.0.4", "", { "dependencies": { "@smithy/abort-controller": "^4.0.3", "@smithy/types": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-73aeIvHjtSB6fd9I08iFaQIGTICKpLrI3EtlWAkStVENGo1ARMq9qdoD4QwkY0RUp6A409xlgbD9NCCfCF5ieg=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
|
||||
|
||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@3.3.1", "", { "dependencies": { "import-meta-resolve": "^4.1.0" }, "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ=="],
|
||||
|
|
@ -270,6 +454,8 @@
|
|||
|
||||
"bits-ui": ["bits-ui@1.5.3", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-BTZ9/GU11DaEGyQp+AY+sXCMLZO0gbDC5J8l7+Ngj4Vf6hNOwrpMmoh5iuKktA6cphXYolVkUDgBWmkh415I+w=="],
|
||||
|
||||
"bowser": ["bowser@2.11.0", "", {}, "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
|
@ -336,6 +522,8 @@
|
|||
|
||||
"fancy-canvas": ["fancy-canvas@2.1.0", "", {}, "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@4.4.1", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw=="],
|
||||
|
||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||
|
||||
"fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="],
|
||||
|
|
@ -404,6 +592,8 @@
|
|||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mode-watcher": ["mode-watcher@1.0.7", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-ZGA7ZGdOvBJeTQkzdBOnXSgTkO6U6iIFWJoyGCTt6oHNg9XP9NBvS26De+V4W2aqI+B0yYXUskFG2VnEo3zyMQ=="],
|
||||
|
||||
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
|
@ -464,6 +654,8 @@
|
|||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
|
||||
|
||||
"svelte": ["svelte@5.32.0", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-2WXcm+mx4D99pb5gvGYiEC6EkPOfzCcgXxcVrxMkAythwzYH5Frr29i3C431U4B8LxXRh9WnFPAz+OzIcFdM7g=="],
|
||||
|
|
@ -472,6 +664,8 @@
|
|||
|
||||
"svelte-lightweight-charts": ["svelte-lightweight-charts@2.2.0", "", { "peerDependencies": { "lightweight-charts": ">=4.0.0", "svelte": ">=3.44.0" } }, "sha512-LXdha4vfLMuOPc0Yyetu+DLSDJkPryGkufUQgpCkfguCscSQUcrLiI9MdqKwQk6Fkm6AZbg8SQ5qDIYxcyC+Dg=="],
|
||||
|
||||
"svelte-sonner": ["svelte-sonner@1.0.2", "", { "dependencies": { "runed": "^0.26.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-hoidgv7hrk3XNZzjXj6/frsvZOiUOtf7Tn2du/hTu1G9PchJGEoy4EpIKyfhVKBiBrxaNFaYPFxN4pHLHCoe/w=="],
|
||||
|
||||
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
|
|
@ -500,6 +694,8 @@
|
|||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"vite": ["vite@5.4.19", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA=="],
|
||||
|
||||
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
|
||||
|
|
@ -510,6 +706,12 @@
|
|||
|
||||
"zod": ["zod@3.25.17", "", {}, "sha512-8hQzQ/kMOIFbwOgPrm9Sf9rtFHpFUMy4HvN0yEB0spw14aYi0uT5xG5CE2DB9cd51GWNsz+DNO7se1kztHMKnw=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@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/sha256-browser/@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=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||
|
|
@ -530,10 +732,20 @@
|
|||
|
||||
"d3-time/d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"mode-watcher/runed": ["runed@0.25.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg=="],
|
||||
|
||||
"svelte-sonner/runed": ["runed@0.26.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-qWFv0cvLVRd8pdl/AslqzvtQyEn5KaIugEernwg9G98uJVSZcs/ygvPBvF80LA46V8pwRvSKnaVLDI3+i2wubw=="],
|
||||
|
||||
"tailwind-variants/tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"@aws-crypto/sha1-browser/@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/sha256-browser/@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=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
|
@ -623,5 +835,11 @@
|
|||
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"@aws-crypto/sha1-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=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,10 @@ CREATE TABLE IF NOT EXISTS "user" (
|
|||
"is_banned" boolean DEFAULT false,
|
||||
"ban_reason" text,
|
||||
"base_currency_balance" numeric(19, 4) DEFAULT '10000.0000' NOT NULL,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
"bio" varchar(160) DEFAULT 'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”',
|
||||
"username" varchar(30) NOT NULL,
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email"),
|
||||
CONSTRAINT "user_username_unique" UNIQUE("username")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "user_portfolio" (
|
||||
1
website/drizzle/0001_last_selene.sql
Normal file
1
website/drizzle/0001_last_selene.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "coin" ADD COLUMN "icon" text;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"id": "d8f103f7-02e7-4506-95a5-4993abe53030",
|
||||
"id": "75ce764f-a039-4e66-9ebb-620e13d1fa85",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
|
|
@ -530,6 +530,19 @@
|
|||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'10000.0000'"
|
||||
},
|
||||
"bio": {
|
||||
"name": "bio",
|
||||
"type": "varchar(160)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'"
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
|
|
@ -542,6 +555,13 @@
|
|||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
709
website/drizzle/meta/0001_snapshot.json
Normal file
709
website/drizzle/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,709 @@
|
|||
{
|
||||
"id": "a272e8ea-cd8d-4f01-b826-5c4ea55499c4",
|
||||
"prevId": "75ce764f-a039-4e66-9ebb-620e13d1fa85",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.coin": {
|
||||
"name": "coin",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"symbol": {
|
||||
"name": "symbol",
|
||||
"type": "varchar(10)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"creator_id": {
|
||||
"name": "creator_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"initial_supply": {
|
||||
"name": "initial_supply",
|
||||
"type": "numeric(28, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"circulating_supply": {
|
||||
"name": "circulating_supply",
|
||||
"type": "numeric(28, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"current_price": {
|
||||
"name": "current_price",
|
||||
"type": "numeric(19, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"market_cap": {
|
||||
"name": "market_cap",
|
||||
"type": "numeric(28, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"volume_24h": {
|
||||
"name": "volume_24h",
|
||||
"type": "numeric(28, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'0.0000'"
|
||||
},
|
||||
"change_24h": {
|
||||
"name": "change_24h",
|
||||
"type": "numeric(8, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'0.0000'"
|
||||
},
|
||||
"pool_coin_amount": {
|
||||
"name": "pool_coin_amount",
|
||||
"type": "numeric(28, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'0.00000000'"
|
||||
},
|
||||
"pool_base_currency_amount": {
|
||||
"name": "pool_base_currency_amount",
|
||||
"type": "numeric(28, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'0.0000'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"is_listed": {
|
||||
"name": "is_listed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"coin_creator_id_user_id_fk": {
|
||||
"name": "coin_creator_id_user_id_fk",
|
||||
"tableFrom": "coin",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"creator_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"coin_symbol_unique": {
|
||||
"name": "coin_symbol_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"symbol"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.price_history": {
|
||||
"name": "price_history",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"coin_id": {
|
||||
"name": "coin_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"type": "numeric(19, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"price_history_coin_id_coin_id_fk": {
|
||||
"name": "price_history_coin_id_coin_id_fk",
|
||||
"tableFrom": "price_history",
|
||||
"tableTo": "coin",
|
||||
"columnsFrom": [
|
||||
"coin_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.transaction": {
|
||||
"name": "transaction",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"coin_id": {
|
||||
"name": "coin_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "transaction_type",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "numeric(28, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"price_per_coin": {
|
||||
"name": "price_per_coin",
|
||||
"type": "numeric(19, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"total_base_currency_amount": {
|
||||
"name": "total_base_currency_amount",
|
||||
"type": "numeric(28, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"transaction_user_id_user_id_fk": {
|
||||
"name": "transaction_user_id_user_id_fk",
|
||||
"tableFrom": "transaction",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"transaction_coin_id_coin_id_fk": {
|
||||
"name": "transaction_coin_id_coin_id_fk",
|
||||
"tableFrom": "transaction",
|
||||
"tableTo": "coin",
|
||||
"columnsFrom": [
|
||||
"coin_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"is_admin": {
|
||||
"name": "is_admin",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"is_banned": {
|
||||
"name": "is_banned",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": false
|
||||
},
|
||||
"ban_reason": {
|
||||
"name": "ban_reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"base_currency_balance": {
|
||||
"name": "base_currency_balance",
|
||||
"type": "numeric(19, 4)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'10000.0000'"
|
||||
},
|
||||
"bio": {
|
||||
"name": "bio",
|
||||
"type": "varchar(160)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”'"
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "varchar(30)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"public.user_portfolio": {
|
||||
"name": "user_portfolio",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"coin_id": {
|
||||
"name": "coin_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"quantity": {
|
||||
"name": "quantity",
|
||||
"type": "numeric(28, 8)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_portfolio_user_id_user_id_fk": {
|
||||
"name": "user_portfolio_user_id_user_id_fk",
|
||||
"tableFrom": "user_portfolio",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_portfolio_coin_id_coin_id_fk": {
|
||||
"name": "user_portfolio_coin_id_coin_id_fk",
|
||||
"tableFrom": "user_portfolio",
|
||||
"tableTo": "coin",
|
||||
"columnsFrom": [
|
||||
"coin_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_portfolio_user_id_coin_id_pk": {
|
||||
"name": "user_portfolio_user_id_coin_id_pk",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"coin_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "serial",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.transaction_type": {
|
||||
"name": "transaction_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"BUY",
|
||||
"SELL"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,15 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1747913743324,
|
||||
"tag": "0000_romantic_firebrand",
|
||||
"when": 1747924055062,
|
||||
"tag": "0000_giant_vanisher",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1747991689472,
|
||||
"tag": "0001_last_selene",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^1.0.2",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^4.1.7",
|
||||
|
|
@ -37,6 +38,8 @@
|
|||
"vite": "^5.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.815.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.815.0",
|
||||
"@tailwindcss/postcss": "^4.1.7",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@visx/scale": "^3.12.0",
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
@import "tw-animate-css";
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
|
||||
|
||||
:root {
|
||||
--radius: 0.5rem;
|
||||
--success: oklch(0.637 0.237 205.331);
|
||||
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
|
|
@ -96,6 +99,7 @@
|
|||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-success: var(--success);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// src/lib/auth.ts (or your auth config file)
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { db } from "./server/db";
|
||||
import * as schema from "./server/db/schema";
|
||||
import { generateUsername } from "./utils/random";
|
||||
import { uploadProfilePicture } from "./server/s3";
|
||||
|
||||
if (!env.GOOGLE_CLIENT_ID) throw new Error('GOOGLE_CLIENT_ID is not set');
|
||||
if (!env.GOOGLE_CLIENT_SECRET) throw new Error('GOOGLE_CLIENT_SECRET is not set');
|
||||
|
|
@ -13,44 +17,67 @@ export const auth = betterAuth({
|
|||
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: schema,
|
||||
}),
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
}
|
||||
},
|
||||
mapProfileToUser: async (profile) => {
|
||||
const newUsername = generateUsername();
|
||||
let s3ImageKey: string | null = null;
|
||||
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 60 * 5, // 5 minutes
|
||||
if (profile.picture) {
|
||||
try {
|
||||
const response = await fetch(profile.picture);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to fetch profile picture: ${response.statusText}`);
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
s3ImageKey = await uploadProfilePicture(
|
||||
profile.sub, // Using Google 'sub' for a unique identifier
|
||||
new Uint8Array(arrayBuffer),
|
||||
blob.type,
|
||||
blob.size
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to upload profile picture during social login:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: s3ImageKey, // Store S3 key in the standard 'image' field
|
||||
username: newUsername,
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
user: {
|
||||
additionalFields: {
|
||||
isAdmin: {
|
||||
type: "boolean",
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
input: false
|
||||
},
|
||||
isBanned: {
|
||||
type: "boolean",
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
input: false
|
||||
},
|
||||
banReason: {
|
||||
type: "string",
|
||||
required: false,
|
||||
defaultValue: null,
|
||||
input: false
|
||||
username: { type: "string", required: true, input: false },
|
||||
isAdmin: { type: "boolean", required: false, input: false },
|
||||
isBanned: { type: "boolean", required: false, input: false },
|
||||
banReason: { type: "string", required: false, input: false },
|
||||
baseCurrencyBalance: { type: "string", required: false, input: false },
|
||||
bio: { type: "string", required: false },
|
||||
// Ensure 'image' is not listed here if it's a core field,
|
||||
// or ensure 'avatarUrl' is used consistently if it is an additional field.
|
||||
// Based on current setup, 'image' is core.
|
||||
}
|
||||
},
|
||||
deleteUser: { enabled: true }
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 60 * 5,
|
||||
}
|
||||
},
|
||||
advanced: {
|
||||
database: {
|
||||
generateId: false,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import * as Sidebar from '$lib/components/ui/sidebar';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import {
|
||||
Moon,
|
||||
Sun,
|
||||
|
|
@ -15,15 +16,19 @@
|
|||
BadgeCheckIcon,
|
||||
CreditCardIcon,
|
||||
BellIcon,
|
||||
LogOutIcon
|
||||
LogOutIcon,
|
||||
Wallet
|
||||
} from 'lucide-svelte';
|
||||
import { mode, setMode } from 'mode-watcher';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/index.js';
|
||||
|
||||
import SignInConfirmDialog from './SignInConfirmDialog.svelte';
|
||||
import { signOut } from '$lib/auth-client';
|
||||
import { getPublicUrl } from '$lib/utils';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const data = {
|
||||
navMain: [
|
||||
|
|
@ -39,6 +44,15 @@
|
|||
const { setOpenMobile, isMobile } = useSidebar();
|
||||
let shouldSignIn = $state(false);
|
||||
|
||||
// Fetch portfolio data when user is authenticated
|
||||
$effect(() => {
|
||||
if ($USER_DATA) {
|
||||
fetchPortfolioData();
|
||||
} else {
|
||||
PORTFOLIO_DATA.set(null);
|
||||
}
|
||||
});
|
||||
|
||||
function handleNavClick(title: string) {
|
||||
setOpenMobile(false);
|
||||
}
|
||||
|
|
@ -47,6 +61,13 @@
|
|||
setMode(mode.current === 'light' ? 'dark' : 'light');
|
||||
setOpenMobile(false);
|
||||
}
|
||||
|
||||
function formatCurrency(value: number): string {
|
||||
return value.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||
|
|
@ -121,6 +142,36 @@
|
|||
</Sidebar.Menu>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
|
||||
<!-- Portfolio Summary -->
|
||||
{#if $USER_DATA && $PORTFOLIO_DATA}
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>Portfolio</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupContent>
|
||||
<div class="px-2 py-1 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Wallet class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">Total Value</span>
|
||||
</div>
|
||||
<Badge variant="secondary" class="font-mono">
|
||||
${formatCurrency($PORTFOLIO_DATA.totalValue)}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="space-y-1 text-xs text-muted-foreground">
|
||||
<div class="flex justify-between">
|
||||
<span>Cash:</span>
|
||||
<span class="font-mono">${formatCurrency($PORTFOLIO_DATA.baseCurrencyBalance)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>Coins:</span>
|
||||
<span class="font-mono">${formatCurrency($PORTFOLIO_DATA.totalCoinValue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Sidebar.GroupContent>
|
||||
</Sidebar.Group>
|
||||
{/if}
|
||||
</Sidebar.Content>
|
||||
|
||||
{#if $USER_DATA}
|
||||
|
|
@ -136,13 +187,12 @@
|
|||
{...props}
|
||||
>
|
||||
<Avatar.Root class="size-8 rounded-lg">
|
||||
<Avatar.Image src={$USER_DATA.image} alt={$USER_DATA.name} />
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
<Avatar.Image src={getPublicUrl($USER_DATA.image)} alt={$USER_DATA.name} />
|
||||
<Avatar.Fallback class="rounded-lg">?</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-medium">{$USER_DATA.name}</span>
|
||||
<span class="truncate text-xs">$35,674.34</span>
|
||||
<!-- TODO: replace with actual db entry -->
|
||||
<span class="truncate text-xs">@{$USER_DATA.username}</span>
|
||||
</div>
|
||||
<ChevronsUpDownIcon class="ml-auto size-4" />
|
||||
</Sidebar.MenuButton>
|
||||
|
|
@ -157,12 +207,12 @@
|
|||
<DropdownMenu.Label class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar.Root class="size-8 rounded-lg">
|
||||
<Avatar.Image src={$USER_DATA.image} alt={$USER_DATA.name} />
|
||||
<Avatar.Fallback class="rounded-lg">CN</Avatar.Fallback>
|
||||
<Avatar.Image src={getPublicUrl($USER_DATA.image)} alt={$USER_DATA.name} />
|
||||
<Avatar.Fallback class="rounded-lg">?</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-medium">{$USER_DATA.name}</span>
|
||||
<span class="truncate text-xs">{$USER_DATA.email}</span>
|
||||
<span class="truncate text-xs">@{$USER_DATA.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Label>
|
||||
|
|
@ -175,7 +225,7 @@
|
|||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => goto('/settings')}>
|
||||
<BadgeCheckIcon />
|
||||
Account
|
||||
</DropdownMenu.Item>
|
||||
|
|
|
|||
23
website/src/lib/components/ui/alert/alert-description.svelte
Normal file
23
website/src/lib/components/ui/alert/alert-description.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-description"
|
||||
class={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
20
website/src/lib/components/ui/alert/alert-title.svelte
Normal file
20
website/src/lib/components/ui/alert/alert-title.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert-title"
|
||||
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
44
website/src/lib/components/ui/alert/alert.svelte
Normal file
44
website/src/lib/components/ui/alert/alert.svelte
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from "tailwind-variants";
|
||||
|
||||
export const alertVariants = tv({
|
||||
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
variant = "default",
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
||||
variant?: AlertVariant;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
data-slot="alert"
|
||||
class={cn(alertVariants({ variant }), className)}
|
||||
{...restProps}
|
||||
role="alert"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
14
website/src/lib/components/ui/alert/index.ts
Normal file
14
website/src/lib/components/ui/alert/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Root from "./alert.svelte";
|
||||
import Description from "./alert-description.svelte";
|
||||
import Title from "./alert-title.svelte";
|
||||
export { alertVariants, type AlertVariant } from "./alert.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Description,
|
||||
Title,
|
||||
//
|
||||
Root as Alert,
|
||||
Description as AlertDescription,
|
||||
Title as AlertTitle,
|
||||
};
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: HoverCardPrimitive.ContentProps & {
|
||||
portalProps?: HoverCardPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<HoverCardPrimitive.Portal {...portalProps}>
|
||||
<HoverCardPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="hover-card-content"
|
||||
{align}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-hidden z-50 mt-3 w-64 rounded-md border p-4 shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: HoverCardPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<HoverCardPrimitive.Trigger bind:ref data-slot="hover-card-trigger" {...restProps} />
|
||||
14
website/src/lib/components/ui/hover-card/index.ts
Normal file
14
website/src/lib/components/ui/hover-card/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
|
||||
import Content from "./hover-card-content.svelte";
|
||||
import Trigger from "./hover-card-trigger.svelte";
|
||||
|
||||
const Root = HoverCardPrimitive.Root;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Content,
|
||||
Trigger,
|
||||
Root as HoverCard,
|
||||
Content as HoverCardContent,
|
||||
Trigger as HoverCardTrigger,
|
||||
};
|
||||
7
website/src/lib/components/ui/label/index.ts
Normal file
7
website/src/lib/components/ui/label/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
20
website/src/lib/components/ui/label/label.svelte
Normal file
20
website/src/lib/components/ui/label/label.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
data-slot="label"
|
||||
class={cn(
|
||||
"flex select-none items-center gap-2 text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
1
website/src/lib/components/ui/sonner/index.ts
Normal file
1
website/src/lib/components/ui/sonner/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as Toaster } from "./sonner.svelte";
|
||||
13
website/src/lib/components/ui/sonner/sonner.svelte
Normal file
13
website/src/lib/components/ui/sonner/sonner.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { Toaster as Sonner, type ToasterProps as SonnerProps } from "svelte-sonner";
|
||||
import { mode } from "mode-watcher";
|
||||
|
||||
let { ...restProps }: SonnerProps = $props();
|
||||
</script>
|
||||
|
||||
<Sonner
|
||||
theme={mode.current}
|
||||
class="toaster group"
|
||||
style="--normal-bg: var(--popover); --normal-text: var(--popover-foreground); --normal-border: var(--border);"
|
||||
{...restProps}
|
||||
/>
|
||||
7
website/src/lib/components/ui/textarea/index.ts
Normal file
7
website/src/lib/components/ui/textarea/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import Root from "./textarea.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Textarea,
|
||||
};
|
||||
22
website/src/lib/components/ui/textarea/textarea.svelte
Normal file
22
website/src/lib/components/ui/textarea/textarea.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
|
||||
import type { HTMLTextareaAttributes } from "svelte/elements";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildren<WithElementRef<HTMLTextareaAttributes>> = $props();
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
bind:this={ref}
|
||||
data-slot="textarea"
|
||||
class={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 field-sizing-content shadow-xs flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
bind:value
|
||||
{...restProps}
|
||||
></textarea>
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
export interface Coin {
|
||||
id: number;
|
||||
name: string;
|
||||
symbol: string;
|
||||
price: number;
|
||||
change24h: number;
|
||||
volume24h: number;
|
||||
marketCap: number;
|
||||
priceHistory: { date: string; price: number }[];
|
||||
}
|
||||
|
||||
export const coins: Coin[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Bitcoin',
|
||||
symbol: 'BTC',
|
||||
price: 67890.42,
|
||||
change24h: 2.3,
|
||||
volume24h: 28500000000,
|
||||
marketCap: 1320000000000,
|
||||
priceHistory: [
|
||||
{ date: '2025-05-14', price: 66250.18 },
|
||||
{ date: '2025-05-15', price: 65890.34 },
|
||||
{ date: '2025-05-16', price: 66780.12 },
|
||||
{ date: '2025-05-17', price: 66920.45 },
|
||||
{ date: '2025-05-18', price: 67120.78 },
|
||||
{ date: '2025-05-19', price: 67450.23 },
|
||||
{ date: '2025-05-20', price: 67890.42 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Ethereum',
|
||||
symbol: 'ETH',
|
||||
price: 3456.78,
|
||||
change24h: -1.2,
|
||||
volume24h: 15200000000,
|
||||
marketCap: 420000000000,
|
||||
priceHistory: [
|
||||
{ date: '2025-05-14', price: 3520.45 },
|
||||
{ date: '2025-05-15', price: 3490.23 },
|
||||
{ date: '2025-05-16', price: 3475.67 },
|
||||
{ date: '2025-05-17', price: 3460.12 },
|
||||
{ date: '2025-05-18', price: 3470.54 },
|
||||
{ date: '2025-05-19', price: 3465.89 },
|
||||
{ date: '2025-05-20', price: 3456.78 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Ripple',
|
||||
symbol: 'XRP',
|
||||
price: 0.54,
|
||||
change24h: 5.7,
|
||||
volume24h: 2100000000,
|
||||
marketCap: 28500000000,
|
||||
priceHistory: [
|
||||
{ date: '2025-05-14', price: 0.49 },
|
||||
{ date: '2025-05-15', price: 0.50 },
|
||||
{ date: '2025-05-16', price: 0.51 },
|
||||
{ date: '2025-05-17', price: 0.52 },
|
||||
{ date: '2025-05-18', price: 0.53 },
|
||||
{ date: '2025-05-19', price: 0.54 },
|
||||
{ date: '2025-05-20', price: 0.54 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Solana',
|
||||
symbol: 'SOL',
|
||||
price: 156.89,
|
||||
change24h: 7.2,
|
||||
volume24h: 5600000000,
|
||||
marketCap: 67800000000,
|
||||
priceHistory: [
|
||||
{ date: '2025-05-14', price: 142.34 },
|
||||
{ date: '2025-05-15', price: 145.67 },
|
||||
{ date: '2025-05-16', price: 148.90 },
|
||||
{ date: '2025-05-17', price: 150.25 },
|
||||
{ date: '2025-05-18', price: 152.30 },
|
||||
{ date: '2025-05-19', price: 154.75 },
|
||||
{ date: '2025-05-20', price: 156.89 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Dogecoin',
|
||||
symbol: 'DOGE',
|
||||
price: 0.12,
|
||||
change24h: -2.5,
|
||||
volume24h: 980000000,
|
||||
marketCap: 16500000000,
|
||||
priceHistory: [
|
||||
{ date: '2025-05-14', price: 0.125 },
|
||||
{ date: '2025-05-15', price: 0.124 },
|
||||
{ date: '2025-05-16', price: 0.123 },
|
||||
{ date: '2025-05-17', price: 0.122 },
|
||||
{ date: '2025-05-18', price: 0.121 },
|
||||
{ date: '2025-05-19', price: 0.120 },
|
||||
{ date: '2025-05-20', price: 0.120 }
|
||||
]
|
||||
}
|
||||
];
|
||||
9
website/src/lib/data/constants.ts
Normal file
9
website/src/lib/data/constants.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// FILE UPLOAD
|
||||
export const MAX_FILE_SIZE = 1 * 1024 * 1024; // 1MB
|
||||
|
||||
// COIN CREATION COSTS
|
||||
export const CREATION_FEE = 100; // $100 creation fee
|
||||
export const FIXED_SUPPLY = 1000000000; // 1 billion tokens
|
||||
export const STARTING_PRICE = 0.000001; // $0.000001 per token
|
||||
export const INITIAL_LIQUIDITY = FIXED_SUPPLY * STARTING_PRICE; // $1000
|
||||
export const TOTAL_COST = CREATION_FEE + INITIAL_LIQUIDITY; // $1100
|
||||
|
|
@ -17,6 +17,8 @@ export const user = pgTable("user", {
|
|||
precision: 19,
|
||||
scale: 4,
|
||||
}).notNull().default("10000.0000"), // 10,000 *BUSS
|
||||
bio: varchar("bio", { length: 160 }).default("Hello am 48 year old man from somalia. Sorry for my bed england. I selled my wife for internet connection for play “conter stirk”"),
|
||||
username: varchar("username", { length: 30 }).notNull().unique(),
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
|
|
@ -59,6 +61,7 @@ export const coin = pgTable("coin", {
|
|||
id: serial("id").primaryKey(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
symbol: varchar("symbol", { length: 10 }).notNull().unique(),
|
||||
icon: text("icon"), // New field for coin icon
|
||||
creatorId: integer("creator_id").references(() => user.id, { onDelete: "set null", }), // Coin can exist even if creator is deleted
|
||||
initialSupply: decimal("initial_supply", { precision: 28, scale: 8 }).notNull(),
|
||||
circulatingSupply: decimal("circulating_supply", { precision: 28, scale: 8 }).notNull(),
|
||||
|
|
|
|||
95
website/src/lib/server/s3.ts
Normal file
95
website/src/lib/server/s3.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { PRIVATE_B2_KEY_ID, PRIVATE_B2_APP_KEY } from '$env/static/private';
|
||||
import { PUBLIC_B2_BUCKET, PUBLIC_B2_ENDPOINT, PUBLIC_B2_REGION } from '$env/static/public';
|
||||
|
||||
const s3Client = new S3Client({
|
||||
endpoint: PUBLIC_B2_ENDPOINT,
|
||||
region: PUBLIC_B2_REGION,
|
||||
credentials: {
|
||||
accessKeyId: PRIVATE_B2_KEY_ID,
|
||||
secretAccessKey: PRIVATE_B2_APP_KEY
|
||||
},
|
||||
forcePathStyle: true,
|
||||
requestChecksumCalculation: 'WHEN_REQUIRED',
|
||||
responseChecksumValidation: 'WHEN_REQUIRED',
|
||||
});
|
||||
|
||||
export async function generatePresignedUrl(key: string, contentType: string): Promise<string> {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key,
|
||||
ContentType: contentType
|
||||
});
|
||||
|
||||
return getSignedUrl(s3Client, command, { expiresIn: 3600 }); // 1 hour
|
||||
}
|
||||
|
||||
export async function deleteObject(key: string): Promise<void> {
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
}
|
||||
|
||||
export async function generateDownloadUrl(key: string): Promise<string> {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key
|
||||
});
|
||||
|
||||
return getSignedUrl(s3Client, command, { expiresIn: 3600 });
|
||||
}
|
||||
|
||||
export async function uploadProfilePicture(
|
||||
identifier: string, // Can be user ID or a unique ID from social provider
|
||||
body: Uint8Array,
|
||||
contentType: string,
|
||||
contentLength?: number
|
||||
): Promise<string> {
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
// Ensure a valid image extension or default to jpg
|
||||
if (!fileExtension || !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension.toLowerCase())) {
|
||||
fileExtension = 'jpg';
|
||||
}
|
||||
const key = `avatars/${identifier}.${fileExtension}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
...(contentLength && { ContentLength: contentLength }),
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function uploadCoinIcon(
|
||||
coinSymbol: string,
|
||||
body: Uint8Array,
|
||||
contentType: string,
|
||||
contentLength?: number
|
||||
): Promise<string> {
|
||||
let fileExtension = contentType.split('/')[1];
|
||||
if (!fileExtension || !['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExtension.toLowerCase())) {
|
||||
fileExtension = 'png';
|
||||
}
|
||||
const key = `coins/${coinSymbol.toLowerCase()}.${fileExtension}`;
|
||||
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: PUBLIC_B2_BUCKET,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: contentType,
|
||||
...(contentLength && { ContentLength: contentLength }),
|
||||
});
|
||||
|
||||
await s3Client.send(command);
|
||||
return key;
|
||||
}
|
||||
|
||||
export { s3Client };
|
||||
30
website/src/lib/stores/portfolio-data.ts
Normal file
30
website/src/lib/stores/portfolio-data.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface PortfolioData {
|
||||
baseCurrencyBalance: number;
|
||||
totalCoinValue: number;
|
||||
totalValue: number;
|
||||
coinHoldings: Array<{
|
||||
symbol: string;
|
||||
quantity: number;
|
||||
currentPrice: number;
|
||||
value: number;
|
||||
}>;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export const PORTFOLIO_DATA = writable<PortfolioData | null>(null);
|
||||
|
||||
export async function fetchPortfolioData() {
|
||||
try {
|
||||
const response = await fetch('/api/portfolio/total');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
PORTFOLIO_DATA.set(data);
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch portfolio data:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -3,11 +3,14 @@ import { writable } from 'svelte/store';
|
|||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
image: string;
|
||||
isBanned: boolean;
|
||||
banReason: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string;
|
||||
} | null;
|
||||
|
||||
export const USER_DATA = writable<User>(undefined);
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { PUBLIC_B2_BUCKET, PUBLIC_B2_ENDPOINT } from "$env/static/public";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
|
|
@ -25,3 +26,20 @@ export function getTimeBasedGreeting(name: string): string {
|
|||
return `Good night, ${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPublicUrl(key: string | null): string | null {
|
||||
if (!key) return null;
|
||||
return `${PUBLIC_B2_ENDPOINT}/${PUBLIC_B2_BUCKET}/${key}`;
|
||||
}
|
||||
|
||||
export function debounce(func: (...args: any[]) => void, wait: number) {
|
||||
let timeout: number | undefined;
|
||||
return function executedFunction(...args: any[]) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
9
website/src/lib/utils/random.ts
Normal file
9
website/src/lib/utils/random.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const adjectives = ['happy', 'lucky', 'sunny', 'clever', 'brave', 'bright', 'cool', 'wild', 'calm', 'kind'];
|
||||
const nouns = ['panda', 'tiger', 'whale', 'eagle', 'lion', 'wolf', 'bear', 'fox', 'deer', 'seal'];
|
||||
|
||||
export function generateUsername(): string {
|
||||
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
|
||||
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
||||
const number = Math.floor(Math.random() * 9999);
|
||||
return `${adj}_${noun}${number}`;
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { db } from '$lib/server/db';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
import '../app.css';
|
||||
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
|
||||
import AppSidebar from '$lib/components/self/AppSidebar.svelte';
|
||||
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import { onMount } from 'svelte';
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import { page } from '$app/state';
|
||||
|
||||
let { data, children } = $props<{
|
||||
data: { userSession?: any };
|
||||
|
|
@ -70,6 +73,7 @@
|
|||
</script>
|
||||
|
||||
<ModeWatcher />
|
||||
<Toaster richColors={true} />
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar />
|
||||
|
|
@ -81,7 +85,13 @@
|
|||
<div class="flex w-full items-center gap-4 px-4 lg:px-6">
|
||||
<Sidebar.Trigger class="-ml-1" />
|
||||
|
||||
<h1 class="mr-6 text-base font-medium">test</h1>
|
||||
<h1 class="mr-6 text-base font-medium">
|
||||
{#if page.route.id === '/coin/create'}
|
||||
Coin: Create
|
||||
{:else}
|
||||
test
|
||||
{/if}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,53 @@
|
|||
<script lang="ts">
|
||||
import { coins } from '$lib/data/coins';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { getTimeBasedGreeting } from '$lib/utils';
|
||||
import { getTimeBasedGreeting, getPublicUrl } from '$lib/utils';
|
||||
import { USER_DATA } from '$lib/stores/user-data';
|
||||
import SignInConfirmDialog from '$lib/components/self/SignInConfirmDialog.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let shouldSignIn = $state(false);
|
||||
let coins = $state<any[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/coins/top');
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
coins = result.coins;
|
||||
} else {
|
||||
toast.error('Failed to load coins');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch coins:', e);
|
||||
toast.error('Failed to load coins');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
function formatPrice(price: number): string {
|
||||
if (price < 0.01) {
|
||||
return price.toFixed(6);
|
||||
} else if (price < 1) {
|
||||
return price.toFixed(4);
|
||||
} else {
|
||||
return price.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatMarketCap(value: number): string {
|
||||
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
|
||||
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
||||
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
|
||||
return `$${value.toFixed(2)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SignInConfirmDialog bind:open={shouldSignIn} />
|
||||
|
|
@ -34,32 +74,48 @@
|
|||
</p>
|
||||
</header>
|
||||
|
||||
{#if loading}
|
||||
<div class="flex h-96 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-xl">Loading market data...</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if coins.length === 0}
|
||||
<div class="flex h-96 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-muted-foreground mb-4 text-xl">No coins available</div>
|
||||
<p class="text-muted-foreground text-sm">Be the first to create a coin!</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each coins as coin}
|
||||
{#each coins.slice(0, 6) as coin}
|
||||
<a href={`/coin/${coin.symbol}`} class="block">
|
||||
<Card.Root class="h-full transition-shadow hover:shadow-md">
|
||||
<Card.Header>
|
||||
<Card.Title class="flex items-center justify-between">
|
||||
<span>{coin.name} ({coin.symbol})</span>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if coin.icon}
|
||||
<img
|
||||
src={getPublicUrl(coin.icon)}
|
||||
alt={coin.name}
|
||||
class="h-6 w-6 rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
<span>{coin.name} (*{coin.symbol})</span>
|
||||
</div>
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="ml-2">
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
||||
</Badge>
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
>Market Cap: ${(coin.marketCap / 1000000000).toFixed(2)}B</Card.Description
|
||||
>
|
||||
<Card.Description>Market Cap: {formatMarketCap(coin.marketCap)}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<span class="text-3xl font-bold"
|
||||
>${coin.price.toLocaleString(undefined, {
|
||||
minimumFractionDigits: coin.price < 1 ? 3 : 2,
|
||||
maximumFractionDigits: coin.price < 1 ? 3 : 2
|
||||
})}</span
|
||||
>
|
||||
<span class="text-muted-foreground text-sm"
|
||||
>24h Vol: ${(coin.volume24h / 1000000000).toFixed(2)}B</span
|
||||
>
|
||||
<span class="text-3xl font-bold">${formatPrice(coin.price)}</span>
|
||||
<span class="text-muted-foreground text-sm">
|
||||
24h Vol: {formatMarketCap(coin.volume24h)}
|
||||
</span>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
|
@ -85,26 +141,31 @@
|
|||
{#each coins as coin}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-medium">
|
||||
<a href={`/coin/${coin.symbol}`} class="hover:underline">
|
||||
{coin.name} <span class="text-muted-foreground">({coin.symbol})</span>
|
||||
<a
|
||||
href={`/coin/${coin.symbol}`}
|
||||
class="flex items-center gap-2 hover:underline"
|
||||
>
|
||||
{#if coin.icon}
|
||||
<img
|
||||
src={getPublicUrl(coin.icon)}
|
||||
alt={coin.name}
|
||||
class="h-4 w-4 rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
{coin.name} <span class="text-muted-foreground">(*{coin.symbol})</span>
|
||||
</a>
|
||||
</Table.Cell>
|
||||
<Table.Cell
|
||||
>${coin.price.toLocaleString(undefined, {
|
||||
minimumFractionDigits: coin.price < 1 ? 3 : 2,
|
||||
maximumFractionDigits: coin.price < 1 ? 3 : 2
|
||||
})}</Table.Cell
|
||||
>
|
||||
<Table.Cell>${formatPrice(coin.price)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="hidden md:table-cell"
|
||||
>${(coin.marketCap / 1000000000).toFixed(2)}B</Table.Cell
|
||||
>{formatMarketCap(coin.marketCap)}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="hidden md:table-cell"
|
||||
>${(coin.volume24h / 1000000000).toFixed(2)}B</Table.Cell
|
||||
>{formatMarketCap(coin.volume24h)}</Table.Cell
|
||||
>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
|
@ -113,4 +174,5 @@
|
|||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
73
website/src/routes/api/coin/[coinSymbol]/+server.ts
Normal file
73
website/src/routes/api/coin/[coinSymbol]/+server.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin, user, priceHistory } from '$lib/server/db/schema';
|
||||
import { eq, desc } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const { coinSymbol } = params;
|
||||
|
||||
if (!coinSymbol) {
|
||||
throw error(400, 'Coin symbol is required');
|
||||
}
|
||||
|
||||
const normalizedSymbol = coinSymbol.toUpperCase();
|
||||
|
||||
const [coinData] = await db
|
||||
.select({
|
||||
id: coin.id,
|
||||
name: coin.name,
|
||||
symbol: coin.symbol,
|
||||
creatorId: coin.creatorId,
|
||||
creatorName: user.name,
|
||||
creatorUsername: user.username,
|
||||
creatorBio: user.bio,
|
||||
creatorImage: user.image,
|
||||
initialSupply: coin.initialSupply,
|
||||
circulatingSupply: coin.circulatingSupply,
|
||||
currentPrice: coin.currentPrice,
|
||||
marketCap: coin.marketCap,
|
||||
icon: coin.icon,
|
||||
volume24h: coin.volume24h,
|
||||
change24h: coin.change24h,
|
||||
poolCoinAmount: coin.poolCoinAmount,
|
||||
poolBaseCurrencyAmount: coin.poolBaseCurrencyAmount,
|
||||
createdAt: coin.createdAt,
|
||||
isListed: coin.isListed
|
||||
})
|
||||
.from(coin)
|
||||
.leftJoin(user, eq(coin.creatorId, user.id))
|
||||
.where(eq(coin.symbol, normalizedSymbol))
|
||||
.limit(1);
|
||||
|
||||
if (!coinData) {
|
||||
throw error(404, 'Coin not found');
|
||||
}
|
||||
|
||||
const priceHistoryData = await db
|
||||
.select({
|
||||
price: priceHistory.price,
|
||||
timestamp: priceHistory.timestamp
|
||||
})
|
||||
.from(priceHistory)
|
||||
.where(eq(priceHistory.coinId, coinData.id))
|
||||
.orderBy(desc(priceHistory.timestamp))
|
||||
.limit(720);
|
||||
|
||||
return json({
|
||||
coin: {
|
||||
...coinData,
|
||||
currentPrice: Number(coinData.currentPrice),
|
||||
marketCap: Number(coinData.marketCap),
|
||||
volume24h: Number(coinData.volume24h || 0),
|
||||
change24h: Number(coinData.change24h || 0),
|
||||
initialSupply: Number(coinData.initialSupply),
|
||||
circulatingSupply: Number(coinData.circulatingSupply),
|
||||
poolCoinAmount: Number(coinData.poolCoinAmount),
|
||||
poolBaseCurrencyAmount: Number(coinData.poolBaseCurrencyAmount)
|
||||
},
|
||||
priceHistory: priceHistoryData.map(p => ({
|
||||
price: Number(p.price),
|
||||
timestamp: p.timestamp
|
||||
}))
|
||||
});
|
||||
}
|
||||
143
website/src/routes/api/coin/create/+server.ts
Normal file
143
website/src/routes/api/coin/create/+server.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin, userPortfolio, user, priceHistory } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { uploadCoinIcon } from '$lib/server/s3';
|
||||
import { CREATION_FEE, FIXED_SUPPLY, STARTING_PRICE, INITIAL_LIQUIDITY, TOTAL_COST, MAX_FILE_SIZE } from '$lib/data/constants';
|
||||
|
||||
function validateInputs(name: string, symbol: string, iconFile: File | null) {
|
||||
if (!name || name.length < 2 || name.length > 255) {
|
||||
throw error(400, 'Name must be between 2 and 255 characters');
|
||||
}
|
||||
|
||||
if (!symbol || symbol.length < 2 || symbol.length > 10) {
|
||||
throw error(400, 'Symbol must be between 2 and 10 characters');
|
||||
}
|
||||
|
||||
if (iconFile && iconFile.size > MAX_FILE_SIZE) {
|
||||
throw error(400, 'Icon file must be smaller than 1MB');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateUserBalance(userId: number) {
|
||||
const [userData] = await db
|
||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const currentBalance = Number(userData.baseCurrencyBalance);
|
||||
if (currentBalance < TOTAL_COST) {
|
||||
throw error(400, `Insufficient funds. You need $${TOTAL_COST.toFixed(2)} but only have $${currentBalance.toFixed(2)}.`);
|
||||
}
|
||||
|
||||
return currentBalance;
|
||||
}
|
||||
|
||||
async function validateSymbolUnique(symbol: string) {
|
||||
const existingCoin = await db.select().from(coin).where(eq(coin.symbol, symbol)).limit(1);
|
||||
if (existingCoin.length > 0) {
|
||||
throw error(400, 'A coin with this symbol already exists');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleIconUpload(iconFile: File | null, symbol: string): Promise<string | null> {
|
||||
if (!iconFile || iconFile.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await iconFile.arrayBuffer();
|
||||
return await uploadCoinIcon(
|
||||
symbol,
|
||||
new Uint8Array(arrayBuffer),
|
||||
iconFile.type,
|
||||
iconFile.size
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name') as string;
|
||||
const symbol = formData.get('symbol') as string;
|
||||
const iconFile = formData.get('icon') as File | null;
|
||||
|
||||
const normalizedSymbol = symbol?.toUpperCase();
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
validateInputs(name, normalizedSymbol, iconFile);
|
||||
|
||||
const [currentBalance] = await Promise.all([
|
||||
validateUserBalance(userId),
|
||||
validateSymbolUnique(normalizedSymbol)
|
||||
]);
|
||||
|
||||
let iconKey: string | null = null;
|
||||
try {
|
||||
iconKey = await handleIconUpload(iconFile, normalizedSymbol);
|
||||
} catch (e) {
|
||||
console.error('Icon upload failed, continuing without icon:', e);
|
||||
}
|
||||
|
||||
let createdCoin: any;
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.update(user)
|
||||
.set({
|
||||
baseCurrencyBalance: (currentBalance - TOTAL_COST).toString(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(user.id, userId));
|
||||
|
||||
const [newCoin] = await tx.insert(coin).values({
|
||||
name,
|
||||
symbol: normalizedSymbol,
|
||||
icon: iconKey,
|
||||
creatorId: userId,
|
||||
initialSupply: FIXED_SUPPLY.toString(),
|
||||
circulatingSupply: FIXED_SUPPLY.toString(),
|
||||
currentPrice: STARTING_PRICE.toString(),
|
||||
marketCap: (FIXED_SUPPLY * STARTING_PRICE).toString(),
|
||||
poolCoinAmount: FIXED_SUPPLY.toString(),
|
||||
poolBaseCurrencyAmount: INITIAL_LIQUIDITY.toString()
|
||||
}).returning();
|
||||
|
||||
createdCoin = newCoin;
|
||||
|
||||
await tx.insert(userPortfolio).values({
|
||||
userId,
|
||||
coinId: newCoin.id,
|
||||
quantity: FIXED_SUPPLY.toString()
|
||||
});
|
||||
|
||||
await tx.insert(priceHistory).values({
|
||||
coinId: newCoin.id,
|
||||
price: STARTING_PRICE.toString()
|
||||
});
|
||||
});
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
coin: {
|
||||
id: createdCoin.id,
|
||||
name: createdCoin.name,
|
||||
symbol: createdCoin.symbol,
|
||||
icon: createdCoin.icon
|
||||
},
|
||||
feePaid: CREATION_FEE,
|
||||
liquidityDeposited: INITIAL_LIQUIDITY,
|
||||
initialPrice: STARTING_PRICE,
|
||||
supply: FIXED_SUPPLY
|
||||
});
|
||||
}
|
||||
37
website/src/routes/api/coins/top/+server.ts
Normal file
37
website/src/routes/api/coins/top/+server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { coin } from '$lib/server/db/schema';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET() {
|
||||
const topCoins = await db
|
||||
.select({
|
||||
id: coin.id,
|
||||
name: coin.name,
|
||||
symbol: coin.symbol,
|
||||
icon: coin.icon,
|
||||
currentPrice: coin.currentPrice,
|
||||
marketCap: coin.marketCap,
|
||||
volume24h: coin.volume24h,
|
||||
change24h: coin.change24h,
|
||||
isListed: coin.isListed
|
||||
})
|
||||
.from(coin)
|
||||
.where(eq(coin.isListed, true))
|
||||
.orderBy(desc(coin.marketCap))
|
||||
.limit(20);
|
||||
|
||||
return json({
|
||||
coins: topCoins.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
symbol: c.symbol,
|
||||
icon: c.icon,
|
||||
price: Number(c.currentPrice),
|
||||
marketCap: Number(c.marketCap),
|
||||
volume24h: Number(c.volume24h || 0),
|
||||
change24h: Number(c.change24h || 0),
|
||||
isListed: c.isListed
|
||||
}))
|
||||
});
|
||||
}
|
||||
62
website/src/routes/api/portfolio/total/+server.ts
Normal file
62
website/src/routes/api/portfolio/total/+server.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user, userPortfolio, coin } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const userId = Number(session.user.id);
|
||||
|
||||
const [userData] = await db
|
||||
.select({ baseCurrencyBalance: user.baseCurrencyBalance })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const holdings = await db
|
||||
.select({
|
||||
quantity: userPortfolio.quantity,
|
||||
currentPrice: coin.currentPrice,
|
||||
symbol: coin.symbol
|
||||
})
|
||||
.from(userPortfolio)
|
||||
.innerJoin(coin, eq(userPortfolio.coinId, coin.id))
|
||||
.where(eq(userPortfolio.userId, userId));
|
||||
|
||||
let totalCoinValue = 0;
|
||||
const coinHoldings = holdings.map(holding => {
|
||||
const quantity = Number(holding.quantity);
|
||||
const price = Number(holding.currentPrice);
|
||||
const value = quantity * price;
|
||||
totalCoinValue += value;
|
||||
|
||||
return {
|
||||
symbol: holding.symbol,
|
||||
quantity,
|
||||
currentPrice: price,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
const baseCurrencyBalance = Number(userData.baseCurrencyBalance);
|
||||
|
||||
return json({
|
||||
baseCurrencyBalance,
|
||||
totalCoinValue,
|
||||
totalValue: baseCurrencyBalance + totalCoinValue,
|
||||
coinHoldings,
|
||||
currency: '$'
|
||||
});
|
||||
}
|
||||
71
website/src/routes/api/settings/+server.ts
Normal file
71
website/src/routes/api/settings/+server.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import { uploadProfilePicture } from '$lib/server/s3';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { MAX_FILE_SIZE } from '$lib/data/constants';
|
||||
|
||||
function validateInputs(name: string, bio: string, username: string, avatarFile: File | null) {
|
||||
if (name && name.length < 1) {
|
||||
throw error(400, 'Name cannot be empty');
|
||||
}
|
||||
|
||||
if (bio && bio.length > 160) {
|
||||
throw error(400, 'Bio must be 160 characters or less');
|
||||
}
|
||||
|
||||
if (username && (username.length < 3 || username.length > 30)) {
|
||||
throw error(400, 'Username must be between 3 and 30 characters');
|
||||
}
|
||||
|
||||
if (avatarFile && avatarFile.size > MAX_FILE_SIZE) {
|
||||
throw error(400, 'Avatar file must be smaller than 1MB');
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST({ request }) {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
throw error(401, 'Not authenticated');
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name') as string;
|
||||
const bio = formData.get('bio') as string;
|
||||
const username = formData.get('username') as string;
|
||||
const avatarFile = formData.get('avatar') as File | null;
|
||||
|
||||
validateInputs(name, bio, username, avatarFile);
|
||||
|
||||
const updates: Record<string, any> = {
|
||||
name,
|
||||
bio,
|
||||
username,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
if (avatarFile && avatarFile.size > 0) {
|
||||
try {
|
||||
const arrayBuffer = await avatarFile.arrayBuffer();
|
||||
const key = await uploadProfilePicture(
|
||||
session.user.id,
|
||||
new Uint8Array(arrayBuffer),
|
||||
avatarFile.type,
|
||||
avatarFile.size
|
||||
);
|
||||
updates.image = key;
|
||||
} catch (e) {
|
||||
console.error('Avatar upload failed, continuing without update:', e);
|
||||
}
|
||||
}
|
||||
|
||||
await db.update(user)
|
||||
.set(updates)
|
||||
.where(eq(user.id, Number(session.user.id)));
|
||||
|
||||
return json({ success: true });
|
||||
}
|
||||
17
website/src/routes/api/settings/check-username/+server.ts
Normal file
17
website/src/routes/api/settings/check-username/+server.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const username = url.searchParams.get('username');
|
||||
if (!username) {
|
||||
return json({ available: false });
|
||||
}
|
||||
|
||||
const exists = await db.query.user.findFirst({
|
||||
where: eq(user.username, username)
|
||||
});
|
||||
|
||||
return json({ available: !exists });
|
||||
}
|
||||
28
website/src/routes/api/user/[userId]/image/+server.ts
Normal file
28
website/src/routes/api/user/[userId]/image/+server.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { error, json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { user } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getPublicUrl } from '$lib/utils';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const { userId } = params;
|
||||
|
||||
try {
|
||||
const [userData] = await db
|
||||
.select({ image: user.image })
|
||||
.from(user)
|
||||
.where(eq(user.id, Number(userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!userData) {
|
||||
throw error(404, 'User not found');
|
||||
}
|
||||
|
||||
const url = getPublicUrl(userData.image);
|
||||
|
||||
return json({ url });
|
||||
} catch (e) {
|
||||
console.error('Failed to get user image:', e);
|
||||
throw error(500, 'Failed to get user image');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,115 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { coins } from '$lib/data/coins';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { createChart, CandlestickSeries, type Time, ColorType } from 'lightweight-charts';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as HoverCard from '$lib/components/ui/hover-card';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
DollarSign,
|
||||
Coins,
|
||||
ChartColumn,
|
||||
CalendarDays
|
||||
} from 'lucide-svelte';
|
||||
import {
|
||||
createChart,
|
||||
ColorType,
|
||||
type Time,
|
||||
type IChartApi,
|
||||
CandlestickSeries
|
||||
} from 'lightweight-charts';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { getPublicUrl } from '$lib/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const coin = coins.find((c) => c.symbol === $page.params.coinSymbol);
|
||||
const { data } = $props();
|
||||
const coinSymbol = data.coinSymbol;
|
||||
|
||||
// Generate mock candlestick data
|
||||
const candleData = Array.from({ length: 30 }, (_, i) => {
|
||||
const basePrice = coin?.price || 100;
|
||||
const date = new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000);
|
||||
const open = basePrice * (1 + Math.sin(i / 5) * 0.1);
|
||||
const close = basePrice * (1 + Math.sin((i + 1) / 5) * 0.1);
|
||||
const high = Math.max(open, close) * (1 + Math.random() * 0.02);
|
||||
const low = Math.min(open, close) * (1 - Math.random() * 0.02);
|
||||
let coin = $state<any>(null);
|
||||
let priceHistory = $state<any[]>([]);
|
||||
let loading = $state(true);
|
||||
let creatorImageUrl = $state<string | null>(null);
|
||||
let chartData = $state<any[]>([]);
|
||||
|
||||
return {
|
||||
time: Math.floor(date.getTime() / 1000) as Time,
|
||||
open,
|
||||
high,
|
||||
low,
|
||||
close
|
||||
};
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/coin/${coinSymbol}`);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
toast.error('Coin not found');
|
||||
} else {
|
||||
toast.error('Failed to load coin data');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
coin = result.coin;
|
||||
priceHistory = result.priceHistory;
|
||||
chartData = generateCandlesticksFromHistory(priceHistory);
|
||||
|
||||
if (coin.creatorId) {
|
||||
try {
|
||||
const imageResponse = await fetch(`/api/user/${coin.creatorId}/image`);
|
||||
const imageResult = await imageResponse.json();
|
||||
creatorImageUrl = imageResult.url;
|
||||
} catch (e) {
|
||||
console.error('Failed to load creator image:', e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch coin data:', e);
|
||||
toast.error('Failed to load coin data');
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
let chartContainer: HTMLDivElement;
|
||||
function generateCandlesticksFromHistory(history: any[]) {
|
||||
const dailyData = new Map();
|
||||
|
||||
onMount(() => {
|
||||
const chart = createChart(chartContainer, {
|
||||
history.forEach((p) => {
|
||||
const date = new Date(p.timestamp);
|
||||
const dayKey = Math.floor(date.getTime() / (24 * 60 * 60 * 1000));
|
||||
|
||||
if (!dailyData.has(dayKey)) {
|
||||
dailyData.set(dayKey, {
|
||||
time: dayKey * 24 * 60 * 60,
|
||||
open: p.price,
|
||||
high: p.price,
|
||||
low: p.price,
|
||||
close: p.price,
|
||||
prices: [p.price]
|
||||
});
|
||||
} else {
|
||||
const dayData = dailyData.get(dayKey);
|
||||
dayData.high = Math.max(dayData.high, p.price);
|
||||
dayData.low = Math.min(dayData.low, p.price);
|
||||
dayData.close = p.price;
|
||||
dayData.prices.push(p.price);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(dailyData.values())
|
||||
.map((d) => ({
|
||||
time: d.time as Time,
|
||||
open: d.open,
|
||||
high: d.high,
|
||||
low: d.low,
|
||||
close: d.close
|
||||
}))
|
||||
.sort((a, b) => (a.time as number) - (b.time as number));
|
||||
}
|
||||
|
||||
let chartContainer = $state<HTMLDivElement>();
|
||||
let chart: IChartApi | null = null;
|
||||
|
||||
$effect(() => {
|
||||
if (chartContainer && chartData.length > 0 && !chart) {
|
||||
chart = createChart(chartContainer, {
|
||||
layout: {
|
||||
textColor: '#666666',
|
||||
background: { type: ColorType.Solid, color: 'transparent' },
|
||||
|
|
@ -51,7 +131,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
const candlesticks = chart.addSeries(CandlestickSeries, {
|
||||
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||
upColor: '#26a69a',
|
||||
downColor: '#ef5350',
|
||||
borderVisible: false,
|
||||
|
|
@ -59,12 +139,12 @@
|
|||
wickDownColor: '#ef5350'
|
||||
});
|
||||
|
||||
candlesticks.setData(candleData);
|
||||
candlestickSeries.setData(chartData);
|
||||
chart.timeScale().fitContent();
|
||||
|
||||
const handleResize = () => {
|
||||
chart.applyOptions({
|
||||
width: chartContainer.clientWidth
|
||||
chart?.applyOptions({
|
||||
width: chartContainer?.clientWidth
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -73,72 +153,300 @@
|
|||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (chart) {
|
||||
chart.remove();
|
||||
chart = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function formatPrice(price: number): string {
|
||||
if (price < 0.01) {
|
||||
return price.toFixed(6);
|
||||
} else if (price < 1) {
|
||||
return price.toFixed(4);
|
||||
} else {
|
||||
return price.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
function formatMarketCap(value: number): string {
|
||||
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
|
||||
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
||||
if (value >= 1e3) return `$${(value / 1e3).toFixed(2)}K`;
|
||||
return `$${value.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatSupply(value: number): string {
|
||||
if (value >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
|
||||
if (value >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
|
||||
if (value >= 1e3) return `${(value / 1e3).toFixed(2)}K`;
|
||||
return value.toLocaleString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-6">
|
||||
{#if coin}
|
||||
<header class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-4xl font-bold">{coin.name} ({coin.symbol})</h1>
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
|
||||
</Badge>
|
||||
<svelte:head>
|
||||
<title>{coin ? `${coin.name} (${coin.symbol})` : 'Loading...'} - Rugplay</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if loading}
|
||||
<div class="container mx-auto max-w-7xl p-6">
|
||||
<div class="flex h-96 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="mb-4 text-xl">Loading coin data...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !coin}
|
||||
<div class="container mx-auto max-w-7xl p-6">
|
||||
<div class="flex h-96 items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="text-muted-foreground mb-4 text-xl">Coin not found</div>
|
||||
<Button onclick={() => goto('/')}>Go Home</Button>
|
||||
</div>
|
||||
<p class="mt-4 text-3xl font-semibold">
|
||||
${coin.price.toLocaleString(undefined, {
|
||||
minimumFractionDigits: coin.price < 1 ? 3 : 2,
|
||||
maximumFractionDigits: coin.price < 1 ? 3 : 2
|
||||
})}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Price Chart</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="h-[400px] w-full" bind:this={chartContainer}></div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Market Cap</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-2xl font-semibold">${(coin.marketCap / 1000000000).toFixed(2)}B</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>24h Volume</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-2xl font-semibold">${(coin.volume24h / 1000000000).toFixed(2)}B</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>24h Change</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<p class="text-2xl font-semibold">
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-lg">
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h}%
|
||||
</Badge>
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p>Coin not found</p>
|
||||
<div class="container mx-auto max-w-7xl p-6">
|
||||
<!-- Header Section -->
|
||||
<header class="mb-8">
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="bg-muted/50 flex h-16 w-16 items-center justify-center overflow-hidden rounded-full border"
|
||||
>
|
||||
{#if coin.icon}
|
||||
<img
|
||||
src={getPublicUrl(coin.icon)}
|
||||
alt={coin.name}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex h-full w-full items-center justify-center rounded-full bg-gradient-to-br from-blue-500 to-purple-600 text-xl font-bold text-white"
|
||||
>
|
||||
{coin.symbol.slice(0, 2)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold">{coin.name}</h1>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<Badge variant="outline" class="text-lg">*{coin.symbol}</Badge>
|
||||
{#if !coin.isListed}
|
||||
<Badge variant="destructive">Delisted</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-3xl font-bold">
|
||||
${formatPrice(coin.currentPrice)}
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
{#if coin.change24h >= 0}
|
||||
<TrendingUp class="h-4 w-4 text-green-500" />
|
||||
{:else}
|
||||
<TrendingDown class="h-4 w-4 text-red-500" />
|
||||
{/if}
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'}>
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Creator Info -->
|
||||
{#if coin.creatorName}
|
||||
<div class="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<span>Created by</span>
|
||||
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger
|
||||
class="flex cursor-pointer items-center gap-2 rounded-sm underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-8"
|
||||
onclick={() => goto(`/user/${coin.creatorId}`)}
|
||||
>
|
||||
<Avatar.Root class="h-4 w-4">
|
||||
<Avatar.Image src={creatorImageUrl} alt={coin.creatorName} />
|
||||
<Avatar.Fallback>{coin.creatorName.charAt(0)}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<span class="font-medium">{coin.creatorName} (@{coin.creatorUsername})</span>
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content class="w-80" side="bottom" sideOffset={3}>
|
||||
<div class="flex justify-between space-x-4">
|
||||
<Avatar.Root class="h-14 w-14">
|
||||
<Avatar.Image src={creatorImageUrl} alt={coin.creatorName} />
|
||||
<Avatar.Fallback>{coin.creatorName.charAt(0)}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="flex-1 space-y-1">
|
||||
<h4 class="text-sm font-semibold">{coin.creatorName}</h4>
|
||||
<p class="text-muted-foreground text-sm">@{coin.creatorUsername}</p>
|
||||
{#if coin.creatorBio}
|
||||
<p class="text-sm">{coin.creatorBio}</p>
|
||||
{/if}
|
||||
<div class="flex items-center pt-2">
|
||||
<CalendarDays class="mr-2 h-4 w-4 opacity-70" />
|
||||
<span class="text-muted-foreground text-xs">
|
||||
Joined {new Date(coin.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6">
|
||||
<!-- Price Chart with Trading Actions -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<!-- Chart (2/3 width) -->
|
||||
<div class="lg:col-span-2">
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-4">
|
||||
<Card.Title class="flex items-center gap-2">
|
||||
<ChartColumn class="h-5 w-5" />
|
||||
Price Chart
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<div class="h-[400px] w-full" bind:this={chartContainer}></div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Right side - Trading Actions + Liquidity Pool (1/3 width) -->
|
||||
<div class="space-y-6 lg:col-span-1">
|
||||
<!-- Trading Actions -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-4">
|
||||
<Card.Title>Trade {coin.symbol}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<div class="space-y-3">
|
||||
<Button class="w-full" variant="default" size="lg">
|
||||
<TrendingUp class="mr-2 h-4 w-4" />
|
||||
Buy {coin.symbol}
|
||||
</Button>
|
||||
<Button class="w-full" variant="outline" size="lg">
|
||||
<TrendingDown class="mr-2 h-4 w-4" />
|
||||
Sell {coin.symbol}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Liquidity Pool -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-4">
|
||||
<Card.Title>Liquidity Pool</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="mb-3 font-medium">Pool Composition</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground text-sm">{coin.symbol}:</span>
|
||||
<span class="font-mono text-sm">{formatSupply(coin.poolCoinAmount)}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground text-sm">Base Currency:</span>
|
||||
<span class="font-mono text-sm"
|
||||
>${coin.poolBaseCurrencyAmount.toLocaleString()}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-3 font-medium">Pool Stats</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground text-sm">Total Liquidity:</span>
|
||||
<span class="font-mono text-sm"
|
||||
>${(coin.poolBaseCurrencyAmount * 2).toLocaleString()}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground text-sm">Price Impact:</span>
|
||||
<Badge variant="success" class="text-xs">Low</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Grid -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- Market Cap -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="flex items-center gap-2 text-sm font-medium">
|
||||
<DollarSign class="h-4 w-4" />
|
||||
Market Cap
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<p class="text-xl font-bold">{formatMarketCap(coin.marketCap)}</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- 24h Volume -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="flex items-center gap-2 text-sm font-medium">
|
||||
<ChartColumn class="h-4 w-4" />
|
||||
24h Volume
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<p class="text-xl font-bold">{formatMarketCap(coin.volume24h)}</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Circulating Supply -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="flex items-center gap-2 text-sm font-medium">
|
||||
<Coins class="h-4 w-4" />
|
||||
Circulating Supply
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<p class="text-xl font-bold">{formatSupply(coin.circulatingSupply)}</p>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
of {formatSupply(coin.initialSupply)} total
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- 24h Change -->
|
||||
<Card.Root>
|
||||
<Card.Header class="pb-2">
|
||||
<Card.Title class="text-sm font-medium">24h Change</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="pt-0">
|
||||
<div class="flex items-center gap-2">
|
||||
{#if coin.change24h >= 0}
|
||||
<TrendingUp class="h-4 w-4 text-green-500" />
|
||||
{:else}
|
||||
<TrendingDown class="h-4 w-4 text-red-500" />
|
||||
{/if}
|
||||
<Badge variant={coin.change24h >= 0 ? 'success' : 'destructive'} class="text-sm">
|
||||
{coin.change24h >= 0 ? '+' : ''}{coin.change24h.toFixed(2)}%
|
||||
</Badge>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
5
website/src/routes/coin/[coinSymbol]/+page.ts
Normal file
5
website/src/routes/coin/[coinSymbol]/+page.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export async function load({ params }) {
|
||||
return {
|
||||
coinSymbol: params.coinSymbol
|
||||
};
|
||||
}
|
||||
329
website/src/routes/coin/create/+page.svelte
Normal file
329
website/src/routes/coin/create/+page.svelte
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||
import { Alert, AlertDescription } from '$lib/components/ui/alert';
|
||||
import { Separator } from '$lib/components/ui/separator';
|
||||
import { Info, Loader2, Coins, ImagePlus } from 'lucide-svelte';
|
||||
import { PORTFOLIO_DATA, fetchPortfolioData } from '$lib/stores/portfolio-data';
|
||||
import { onMount } from 'svelte';
|
||||
import { CREATION_FEE, INITIAL_LIQUIDITY, TOTAL_COST } from '$lib/data/constants';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let name = $state('');
|
||||
let symbol = $state('');
|
||||
let iconFile = $state<File | null>(null);
|
||||
let iconPreview = $state<string | null>(null);
|
||||
let isSubmitting = $state(false);
|
||||
let error = $state('');
|
||||
|
||||
onMount(() => {
|
||||
fetchPortfolioData();
|
||||
});
|
||||
|
||||
let nameError = $derived(
|
||||
name.length > 0 && (name.length < 2 || name.length > 255)
|
||||
? 'Name must be between 2 and 255 characters'
|
||||
: ''
|
||||
);
|
||||
|
||||
let symbolError = $derived(
|
||||
symbol.length > 0 && (symbol.length < 2 || symbol.length > 10)
|
||||
? 'Symbol must be between 2 and 10 characters'
|
||||
: ''
|
||||
);
|
||||
|
||||
let iconError = $derived(
|
||||
iconFile && iconFile.size > 1 * 1024 * 1024 ? 'Icon must be smaller than 1MB' : ''
|
||||
);
|
||||
|
||||
let isFormValid = $derived(
|
||||
name.length >= 2 && symbol.length >= 2 && !nameError && !symbolError && !iconError
|
||||
);
|
||||
|
||||
let hasEnoughFunds = $derived(
|
||||
$PORTFOLIO_DATA ? $PORTFOLIO_DATA.baseCurrencyBalance >= TOTAL_COST : false
|
||||
);
|
||||
|
||||
let canSubmit = $derived(isFormValid && hasEnoughFunds && !isSubmitting);
|
||||
|
||||
function handleIconChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
if (file.type.startsWith('image/')) {
|
||||
iconFile = file;
|
||||
console.log(iconFile.size);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
iconPreview = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
error = 'Please select a valid image file';
|
||||
target.value = '';
|
||||
}
|
||||
} else {
|
||||
iconFile = null;
|
||||
iconPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event: { preventDefault: () => void }) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!canSubmit) return;
|
||||
|
||||
isSubmitting = true;
|
||||
error = '';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('symbol', symbol.toUpperCase());
|
||||
|
||||
if (iconFile) {
|
||||
formData.append('icon', iconFile);
|
||||
}
|
||||
|
||||
const response = await fetch('/api/coin/create', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.message || 'Failed to create coin');
|
||||
}
|
||||
|
||||
await fetchPortfolioData();
|
||||
|
||||
goto(`/coin/${result.coin.symbol}`);
|
||||
} catch (e) {
|
||||
toast.error('Failed to create coin', {
|
||||
description: (e as Error).message || 'An error occurred while creating the coin'
|
||||
});
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Create Coin - Rugplay</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container mx-auto max-w-5xl px-4 py-6">
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<!-- Main Form Column -->
|
||||
<div class="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg">Coin Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onsubmit={handleSubmit} class="space-y-6">
|
||||
<!-- Icon Upload -->
|
||||
<div>
|
||||
<Label for="icon">Coin Icon (Optional)</Label>
|
||||
<div class="mt-2 space-y-2">
|
||||
<label for="icon" class="block cursor-pointer">
|
||||
<div
|
||||
class="border-muted-foreground/25 bg-muted/50 hover:border-muted-foreground/50 group h-24 w-24 overflow-hidden rounded-full border-2 border-dashed transition-colors"
|
||||
>
|
||||
<Input
|
||||
id="icon"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onchange={handleIconChange}
|
||||
class="hidden"
|
||||
/>
|
||||
{#if iconPreview}
|
||||
<img src={iconPreview} alt="Preview" class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<ImagePlus class="text-muted-foreground h-8 w-8" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
<p class="{iconError ? 'text-destructive' : 'text-muted-foreground'} text-sm">
|
||||
{#if iconError}
|
||||
{iconError}
|
||||
{:else if iconFile}
|
||||
{iconFile.name} ({(iconFile.size / 1024).toFixed(2)} KB)
|
||||
{:else}
|
||||
Click to upload your coin's icon (PNG or JPG, max 1MB)
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Name Input -->
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Coin Name</Label>
|
||||
<Input id="name" type="text" bind:value={name} placeholder="e.g., Bitcoin" required />
|
||||
{#if nameError}
|
||||
<p class="text-destructive text-xs">{nameError}</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Choose a memorable name for your cryptocurrency
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Symbol Input -->
|
||||
<div class="space-y-2">
|
||||
<Label for="symbol">Symbol</Label>
|
||||
<div class="relative">
|
||||
<span class="text-muted-foreground absolute left-3 top-1/2 -translate-y-1/2 text-sm"
|
||||
>*</span
|
||||
>
|
||||
<Input
|
||||
id="symbol"
|
||||
type="text"
|
||||
bind:value={symbol}
|
||||
placeholder="BTC"
|
||||
class="pl-8 uppercase"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
{#if symbolError}
|
||||
<p class="text-destructive text-xs">{symbolError}</p>
|
||||
{:else}
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Short identifier for your coin (e.g., BTC for Bitcoin). Will be displayed as *{symbol ||
|
||||
'SYMBOL'}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Fair Launch Info -->
|
||||
<Alert variant="default" class="bg-muted/50">
|
||||
<Info class="h-4 w-4" />
|
||||
<AlertDescription class="space-y-2">
|
||||
<p class="font-medium">Fair Launch Settings</p>
|
||||
<div class="text-muted-foreground space-y-1 text-sm">
|
||||
<p>• Total Supply: <span class="font-medium">1,000,000,000 tokens</span></p>
|
||||
<p>• Starting Price: <span class="font-medium">$0.000001 per token</span></p>
|
||||
<p>• You receive <span class="font-medium">100%</span> of the supply</p>
|
||||
<p>• Initial Market Cap: <span class="font-medium">$1,000</span></p>
|
||||
<p class="mt-2 text-sm">
|
||||
These settings ensure a fair start for all traders. The price will increase
|
||||
naturally as people buy tokens.
|
||||
</p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<Button type="submit" disabled={!canSubmit} class="w-full" size="lg">
|
||||
{#if isSubmitting}
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
{:else}
|
||||
<Coins class="h-4 w-4" />
|
||||
Create Coin (${TOTAL_COST.toFixed(2)})
|
||||
{/if}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Right Column - Preview and Info -->
|
||||
<div class="space-y-4">
|
||||
<!-- Cost Summary Card -->
|
||||
{#if $PORTFOLIO_DATA}
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="text-base">Cost Summary</CardTitle>
|
||||
<div class="text-sm">
|
||||
<span class="text-muted-foreground">Balance: </span>
|
||||
<span class={hasEnoughFunds ? 'text-green-600' : 'text-destructive'}>
|
||||
${$PORTFOLIO_DATA.baseCurrencyBalance.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-2">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">Creation Fee</span>
|
||||
<span>${CREATION_FEE}</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground">Initial Liquidity</span>
|
||||
<span>${INITIAL_LIQUIDITY}</span>
|
||||
</div>
|
||||
<Separator class="my-2" />
|
||||
<div class="flex justify-between font-medium">
|
||||
<span>Total Cost</span>
|
||||
<span class="text-primary">${TOTAL_COST}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
<!-- Info Card -->
|
||||
<Card>
|
||||
<CardHeader class="pb-2">
|
||||
<CardTitle class="text-base">What Happens After Launch?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">Fair Distribution</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Everyone starts buying at the same price - no pre-sales or hidden allocations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">Price Discovery</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Token price increases automatically as more people buy, following a bonding curve
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
class="bg-primary/10 text-primary flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-sm font-medium"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium">Instant Trading</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Trading begins immediately - buy, sell, or distribute your tokens as you wish
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
min-height: calc(100vh - 4rem);
|
||||
}
|
||||
</style>
|
||||
12
website/src/routes/settings/+page.server.ts
Normal file
12
website/src/routes/settings/+page.server.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { auth } from '$lib/auth';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: event.request.headers
|
||||
});
|
||||
if (!session?.user) throw error(401, 'Not authenticated');
|
||||
|
||||
return { user: session.user };
|
||||
};
|
||||
213
website/src/routes/settings/+page.svelte
Normal file
213
website/src/routes/settings/+page.svelte
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import { getPublicUrl, debounce } from '$lib/utils';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { CheckIcon } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { MAX_FILE_SIZE } from '$lib/data/constants';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let name = $state(data.user.name);
|
||||
let bio = $state(data.user.bio ?? '');
|
||||
let username = $state(data.user.username);
|
||||
|
||||
const initialUsername = data.user.username;
|
||||
let avatarFile: FileList | undefined = $state(undefined);
|
||||
|
||||
let previewUrl: string | null = $state(null);
|
||||
let currentAvatarUrl = $derived(previewUrl || getPublicUrl(data.user.image ?? null));
|
||||
|
||||
let isDirty = $derived(
|
||||
name !== data.user.name ||
|
||||
bio !== (data.user.bio ?? '') ||
|
||||
username !== data.user.username ||
|
||||
avatarFile !== undefined
|
||||
);
|
||||
|
||||
let fileInput: HTMLInputElement;
|
||||
|
||||
let loading = $state(false);
|
||||
let usernameAvailable: boolean | null = $state(null);
|
||||
let checkingUsername = $state(false);
|
||||
|
||||
function beforeUnloadHandler(e: BeforeUnloadEvent) {
|
||||
if (isDirty) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('beforeunload', beforeUnloadHandler);
|
||||
});
|
||||
|
||||
function handleAvatarClick() {
|
||||
fileInput.click();
|
||||
}
|
||||
function handleAvatarChange(e: Event) {
|
||||
const f = (e.target as HTMLInputElement).files?.[0];
|
||||
if (f) {
|
||||
// Check file size
|
||||
if (f.size > MAX_FILE_SIZE) {
|
||||
toast.error('Profile picture must be smaller than 1MB');
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check file type
|
||||
if (!f.type.startsWith('image/')) {
|
||||
toast.error('Please select a valid image file');
|
||||
(e.target as HTMLInputElement).value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
previewUrl = URL.createObjectURL(f);
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files) avatarFile = files;
|
||||
}
|
||||
}
|
||||
|
||||
const checkUsername = debounce(async (val: string) => {
|
||||
if (val.length < 3) return (usernameAvailable = null);
|
||||
checkingUsername = true;
|
||||
const res = await fetch(`/api/settings/check-username?username=${val}`);
|
||||
usernameAvailable = (await res.json()).available;
|
||||
checkingUsername = false;
|
||||
}, 500);
|
||||
|
||||
$effect(() => {
|
||||
if (username !== initialUsername) checkUsername(username);
|
||||
});
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('name', name);
|
||||
fd.append('bio', bio);
|
||||
fd.append('username', username);
|
||||
if (avatarFile?.[0]) fd.append('avatar', avatarFile[0]);
|
||||
|
||||
const res = await fetch('/api/settings', { method: 'POST', body: fd });
|
||||
|
||||
if (res.ok) {
|
||||
await invalidateAll();
|
||||
toast.success('Settings updated successfully!', {
|
||||
action: { label: 'Refresh', onClick: () => window.location.reload() }
|
||||
});
|
||||
} else {
|
||||
const result = await res.json();
|
||||
toast.error('Failed to update settings', {
|
||||
description: result.message || 'An error occurred while updating your settings'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to update settings', {
|
||||
description: 'An unexpected error occurred'
|
||||
});
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto max-w-2xl p-6">
|
||||
<h1 class="mb-6 text-2xl font-bold">Settings</h1>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Profile Settings</Card.Title>
|
||||
<Card.Description>Update your profile information</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<div
|
||||
class="group relative cursor-pointer"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={handleAvatarClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleAvatarClick()}
|
||||
>
|
||||
<Avatar.Root class="size-20">
|
||||
<Avatar.Image src={currentAvatarUrl} alt={name} />
|
||||
<Avatar.Fallback>?</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center rounded-full bg-black/50 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
>
|
||||
<span class="text-xs text-white">Change</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">{name}</h3>
|
||||
<p class="text-muted-foreground text-sm">@{username}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="hidden"
|
||||
bind:this={fileInput}
|
||||
onchange={handleAvatarChange}
|
||||
/>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Display Name</Label>
|
||||
<Input id="name" bind:value={name} required />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="username">Username</Label>
|
||||
<div class="relative">
|
||||
<span class="text-muted-foreground absolute left-3 top-4 -translate-y-1/2 transform"
|
||||
>@</span
|
||||
>
|
||||
<Input
|
||||
id="username"
|
||||
bind:value={username}
|
||||
required
|
||||
pattern={'^[a-zA-Z0-9_]{3,30}$'}
|
||||
class="pl-8"
|
||||
/>
|
||||
<div class="absolute right-3 top-1.5">
|
||||
{#if checkingUsername}
|
||||
<span class="text-muted-foreground text-sm">Checking…</span>
|
||||
{:else if username !== initialUsername}
|
||||
{#if usernameAvailable}
|
||||
<CheckIcon class="text-success" />
|
||||
{:else}
|
||||
<span class="text-destructive text-sm">Taken</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted-foreground text-xs">
|
||||
Only letters, numbers, underscores. 3–30 characters.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bio">Bio</Label>
|
||||
<Textarea id="bio" bind:value={bio} rows={4} placeholder="Tell us about yourself" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={loading || !isDirty}>
|
||||
{loading ? 'Saving…' : 'Save Changes'}
|
||||
</Button>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
Reference in a new issue