Compare commits
28 commits
main
...
dashboard-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d7512bc151 | ||
![]() |
82bda3850f | ||
![]() |
30b3ae5044 | ||
![]() |
393f08129b | ||
![]() |
8860f4a5ac | ||
![]() |
3905155ab7 | ||
![]() |
ae74906217 | ||
![]() |
972c8930cd | ||
![]() |
aaaebc20a5 | ||
![]() |
1599371df5 | ||
![]() |
31a8d62f1d | ||
![]() |
4f54880400 | ||
![]() |
60cc3e958c | ||
![]() |
5e5fc372cc | ||
![]() |
9240263490 | ||
![]() |
dcfd3df713 | ||
![]() |
d5dfeac9c0 | ||
![]() |
5f7885a6bd | ||
![]() |
f25e096063 | ||
![]() |
c0563b844a | ||
![]() |
361c4e0b62 | ||
![]() |
8e5dff01c0 | ||
![]() |
f843155394 | ||
![]() |
3217373024 | ||
![]() |
b36d3b76a1 | ||
![]() |
f164351011 | ||
![]() |
f47a1b2226 | ||
![]() |
0d29d59937 |
29 changed files with 948 additions and 451 deletions
1
.env.local.example
Normal file
1
.env.local.example
Normal file
|
@ -0,0 +1 @@
|
|||
BACKEND_URL=
|
|
@ -1,9 +1,11 @@
|
|||
# stereo.cat frontend
|
||||
|
||||
written in typescript with qwik
|
||||
written in typescript with qwik & bun
|
||||
|
||||
## running in dev env
|
||||
```
|
||||
## development
|
||||
https://bun.sh/docs/installation
|
||||
|
||||
```bash
|
||||
git clone https://git.iwakura.rip/stereo.cat/frontend.git
|
||||
git submodule update --init --recursive
|
||||
bun install
|
||||
|
@ -11,4 +13,5 @@ bun dev
|
|||
```
|
||||
|
||||
## disclaimer
|
||||
|
||||
All graphic assets belonging to stereo.cat may not be used in unofficial instances, forks or versions of our software. Please replace them if you are hosting our software yourself, they can be found in the ``public`` folder in this repository. More information (like the full license) can be found [here](https://git.iwakura.rip/stereo.cat/public)
|
||||
|
|
81
bun.lock
81
bun.lock
|
@ -6,36 +6,39 @@
|
|||
"dependencies": {
|
||||
"@types/aos": "^3.0.7",
|
||||
"aos": "^3.0.0-beta.6",
|
||||
"ky": "^1.8.1",
|
||||
"buffer": "^6.0.3",
|
||||
"fast-blurhash": "^1.1.4",
|
||||
"ky": "^1.8.2",
|
||||
"nanostores": "^1.0.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"thumbhash": "^0.1.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.14.1",
|
||||
"@builder.io/qwik-city": "^1.14.1",
|
||||
"@builder.io/qwik": "^1.15.0",
|
||||
"@builder.io/qwik-city": "^1.15.0",
|
||||
"@eslint/js": "latest",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "20.14.11",
|
||||
"eslint": "9.25.1",
|
||||
"eslint-plugin-qwik": "^1.14.1",
|
||||
"eslint-plugin-qwik": "^1.15.0",
|
||||
"globals": "16.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "5.4.5",
|
||||
"typescript-eslint": "8.26.1",
|
||||
"undici": "*",
|
||||
"undici": "^7.12.0",
|
||||
"vite": "5.3.5",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@builder.io/qwik": ["@builder.io/qwik@1.14.1", "", { "dependencies": { "csstype": "^3.1", "rollup": ">= 4.39.0" }, "peerDependencies": { "vite": "^5" }, "bin": { "qwik": "qwik-cli.cjs" } }, "sha512-bq1fkt8RhjkENQOs/bxh8i1IuQ0DIfIZ1RgyoZYmADxcyKjhVRcCx+6kaRUkjovb5sR06pqH96YaHjI7iI/CMw=="],
|
||||
"@builder.io/qwik": ["@builder.io/qwik@1.15.0", "", { "dependencies": { "csstype": "^3.1", "rollup": ">= 4.39.0" }, "peerDependencies": { "vite": ">=5 <8" }, "bin": { "qwik": "qwik-cli.cjs" } }, "sha512-0PUXGbH+9htHPq0Br+M/QsY7aGJhG7C1A4dZziDGONAB2INcOUkzoOF3Qv2S7JzFyUlgBtN1drmw6P9jLryRyQ=="],
|
||||
|
||||
"@builder.io/qwik-city": ["@builder.io/qwik-city@1.14.1", "", { "dependencies": { "@mdx-js/mdx": "^3", "@types/mdx": "^2", "source-map": "^0.7.4", "svgo": "^3.3", "undici": "*", "valibot": ">=0.36.0 <2", "vfile": "6.0.2", "vite": "^5", "vite-imagetools": "^7", "zod": "3.22.4" } }, "sha512-VsAvk7u2HyyTnL9GhpT+h10t2XAIlxtv6LFL3Xt9/1QZ6lMfGWMcMEAMuZB1Ib+D/oTfu7QRqZngRg3FsrIKyg=="],
|
||||
"@builder.io/qwik-city": ["@builder.io/qwik-city@1.15.0", "", { "dependencies": { "@mdx-js/mdx": "^3", "@types/mdx": "^2", "source-map": "^0.7.4", "svgo": "^3.3", "undici": "*", "valibot": ">=0.36.0 <2", "vfile": "6.0.2", "vite": ">=5 <8", "vite-imagetools": "^7", "zod": "3.22.4" } }, "sha512-fy84pb6fat8YKDBh06tw+vgZ+Iwlhq/qEnFLUpMFLAG+Vll8diH6t0MeK9rtiPG9VsPZYZa32euTy4LnHj2Tiw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
|
||||
|
@ -97,7 +100,7 @@
|
|||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
|
||||
"@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
|
@ -215,35 +218,35 @@
|
|||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.42.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="],
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="],
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="],
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="],
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="],
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="],
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="],
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="],
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="],
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="],
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="],
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="],
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="],
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.8", "", { "dependencies": { "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "tailwindcss": "4.1.8" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-CQ+I8yxNV5/6uGaJjiuymgw0kEQiNKRinYbZXPdx1fk5WgiyReG0VaUx/Xq6aVNSUNJFzxm6o8FNKS5aMaim5A=="],
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
|
||||
|
||||
|
@ -323,12 +326,16 @@
|
|||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
@ -443,7 +450,7 @@
|
|||
|
||||
"eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
|
||||
|
||||
"eslint-plugin-qwik": ["eslint-plugin-qwik@1.14.1", "", { "dependencies": { "@typescript-eslint/utils": "^8.31.0", "jsx-ast-utils": "^3.3.5" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-N0C8V/6F4EfRdC6EmE7o0VnUod9T16u94C+AD325xA0U4IQoC191uvkGeeJtJ7RXv6UPWz0Evh1aHTfaEILhkg=="],
|
||||
"eslint-plugin-qwik": ["eslint-plugin-qwik@1.15.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.31.0", "jsx-ast-utils": "^3.3.5" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-teDkSl0N/2vDCYK1LKfagnbE0A1RCV00tMekitVX6sfrfpM/nJ199Kyco5nrgp6qvhLJgFXsRBXGEEobPgn2jA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.3.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ=="],
|
||||
|
||||
|
@ -475,6 +482,8 @@
|
|||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-blurhash": ["fast-blurhash@1.1.4", "", {}, "sha512-xeH121M027hgWHHhHWYYjUmMKl8vCH3PPkXk439ixsP8Bvb/r3UFqg12oMSToD/aSAw8EE6XiTdfZ6M5jaLfzg=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
@ -545,6 +554,8 @@
|
|||
|
||||
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"imagetools-core": ["imagetools-core@7.1.0", "", {}, "sha512-8Aa4NecBBGmTkaAUjcuRYgTPKHCsBEWYmCnvKCL6/bxedehtVVFyZPdXe8DD0Nevd6UWBq85ifUaJ8498lgqNQ=="],
|
||||
|
@ -635,7 +646,7 @@
|
|||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"ky": ["ky@1.8.1", "", {}, "sha512-7Bp3TpsE+L+TARSnnDpk3xg8Idi8RwSLdj6CMbNWoOARIrGrbuLGusV0dYwbZOm4bB3jHNxSw8Wk/ByDqJEnDw=="],
|
||||
"ky": ["ky@1.8.2", "", {}, "sha512-XybQJ3d4Ea1kI27DoelE5ZCT3bSJlibYTtQuMsyzKox3TMyayw1asgQdl54WroAm+fIA3ZCr8zXW2RpR7qWVpA=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
|
@ -811,7 +822,7 @@
|
|||
|
||||
"prettier": ["prettier@3.3.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew=="],
|
||||
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.12", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw=="],
|
||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="],
|
||||
|
||||
"prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
|
||||
|
||||
|
@ -909,12 +920,14 @@
|
|||
|
||||
"tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="],
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"thumbhash": ["thumbhash@0.1.1", "", {}, "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
@ -943,7 +956,7 @@
|
|||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici": ["undici@7.10.0", "", {}, "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw=="],
|
||||
"undici": ["undici@7.12.0", "", {}, "sha512-GrKEsc3ughskmGA9jevVlIOPMiiAHJ4OFUtaAH+NhfTUSiZ1wMPIQqQvAJUrJspFXJt3EBWgpAeoHEDVT1IBug=="],
|
||||
|
||||
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
|
||||
|
||||
|
@ -1009,7 +1022,7 @@
|
|||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.10", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ=="],
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
|
@ -1051,6 +1064,8 @@
|
|||
|
||||
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.26.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.26.1", "", {}, "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.26.1", "", {}, "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ=="],
|
||||
|
|
23
package.json
23
package.json
|
@ -24,28 +24,31 @@
|
|||
"qwik": "qwik"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@builder.io/qwik": "^1.14.1",
|
||||
"@builder.io/qwik-city": "^1.14.1",
|
||||
"@builder.io/qwik": "^1.15.0",
|
||||
"@builder.io/qwik-city": "^1.15.0",
|
||||
"@eslint/js": "latest",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@types/node": "20.14.11",
|
||||
"eslint": "9.25.1",
|
||||
"eslint-plugin-qwik": "^1.14.1",
|
||||
"eslint-plugin-qwik": "^1.15.0",
|
||||
"globals": "16.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "5.4.5",
|
||||
"typescript-eslint": "8.26.1",
|
||||
"undici": "*",
|
||||
"undici": "^7.12.0",
|
||||
"vite": "5.3.5",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/aos": "^3.0.7",
|
||||
"aos": "^3.0.0-beta.6",
|
||||
"ky": "^1.8.1",
|
||||
"buffer": "^6.0.3",
|
||||
"fast-blurhash": "^1.1.4",
|
||||
"ky": "^1.8.2",
|
||||
"nanostores": "^1.0.1",
|
||||
"tailwind-scrollbar": "^4.0.2"
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"thumbhash": "^0.1.1"
|
||||
}
|
||||
}
|
||||
|
|
2
public
2
public
|
@ -1 +1 @@
|
|||
Subproject commit a4f5725fbaf2db1053be198d81d08e1ea0c3e843
|
||||
Subproject commit ffdd93af120e3bec48736bfc8a2ae7824c941cfa
|
93
src/components/dashboard/Actionbar.tsx
Normal file
93
src/components/dashboard/Actionbar.tsx
Normal file
|
@ -0,0 +1,93 @@
|
|||
/* eslint-disable qwik/jsx-a */
|
||||
import { $, component$, useSignal } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { isSettingsOpen, loadedFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
import { SolarLibraryLinear, SolarQuestionCircleLinear, SolarRoundedMagniferLinear, SolarSettingsLinear, SolarUploadMinimalisticLinear, StereoCircularProgress, StereoLogoLinear } from "../misc/Icons";
|
||||
|
||||
export default component$(() => {
|
||||
const used = 3.8;
|
||||
const total = 15;
|
||||
|
||||
const settingsOpen = useNanostore$<boolean>(isSettingsOpen);
|
||||
const fileInputRef = useSignal<HTMLInputElement>();
|
||||
const files = useNanostore$<StereoFile[]>(loadedFiles, []);
|
||||
|
||||
const handleFileChange = $(async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
const fi: File[] = Array.from(input.files);
|
||||
|
||||
const metas: StereoFile[] = await Promise.all(
|
||||
fi.map(async (file) => {
|
||||
try {
|
||||
const id = (await (await api.upload(file)).json()).id;
|
||||
return await api.meta(id);
|
||||
} catch (error) {
|
||||
console.error("actionbar: file upload failed:", error);
|
||||
throw new Error("File upload failed");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
input.value = "";
|
||||
//files.value = [...files.value, ...metas].sort((a, b) => new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime());
|
||||
files.value = [...files.value, ...metas]
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="absolute bottom-0 left-0 flex items-center justify-between py-7 px-16 w-full">
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center px-6 py-4 h-full gap-2 text-white text-xl">
|
||||
<StereoCircularProgress value={used/total} class="text-2xl"/>
|
||||
<p class="hidden md:block">{used} / {total} GB</p>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center px-6 py-4 gap-5 text-white text-3xl absolute left-1/2 transform -translate-x-1/2">
|
||||
<StereoLogoLinear />
|
||||
<SolarLibraryLinear />
|
||||
<a
|
||||
onClick$={(e) => {
|
||||
e.preventDefault();
|
||||
fileInputRef.value?.click();
|
||||
}}
|
||||
>
|
||||
<SolarUploadMinimalisticLinear />
|
||||
</a>
|
||||
<SolarRoundedMagniferLinear />
|
||||
<a onClick$={() => settingsOpen.value = true}><SolarSettingsLinear /></a>
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
class="hidden"
|
||||
onChange$={handleFileChange}
|
||||
/>
|
||||
|
||||
<div style={{
|
||||
borderRadius: "999px",
|
||||
border: "0.5px solid #FF264E",
|
||||
background: "rgba(255, 38, 78, 0.15)",
|
||||
boxShadow: "0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
backdropFilter: "blur(12px)",
|
||||
}} class="flex items-center justify-center p-4 gap-2 text-white text-3xl h-full">
|
||||
<SolarQuestionCircleLinear />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,88 +0,0 @@
|
|||
import { $, component$, noSerialize, NoSerialize, useSignal, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "../misc/Icons";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
|
||||
export default component$(() => {
|
||||
const loaded = useNanostore$<boolean>(areFilesLoaded);
|
||||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
const fileInputRef = useSignal<HTMLInputElement>();
|
||||
const uploadingFiles = useSignal<NoSerialize<File[]> | undefined>();
|
||||
const now = useSignal(new Date());
|
||||
|
||||
useVisibleTask$(() => {
|
||||
const interval = setInterval(() => {
|
||||
now.value = new Date();
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
|
||||
const uploadFiles = $(async () => {
|
||||
if (!uploadingFiles.value) {
|
||||
console.error("No file(s) selected for upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const ufiles = uploadingFiles.value as File[];
|
||||
|
||||
for (const file of ufiles) {
|
||||
const name = file.name.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
||||
const f = new File([file], name, { type: file.type });
|
||||
|
||||
await api.upload(f);
|
||||
}
|
||||
|
||||
files.value = await api.list();
|
||||
} catch (error) {
|
||||
console.error("Error uploading file:", error);
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div class="z-[999999999] fixed bottom-4 left-1/2 transform -translate-x-1/2 bg-neutral-700/10 backdrop-blur-3xl lg:w-1/3 md:w-2/3 w-4/5 p-2 pr-4 rounded-lg flex items-center justify-between">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style="display: none;"
|
||||
onChange$={async (e: Event) => {
|
||||
uploadingFiles.value = noSerialize(Object.values((e.target as HTMLInputElement).files || {}));
|
||||
await uploadFiles();
|
||||
}}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
{/* TODO: replace this button with a modal with options like settings log out etc */}
|
||||
<button
|
||||
class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg"
|
||||
onClick$={async () => {
|
||||
loaded.value = false;
|
||||
files.value = await api.list()
|
||||
loaded.value = true;
|
||||
}}
|
||||
>
|
||||
{
|
||||
loaded.value ? (
|
||||
<StereoLogo class="w-6 h-6" />
|
||||
) : (
|
||||
<SvgSpinnersBarsRotateFade class="w-6 h-6" />
|
||||
)
|
||||
}
|
||||
</button>
|
||||
|
||||
<p class="text-white/25 font-light text-xl"> | </p>
|
||||
|
||||
<button
|
||||
class="duration-100 hover:bg-white text-white hover:text-black p-2 rounded-lg"
|
||||
onClick$={() => { fileInputRef.value?.click() }}
|
||||
>
|
||||
<SolarUploadLinear class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-white font-medium">{now.value.toLocaleTimeString()}</p>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,199 +0,0 @@
|
|||
import { $, component$, Signal, useSignal, useTask$ } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { api } from "~/lib/api";
|
||||
import { dashboardFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
import { SolarClipboardAddBold, SolarDownloadMinimalisticBold, SolarTrashBin2Bold } from "../misc/Icons";
|
||||
|
||||
type FileProps = {
|
||||
file: StereoFile;
|
||||
}
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
export default component$(({ file }: FileProps) => {
|
||||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
|
||||
const deleteFile = $(async (id: string) => {
|
||||
if (!confirm("Are you sure you want to delete this file?")) return;
|
||||
await api.delete(id);
|
||||
files.value = await api.list();
|
||||
});
|
||||
|
||||
const addFileToClipboard = $(async () => {
|
||||
const response = await api.file(file.Name);
|
||||
const data = await response.blob();
|
||||
let mime = data.type || "application/octet-stream";
|
||||
let clip;
|
||||
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
if (mime === "image/jpeg" || mime === "image/jpg") {
|
||||
const img = document.createElement("img");
|
||||
img.src = URL.createObjectURL(data);
|
||||
await new Promise((res) => (img.onload = res));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
const png = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((b) => resolve(b!), "image/png")
|
||||
);
|
||||
mime = "image/png";
|
||||
clip = new ClipboardItem({ [mime]: png });
|
||||
} else {
|
||||
clip = new ClipboardItem({ [mime]: data });
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.write([clip]);
|
||||
alert("File added to clipboard successfully!");
|
||||
} catch (error) {
|
||||
console.error("Failed to add file to clipboard:", error);
|
||||
alert("Failed to add file to clipboard. Please try again.");
|
||||
}
|
||||
} else {
|
||||
alert("Clipboard API not supported in this browser.");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
|
||||
<div class="relative">
|
||||
<a
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
class="flex w-full h-60 overflow-clip"
|
||||
>
|
||||
<div class="flex flex-grow group-hover:scale-105 transition-all duration-500 bg-neutral-800">
|
||||
<FilePreview file={file} />
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
|
||||
<a
|
||||
class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
>
|
||||
<SolarDownloadMinimalisticBold class="w-6 h-6"/>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="bg-green-600/50 backdrop-blur-lg hover:bg-green-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
onClick$={async () => await addFileToClipboard()}
|
||||
>
|
||||
<SolarClipboardAddBold class="w-6 h-6"/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
onClick$={async () => await deleteFile(file.ID)}
|
||||
>
|
||||
<SolarTrashBin2Bold class="w-6 h-6"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="p-4 flex flex-col w-full text-center">
|
||||
<p class="text-lg font-semibold text-white w-full truncate">
|
||||
{ file.Name || "Untitled" }
|
||||
</p>
|
||||
<div class="flex gap-1 text-sm text-neutral-500 items-center justify-center">
|
||||
<span>{ formatSize(file.Size) }</span>
|
||||
<span class="text-neutral-600">•</span>
|
||||
<span>Uploaded on { new Date(file.CreatedAt).toLocaleDateString() }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const FilePreview = component$(({ file }: FileProps) => {
|
||||
type FileType =
|
||||
| "image"
|
||||
| "video"
|
||||
| "audio"
|
||||
| "other";
|
||||
|
||||
const type: Signal<FileType> = useSignal<FileType>("other");
|
||||
const extension = file.Name.split('.').pop()?.toLowerCase() || "";
|
||||
|
||||
useTask$(async () => {
|
||||
if (
|
||||
["png", "jpg", "jpeg", "gif"]
|
||||
.includes(extension)) type.value = "image";
|
||||
|
||||
else if (
|
||||
["mp4", "webm", "ogg", "avi", "mov", "mkv"]
|
||||
.includes(extension)) type.value = "video";
|
||||
else if (
|
||||
["mp3", "wav", "ogg", "flac", "aac"]
|
||||
.includes(extension)) type.value = "audio";
|
||||
|
||||
else type.value = "other";
|
||||
});
|
||||
|
||||
switch (type.value) {
|
||||
case "image":
|
||||
return (
|
||||
<div class="w-full h-60 object-cover flex-grow relative">
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "video":
|
||||
return (
|
||||
<div class="w-full h-60 object-cover flex-grow relative">
|
||||
<video
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
class="w-full h-60 object-cover flex-grow"
|
||||
controls
|
||||
autoplay
|
||||
muted
|
||||
>
|
||||
<div class="w-full h-60 flex items-center justify-center">
|
||||
<p class="text-white/50 text-lg font-light">
|
||||
Preview not available
|
||||
</p>
|
||||
</div>
|
||||
</video>
|
||||
</div>
|
||||
);
|
||||
case "audio":
|
||||
return (
|
||||
<div class="w-full h-60 flex items-center justify-center p-2">
|
||||
<audio
|
||||
controls
|
||||
class="w-full h-12"
|
||||
src={`/api/${file.ID}`}
|
||||
>
|
||||
Your browser does not support the audio element.
|
||||
</audio>
|
||||
</div>
|
||||
);
|
||||
case "other":
|
||||
default:
|
||||
return (
|
||||
<div class="w-full h-60 flex items-center justify-center">
|
||||
<p class="text-white/50 text-lg font-light">
|
||||
Preview not available
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
259
src/components/dashboard/Settings.tsx
Normal file
259
src/components/dashboard/Settings.tsx
Normal file
|
@ -0,0 +1,259 @@
|
|||
// import { component$ } from "@builder.io/qwik";
|
||||
// import { useNanostore$ } from "~/hooks/nanostores";
|
||||
// import { isSettingsOpen, userInfo } from "~/lib/stores";
|
||||
// import { StereoUser } from "~/lib/types";
|
||||
|
||||
import { $, component$, useComputed$, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import ky from "ky";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { isSettingsOpen, userInfo } from "~/lib/stores";
|
||||
|
||||
const StorageAndPlan = component$(() => {
|
||||
const user = useNanostore$(userInfo);
|
||||
|
||||
useVisibleTask$(({track}) => {
|
||||
if (user.value) {
|
||||
console.log(user.value.global_name);
|
||||
}
|
||||
});
|
||||
|
||||
const title = useSignal("this is a test");
|
||||
const description = useSignal("this is a test description");
|
||||
const color = useSignal("#FF264E");
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-stereo" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a6 6 0 016 6v2a6 6 0 01-12 0V8a6 6 0 016-6zm0 2a4 4 0 00-4 4v2a4 4 0 008 0V8a4 4 0 00-4-4z"/></svg>
|
||||
<p class="text-white/80">current plan: <span class="text-stereo font-semibold">pro+</span></p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-stereo" fill="currentColor" viewBox="0 0 20 20"><path d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm0 2h12v10H4V5zm2 2v6h8V7H6z"/></svg>
|
||||
<p class="text-white/80">storage used: <span class="font-semibold text-white">3.8</span> / <span class="text-white/50">15 GB</span></p>
|
||||
</div>
|
||||
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden mt-1 mb-2">
|
||||
<div class="h-full bg-stereo rounded-full w-[calc(3.8/15*100%)]" />
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-white/40">upgrade your plan for more features</p>
|
||||
</div>
|
||||
|
||||
<p class="text-xl">embed editor</p>
|
||||
{user.value && (
|
||||
<div class="flex gap-3">
|
||||
<div class="flex gap-3">
|
||||
<img
|
||||
class="rounded-full w-12 h-12"
|
||||
src={
|
||||
user.value.id && user.value.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${user.value.id}/${user.value.avatar}.webp?size=512`
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-1 items-center">
|
||||
<p>{user.value.global_name}</p>
|
||||
<p class="text-sm opacity-50">{new Date().toLocaleTimeString().split(":").slice(0, 2).join(":")}</p>
|
||||
</div>
|
||||
<div class="overflow-clip flex bg-black/25 rounded-lg">
|
||||
{(title.value || description.value) ? (
|
||||
<>
|
||||
<div class="w-1" style={{ backgroundColor: color.value }} />
|
||||
<div class="flex flex-col p-2 gap-2 border-2 border-white/10 border-l-0 rounded-r-lg">
|
||||
<div class="flex flex-col">
|
||||
{ title.value && <p>{title.value}</p> }
|
||||
{ description.value && <p class="text-sm opacity-50">{description.value}</p> }
|
||||
</div>
|
||||
|
||||
<img
|
||||
class="rounded-sm aspect-[3/2]"
|
||||
src="https://placehold.co/300x200"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<img
|
||||
class="rounded-sm aspect-[3/2]"
|
||||
src="https://placehold.co/300x200"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-white/50">title</p>
|
||||
<input
|
||||
type="text"
|
||||
class="bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-stereo transition"
|
||||
placeholder="enter a title..."
|
||||
value={title.value}
|
||||
onInput$={(e) => title.value = (e.target as HTMLInputElement).value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-white/50">description</p>
|
||||
<textarea
|
||||
class="bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-stereo transition"
|
||||
placeholder="enter a description..."
|
||||
rows={3}
|
||||
value={description.value}
|
||||
onInput$={(e) => description.value = (e.target as HTMLTextAreaElement).value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<p class="text-sm text-white/50">color</p>
|
||||
<input
|
||||
type="color"
|
||||
class="bg-black/40 rounded-lg w-full"
|
||||
value={color.value}
|
||||
onInput$={(e) => color.value = (e.target as HTMLInputElement).value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Integrations = component$(() => {
|
||||
return (
|
||||
<div>
|
||||
<p>manage your api keys and integrations here</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const PrivacyAndSecurity = component$(() => {
|
||||
return (
|
||||
<div>
|
||||
<p>manage your privacy settings and security options</p>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const DangerZone = component$(() => {
|
||||
const handleLogout = $(() => {
|
||||
ky.get("/api/auth/logout", { credentials: "include" })
|
||||
.then(() => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>delete your account and data here</p>
|
||||
<button
|
||||
onClick$={handleLogout}
|
||||
class="text-white bg-stereo hover:bg-stereo/80 transition-colors rounded-lg px-4 py-2"
|
||||
>
|
||||
log out
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
export default component$(() => {
|
||||
const open = useNanostore$<boolean>(isSettingsOpen);
|
||||
const visible = useSignal(false);
|
||||
const shouldRender = useSignal(open.value);
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => open.value);
|
||||
if (open.value) {
|
||||
shouldRender.value = true;
|
||||
setTimeout(() => { visible.value = true; }, 10);
|
||||
} else {
|
||||
visible.value = false;
|
||||
setTimeout(() => { shouldRender.value = false; }, 300);
|
||||
}
|
||||
});
|
||||
|
||||
const categories = useSignal([
|
||||
{
|
||||
name: "storage & plan",
|
||||
description: "manage your storage and plan details",
|
||||
component: StorageAndPlan
|
||||
},
|
||||
{
|
||||
name: "api & integrations",
|
||||
description: "manage your api keys and integrations",
|
||||
component: Integrations
|
||||
},
|
||||
{
|
||||
name: "privacy & security",
|
||||
description: "manage your privacy settings and security options",
|
||||
component: PrivacyAndSecurity
|
||||
},
|
||||
{
|
||||
name: "danger zone",
|
||||
description: "delete your account and data",
|
||||
component: DangerZone
|
||||
}
|
||||
])
|
||||
|
||||
const selectedCategory = useSignal(0);
|
||||
const SelectedComponent = useComputed$(() => {
|
||||
return categories.value[selectedCategory.value].component;
|
||||
});
|
||||
|
||||
if (!shouldRender.value) return <></>;
|
||||
return (
|
||||
<div
|
||||
onClick$={() => (open.value = false)}
|
||||
style={{
|
||||
opacity: visible.value ? 1 : 0,
|
||||
}}
|
||||
class="z-[50] fixed inset-0 flex items-center justify-center bg-black/50 text-white text-6xl backdrop-blur-3xl transition-opacity duration-300"
|
||||
>
|
||||
<div
|
||||
data-aos="fade-up"
|
||||
data-aos-duration="400"
|
||||
data-aos-anchor-placement="top-center"
|
||||
data-aos-easing="ease-out-quad"
|
||||
style={{
|
||||
border: "0.5px solid #FF264E",
|
||||
boxShadow:
|
||||
"0px 4px 20px 0px rgba(255, 38, 78, 0.08), 0px 8px 12px 0px rgba(0, 0, 0, 0.12), 0px 4px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 1px 0px rgba(0, 0, 0, 0.04), 0px 4px 8px 0px rgba(255, 38, 78, 0.12) inset, 0px 1px 3px 0px rgba(255, 38, 78, 0.24) inset",
|
||||
}}
|
||||
onClick$={e => e.stopPropagation()}
|
||||
class="flex gap-8 bg-black/30 bg-gradient-to-t from-stereo/20 to-transparent p-8 rounded-3xl shadow-lg w-4/7 h-4/7"
|
||||
>
|
||||
<div class="flex flex-col justify-between flex-1/4 border-r-2 border-white/15">
|
||||
<h1 class="text-2xl">settings</h1>
|
||||
<div>
|
||||
{categories.value.map((category, i) => (
|
||||
<div
|
||||
key={category.name}
|
||||
class={[
|
||||
"group py-2 px-3 transition-colors cursor-pointer rounded-l-xl select-none",
|
||||
selectedCategory.value === i
|
||||
? "bg-white/10 text-white"
|
||||
: "hover:bg-white/5 text-white/75 hover:text-white"
|
||||
].join(" ")}
|
||||
onClick$={() => (selectedCategory.value = i)}
|
||||
>
|
||||
<h2 class="text-lg">{category.name}</h2>
|
||||
<p class="text-sm text-white/25">{category.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-3/4 text-lg overflow-clip overflow-y-auto">
|
||||
<h1 class="text-2xl mb-2">{categories.value[selectedCategory.value].name}</h1>
|
||||
<SelectedComponent.value />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
54
src/components/dashboard/Thumbhash.tsx
Normal file
54
src/components/dashboard/Thumbhash.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { component$, QwikIntrinsicElements, useSignal, useTask$ } from "@builder.io/qwik";
|
||||
import { thumbHashToRGBA } from "thumbhash";
|
||||
|
||||
export default component$<QwikIntrinsicElements['img'] & {
|
||||
hash: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>(({ hash, width, height, class: cls, ...otherProps }) => {
|
||||
const dataUrl = useSignal<string | null>(null);
|
||||
|
||||
useTask$(() => {
|
||||
if (!hash) return;
|
||||
|
||||
// Decode base64 to Uint8Array (browser-safe)
|
||||
const binary = atob(hash);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
|
||||
try {
|
||||
const image = thumbHashToRGBA(bytes);
|
||||
console.log("pixels", image);
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = image.w;
|
||||
canvas.height = image.h;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
console.error("failed to get canvas context");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = ctx.createImageData(image.w, image.h);
|
||||
imageData.data.set(image.rgba);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
ctx.drawImage(canvas, 0, 0);
|
||||
dataUrl.value = canvas.toDataURL("image/png");
|
||||
} catch (e) {
|
||||
console.error("ThumbHash decode error:", e);
|
||||
}
|
||||
});
|
||||
|
||||
return dataUrl.value ? (
|
||||
<><img
|
||||
src={dataUrl.value}
|
||||
width={width}
|
||||
height={height}
|
||||
class={["object-cover", cls]}
|
||||
alt="blurhash placeholder"
|
||||
{...otherProps}
|
||||
/></>
|
||||
) : null;
|
||||
});
|
60
src/components/dashboard/TitleBar.tsx
Normal file
60
src/components/dashboard/TitleBar.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { component$, useTask$ } from "@builder.io/qwik";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
import { userInfo } from "~/lib/stores";
|
||||
import { StereoUser } from "~/lib/types";
|
||||
|
||||
export default component$(() => {
|
||||
const greetings = [
|
||||
"what's on the agenda today, <username>?",
|
||||
"what's on your mind, <username>?",
|
||||
"what's the plan, <username>?",
|
||||
"ready to rock, <username>?",
|
||||
"what's brewing, <username>?",
|
||||
"what's the latest, <username>?",
|
||||
"how's your day going, <username>?",
|
||||
"need some inspiration, <username>?",
|
||||
"let's make some noise, <username>!",
|
||||
"welcome back, <username>!",
|
||||
"good to see you, <username>!",
|
||||
"what are we making today, <username>?",
|
||||
"time to make some magic, <username>!",
|
||||
"let's get creative, <username>!",
|
||||
"what's the vibe today, <username>?",
|
||||
"let's create something awesome, <username>!",
|
||||
"what's the next big thing, <username>?",
|
||||
"let's turn ideas into reality, <username>!",
|
||||
"let's see your next masterpiece, <username>!",
|
||||
"let's make some art, <username>!",
|
||||
"what's the next hit, <username>?",
|
||||
"let's make some music, <username>!",
|
||||
"let's make some waves, <username>!",
|
||||
"what brilliance awaits, <username>?",
|
||||
"ready to brainstorm, <username>?",
|
||||
"let's bring ideas to life, <username>!",
|
||||
"don't get any ideas, <username>...",
|
||||
"let's try this again, <username>!",
|
||||
"let's get this party started, <username>!",
|
||||
"let's make something unforgettable, <username>!",
|
||||
]
|
||||
|
||||
const greeting = greetings[Math.floor(Math.random() * greetings.length)];
|
||||
const user = useNanostore$<StereoUser>(userInfo);
|
||||
|
||||
const splits = greeting.split("<username>");
|
||||
|
||||
useTask$(({ track }) => {
|
||||
track(() => user.value);
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="rounded-lg">
|
||||
<p class="font-medium text-4xl text-stereo/45">
|
||||
{splits[0]}
|
||||
<span class="text-stereo">
|
||||
@{user.value?.username || "..."}
|
||||
</span>
|
||||
{splits[1]}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -1,5 +1,4 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
|
||||
export default component$(() => (
|
||||
<div
|
||||
|
@ -14,7 +13,7 @@ export default component$(() => (
|
|||
join over <span class="text-stereo">100k</span> other people hosting their files with <span class="text-stereo">stereo</span>!
|
||||
</p>
|
||||
<a
|
||||
href={OAUTH_LINK}
|
||||
href={"/api/auth/login"}
|
||||
class="px-12 py-1.5 mt-1.5 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
|
||||
>
|
||||
get started
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
import { StereoLogoBold } from "../misc/Icons";
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
|
@ -7,7 +7,7 @@ export default component$(() => {
|
|||
<div class="flex flex-col flex-shrink h-full justify-start items-start gap-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="flex gap-[1ch]">
|
||||
<StereoLogo class="w-8 h-8 text-stereo" />
|
||||
<StereoLogoBold class="w-8 h-8 text-stereo" />
|
||||
<span class="text-white font-medium text-2xl">stereo<span class="text-stereo font-bold">.</span>cat</span>
|
||||
</span>
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { component$, useSignal } from "@builder.io/qwik";
|
||||
import { useRelativeMouse } from "~/hooks/mouse";
|
||||
import { OAUTH_LINK } from "~/lib/constants";
|
||||
import GradientBorder from "../misc/GradientBorder";
|
||||
|
||||
export default component$(() => {
|
||||
|
@ -22,7 +21,7 @@ export default component$(() => {
|
|||
</p>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<a
|
||||
href={OAUTH_LINK}
|
||||
href={"/api/auth/login"}
|
||||
class="px-6 py-1 text-lg font-medium text-white bg-stereo rounded-full hover:text-stereo hover:bg-white transition duration-300"
|
||||
>
|
||||
get started
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { component$ } from "@builder.io/qwik";
|
||||
import StereoLogo from "../misc/StereoLogo";
|
||||
import { StereoLogoBold } from "../misc/Icons";
|
||||
|
||||
export default component$(() => {
|
||||
const items = [
|
||||
|
@ -17,7 +17,7 @@ export default component$(() => {
|
|||
data-aos-duration="1000"
|
||||
class="fixed flex items-center justify-start top-6 left-1/2 transform -translate-x-1/2 bg-neutral-950 p-8 h-10 rounded-full lg:w-2/3 md:w-4/5 w-4/5 z-[9999999] shadow-lg">
|
||||
<div class="flex flex-1/3">
|
||||
<StereoLogo class="w-10 h-10 text-stereo hover:text-white transition-all duration-300 hover:cursor-pointer" />
|
||||
<StereoLogoBold class="w-10 h-10 text-stereo hover:text-white transition-all duration-300 hover:cursor-pointer" />
|
||||
</div>
|
||||
<div class="w-full hidden md:flex flex-grow gap-4 items-center justify-center">
|
||||
{items.map(({ text, href, highlighted: h }) => (
|
||||
|
|
|
@ -2,7 +2,7 @@ import { $, component$, useOnDocument, useSignal } from "@builder.io/qwik";
|
|||
import ky from "ky";
|
||||
|
||||
export default component$(() => {
|
||||
type TestimonialProps = {
|
||||
type Testimonial = {
|
||||
nickname: string;
|
||||
pfp?: string;
|
||||
id?: string;
|
||||
|
@ -19,7 +19,31 @@ export default component$(() => {
|
|||
success: boolean;
|
||||
}
|
||||
|
||||
const Testimonial = component$(({ nickname, id, quote, pfp }: TestimonialProps) => {
|
||||
// Array of testimonial objects
|
||||
const testimonials: Testimonial[] = [
|
||||
{
|
||||
nickname: "grng",
|
||||
id: "829372486780715018",
|
||||
quote: "stereo is the best file host I've ever used, it's fast, reliable, and the interface is so clean and easy to use. I love it!"
|
||||
},
|
||||
{
|
||||
nickname: "hexlocation",
|
||||
pfp: "https://git.iwakura.rip/avatars/38bbf57a26f2891c59102582240386a4e2fa52b3999374673e0f4c4249ed4149?size=512",
|
||||
quote: "I've been using stereo for a while now, and I can't imagine going back to any other file host. It's just that good!"
|
||||
},
|
||||
{
|
||||
nickname: "typed",
|
||||
pfp: "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fclipartcraft.com%2Fimages%2Ffire-emoji-transparent-snapchat-4.png&f=1&nofb=1&ipt=d59b80eec4d535f7f10b618a0daa9c0689a408643eaa6c1a054c0a03e7ca1835",
|
||||
quote: "stereo.cat saved my house from a fire, because when I was signing up I realized the email I had to log into needed my phone for 2fa, so when I went to the kitchen and grabbed my phone I noticed the stove was on, I thankfully turned it off. Thank you stereo.cat"
|
||||
},
|
||||
{
|
||||
nickname: "starlo",
|
||||
pfp: "https://cdn.discordapp.com/avatars/962173926849519716/04af851a9954ba623b5d2eb5dd189785.webp?size=512",
|
||||
quote: "I was expecting that stereo would be a greater website for storing my images, and I wasn't wrong and my doubts are always correct, it's more cooler than I expected 🔥"
|
||||
}
|
||||
];
|
||||
|
||||
const Testimonial = component$(({ nickname, id, quote, pfp }: Testimonial) => {
|
||||
const lanyard = useSignal<LanyardResponse>();
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(async () => {
|
||||
|
@ -28,7 +52,6 @@ export default component$(() => {
|
|||
try {
|
||||
const response = await ky.get(`https://api.lanyard.rest/v1/users/${id}`).json<LanyardResponse>();
|
||||
lanyard.value = response;
|
||||
console.log("Lanyard data:", lanyard.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching lanyard data:", error);
|
||||
}
|
||||
|
@ -87,22 +110,9 @@ export default component$(() => {
|
|||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Testimonial
|
||||
nickname="grng"
|
||||
id="829372486780715018"
|
||||
quote="stereo is the best file host I've ever used, it's fast, reliable, and the interface is so clean and easy to use. I love it!"
|
||||
/>
|
||||
|
||||
<Testimonial
|
||||
nickname="hexlocation"
|
||||
id="1325924978805440522"
|
||||
quote="I've been using stereo for a while now, and I can't imagine going back to any other file host. It's just that good!"
|
||||
/>
|
||||
|
||||
<Testimonial
|
||||
nickname="an anonymous user"
|
||||
quote="stereo has changed the way I share files, it's so easy to use and the performance is top-notch. Highly recommend!"
|
||||
/>
|
||||
{testimonials.map((t, i) => (
|
||||
<Testimonial key={i} {...t} />
|
||||
))}
|
||||
</div>
|
||||
<p class="text-xl text-white/50">
|
||||
and many, many more...
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { QwikIntrinsicElements } from "@builder.io/qwik";
|
||||
|
||||
// Solar - https://icones.js.org/collection/solar
|
||||
|
||||
export function SolarUploadLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="1.5"><path d="M17 9.002c2.175.012 3.353.109 4.121.877C22 10.758 22 12.172 22 15v1c0 2.829 0 4.243-.879 5.122C20.243 22 18.828 22 16 22H8c-2.828 0-4.243 0-5.121-.878C2 20.242 2 18.829 2 16v-1c0-2.828 0-4.242.879-5.121c.768-.768 1.946-.865 4.121-.877"></path><path stroke-linejoin="round" d="M12 15V2m0 0l3 3.5M12 2L9 5.5"></path></g></svg>
|
||||
|
@ -26,7 +28,6 @@ export function SolarLinkRoundBold(props: QwikIntrinsicElements['svg'], key: str
|
|||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarDownloadMinimalisticBold(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><path fill="currentColor" d="M12.554 16.506a.75.75 0 0 1-1.107 0l-4-4.375a.75.75 0 0 1 1.107-1.012l2.696 2.95V3a.75.75 0 0 1 1.5 0v11.068l2.697-2.95a.75.75 0 1 1 1.107 1.013z" /><path fill="currentColor" d="M3.75 15a.75.75 0 0 0-1.5 0v.055c0 1.367 0 2.47.117 3.337c.12.9.38 1.658.981 2.26c.602.602 1.36.86 2.26.982c.867.116 1.97.116 3.337.116h6.11c1.367 0 2.47 0 3.337-.116c.9-.122 1.658-.38 2.26-.982s.86-1.36.982-2.26c.116-.867.116-1.97.116-3.337V15a.75.75 0 0 0-1.5 0c0 1.435-.002 2.436-.103 3.192c-.099.734-.28 1.122-.556 1.399c-.277.277-.665.457-1.4.556c-.755.101-1.756.103-3.191.103H9c-1.435 0-2.437-.002-3.192-.103c-.734-.099-1.122-.28-1.399-.556c-.277-.277-.457-.665-.556-1.4c-.101-.755-.103-1.756-.103-3.191" /></svg>
|
||||
|
@ -36,4 +37,93 @@ export function SvgSpinnersBarsRotateFade(props: QwikIntrinsicElements['svg'], k
|
|||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".14" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".29" transform="rotate(30 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".43" transform="rotate(60 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".57" transform="rotate(90 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".71" transform="rotate(120 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".86" transform="rotate(150 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" transform="rotate(180 12 12)" /><animateTransform attributeName="transform" calcMode="discrete" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SolarLibraryLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M19.562 7a2.132 2.132 0 0 0-2.1-2.5H6.538a2.132 2.132 0 0 0-2.1 2.5M17.5 4.5c.028-.26.043-.389.043-.496a2 2 0 0 0-1.787-1.993C15.65 2 15.52 2 15.26 2H8.74c-.26 0-.391 0-.497.011a2 2 0 0 0-1.787 1.993c0 .107.014.237.043.496" /><path stroke-linecap="round" d="M15 18H9" /><path d="M2.384 13.793c-.447-3.164-.67-4.745.278-5.77C3.61 7 5.298 7 8.672 7h6.656c3.374 0 5.062 0 6.01 1.024s.724 2.605.278 5.769l-.422 3c-.35 2.48-.525 3.721-1.422 4.464s-2.22.743-4.867.743h-5.81c-2.646 0-3.97 0-4.867-.743s-1.072-1.983-1.422-4.464z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SolarUploadMinimalisticLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 15c0 2.828 0 4.243.879 5.121C4.757 21 6.172 21 9 21h6c2.828 0 4.243 0 5.121-.879C21 19.243 21 17.828 21 15m-9 1V3m0 0l4 4.375M12 3L8 7.375" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarRoundedMagniferLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="9" /><path stroke-linecap="round" d="M21.812 20.975c-.063.095-.176.208-.403.434c-.226.227-.34.34-.434.403a1.13 1.13 0 0 1-1.62-.408c-.053-.1-.099-.254-.19-.561c-.101-.335-.151-.503-.161-.621a1.13 1.13 0 0 1 1.218-1.218c.118.01.285.06.621.16c.307.092.46.138.56.192a1.13 1.13 0 0 1 .409 1.619Z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarSettingsLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3" /><path d="M13.765 2.152C13.398 2 12.932 2 12 2s-1.398 0-1.765.152a2 2 0 0 0-1.083 1.083c-.092.223-.129.484-.143.863a1.62 1.62 0 0 1-.79 1.353a1.62 1.62 0 0 1-1.567.008c-.336-.178-.579-.276-.82-.308a2 2 0 0 0-1.478.396C4.04 5.79 3.806 6.193 3.34 7s-.7 1.21-.751 1.605a2 2 0 0 0 .396 1.479c.148.192.355.353.676.555c.473.297.777.803.777 1.361s-.304 1.064-.777 1.36c-.321.203-.529.364-.676.556a2 2 0 0 0-.396 1.479c.052.394.285.798.75 1.605c.467.807.7 1.21 1.015 1.453a2 2 0 0 0 1.479.396c.24-.032.483-.13.819-.308a1.62 1.62 0 0 1 1.567.008c.483.28.77.795.79 1.353c.014.38.05.64.143.863a2 2 0 0 0 1.083 1.083C10.602 22 11.068 22 12 22s1.398 0 1.765-.152a2 2 0 0 0 1.083-1.083c.092-.223.129-.483.143-.863c.02-.558.307-1.074.79-1.353a1.62 1.62 0 0 1 1.567-.008c.336.178.579.276.819.308a2 2 0 0 0 1.479-.396c.315-.242.548-.646 1.014-1.453s.7-1.21.751-1.605a2 2 0 0 0-.396-1.479c-.148-.192-.355-.353-.676-.555A1.62 1.62 0 0 1 19.562 12c0-.558.304-1.064.777-1.36c.321-.203.529-.364.676-.556a2 2 0 0 0 .396-1.479c-.052-.394-.285-.798-.75-1.605c-.467-.807-.7-1.21-1.015-1.453a2 2 0 0 0-1.479-.396c-.24.032-.483.13-.82.308a1.62 1.62 0 0 1-1.566-.008a1.62 1.62 0 0 1-.79-1.353c-.014-.38-.05-.64-.143-.863a2 2 0 0 0-1.083-1.083Z" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function SolarQuestionCircleLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="1.5" /><path stroke="currentColor" stroke-linecap="round" stroke-width="1.5" d="M10.125 8.875a1.875 1.875 0 1 1 2.828 1.615c-.475.281-.953.708-.953 1.26V13" /><circle cx="12" cy="16" r="1" fill="currentColor" /></g></svg>
|
||||
)
|
||||
}
|
||||
|
||||
// Stereo
|
||||
|
||||
export function StereoLogoBold(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} key={key}><path d="M0.744835 19.1645C0.340705 16.0258 0.876443 12.8377 2.2843 10.0034L6.37392 1.7703C7.01957 0.470487 8.7912 0.269553 9.71155 1.39175L85.375 93.6495H195.875L271.538 1.39175C272.459 0.269551 274.23 0.470487 274.876 1.77029L278.966 10.0034C280.374 12.8377 280.909 16.0258 280.505 19.1645L264.378 144.419C256.8 203.277 206.688 247.35 147.344 247.35H133.906C74.5619 247.35 24.4504 203.277 16.872 144.419L0.744835 19.1645Z" fill="currentColor"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function StereoLogoLinear(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 21 18" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} key={key}><path d="M1.26893 2.381C1.21059 1.92943 1.28782 1.4706 1.49067 1.06358C1.58237 0.879581 1.8316 0.851009 1.96205 1.00954L6.83479 6.93126H14.1652L19.038 1.00954C19.1684 0.851008 19.4176 0.879581 19.5093 1.06358C19.7122 1.4706 19.7894 1.92943 19.7311 2.381L18.7758 9.77571C18.235 13.9617 14.693 17.0937 10.5 17.0937C6.30697 17.0937 2.76496 13.9617 2.2242 9.77571L1.26893 2.381Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function StereoCircularProgress(
|
||||
{ value, ...svgProps }: QwikIntrinsicElements['svg'] & { value: number },
|
||||
key: string
|
||||
) {
|
||||
const radius = 10;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const dashOffset = circumference * (1 - value);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...svgProps}
|
||||
key={key}
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-opacity={0.25}
|
||||
stroke-width={2}
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r={radius}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width={2}
|
||||
stroke-dasharray={`${circumference} ${circumference}`}
|
||||
stroke-dashoffset={dashOffset}
|
||||
transform="rotate(-90 12 12)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { component$, QwikIntrinsicElements } from "@builder.io/qwik";
|
||||
|
||||
export default component$((props: QwikIntrinsicElements['svg']) => {
|
||||
return (
|
||||
<svg width="281" height="248" viewBox="0 0 281 248" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M0.744835 19.1645C0.340705 16.0258 0.876443 12.8377 2.2843 10.0034L6.37392 1.7703C7.01957 0.470487 8.7912 0.269553 9.71155 1.39175L85.375 93.6495H195.875L271.538 1.39175C272.459 0.269551 274.23 0.470487 274.876 1.77029L278.966 10.0034C280.374 12.8377 280.909 16.0258 280.505 19.1645L264.378 144.419C256.8 203.277 206.688 247.35 147.344 247.35H133.906C74.5619 247.35 24.4504 203.277 16.872 144.419L0.744835 19.1645Z" fill="currentColor"/>
|
||||
</svg>
|
||||
)
|
||||
})
|
|
@ -1,7 +1,15 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap');
|
||||
|
||||
@import "tailwindcss";
|
||||
@plugin 'tailwind-scrollbar' {
|
||||
nocompatible: true;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans: 'DM Sans', sans-serif;
|
||||
--color-stereo: #ff264e;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply scrollbar-thin scrollbar-thumb-rounded-full scrollbar-track-rounded-full scrollbar-thumb-stereo/30 scrollbar-track-stereo/10;
|
||||
}
|
|
@ -14,8 +14,8 @@ function writeable<T>(store: Atom<T> | WritableAtom<T>): store is WritableAtom<T
|
|||
return typeof (store as WritableAtom<T>).set === 'function';
|
||||
}
|
||||
|
||||
export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>): Signal<T> {
|
||||
const signal = useSignal<T | undefined>(undefined);
|
||||
export function useNanostoreQrl<T>(qrl: QRL<WritableAtom<T> | Atom<T>>, defaultValue?: T): Signal<T> {
|
||||
const signal = useSignal<T | undefined>(defaultValue);
|
||||
const storeSignal = useSignal<NoSerialize<WritableAtom<T> | Atom<T>> | undefined>(undefined);
|
||||
|
||||
useTask$(async ({ track }) => {
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
import ky from 'ky';
|
||||
import { StereoFile } from './types';
|
||||
import ky from "ky";
|
||||
import { StereoFile, StereoUser } from "./types";
|
||||
|
||||
export const client = ky.create({
|
||||
prefixUrl: '/api',
|
||||
credentials: 'include'
|
||||
prefixUrl: "/api",
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
export const api = {
|
||||
file: async (uid: string) => await client.get<Blob>(uid),
|
||||
list: async () => await client.get('list').json<StereoFile[]>(),
|
||||
meta: async (uid: string) => await client.get<StereoFile>(uid + "/meta").json(),
|
||||
list: async (page?: number, size?: number) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (page !== undefined) searchParams.append("page", String(page));
|
||||
if (size !== undefined) searchParams.append("size", String(size));
|
||||
|
||||
return await client.get("list", { searchParams }).json<StereoFile[]>();
|
||||
},
|
||||
upload: async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
return await client.post('upload', { body: formData });
|
||||
formData.append("file", file);
|
||||
return await client.post<{id: string, message: string}>("upload", { body: formData });
|
||||
},
|
||||
delete: async (uid: string) => await client.delete(uid).json(),
|
||||
me: async () => (await client.get<any>("auth/me").json()).user as StereoUser,
|
||||
}
|
|
@ -1 +1 @@
|
|||
export const OAUTH_LINK = "https://discord.com/oauth2/authorize?client_id=1368939221678817382&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fapi%2Fauth%2Fcallback&scope=identify+email"
|
||||
//export const OAUTH_LINK = "https://discord.com/oauth2/authorize?client_id=1368939221678817382&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fapi%2Fauth%2Fcallback&scope=identify+email"
|
7
src/lib/misc.ts
Normal file
7
src/lib/misc.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function debounce<T extends (...args: any[]) => void>(fn: T, delay = 200) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
return (...args: Parameters<T>) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(async () => await fn(...args), delay);
|
||||
};
|
||||
}
|
|
@ -1,5 +1,16 @@
|
|||
import { atom } from "nanostores";
|
||||
import { StereoFile } from "./types";
|
||||
import { StereoFile, StereoUser } from "./types";
|
||||
|
||||
export const areFilesLoaded = atom<boolean>(false);
|
||||
export const dashboardFiles = atom<StereoFile[]>([]);
|
||||
export const dashboardFiles = atom<StereoFile[]>([]);
|
||||
export const userInfo = atom<StereoUser>({
|
||||
id: "1",
|
||||
username: "user",
|
||||
blacklisted: false,
|
||||
email: "user@example.com",
|
||||
avatar: "",
|
||||
global_name: "User",
|
||||
created_at: Date.now().toString(),
|
||||
});
|
||||
export const isSettingsOpen = atom<boolean>(false);
|
||||
export const loadedFiles = atom<StereoFile[]>([]);
|
|
@ -5,4 +5,17 @@ export type StereoFile = {
|
|||
Size: number;
|
||||
CreatedAt: string;
|
||||
Mime: string;
|
||||
Hash?: string;
|
||||
Width?: number;
|
||||
Height?: number;
|
||||
}
|
||||
|
||||
export type StereoUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
global_name: string;
|
||||
blacklisted: boolean;
|
||||
email: string;
|
||||
avatar: string;
|
||||
created_at: string;
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
import type { RequestEvent, RequestHandler } from '@builder.io/qwik-city';
|
||||
|
||||
const proxy = async ({ send, url, pathname, request }: RequestEvent) => {
|
||||
const targetUrl = new URL(`http://localhost:8081${pathname}`, url);
|
||||
const proxy = async ({ send, url, pathname, request, env }: RequestEvent) => {
|
||||
const backend = env.get("BACKEND_URI")
|
||||
if (!backend) {
|
||||
send(new Response("backend uri is not configured in .env.local", { status: 500 }));
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUrl = new URL(`${backend}${pathname}${url.search}`, url);
|
||||
const headers = new Headers(request.headers);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
|
|
|
@ -1,69 +1,218 @@
|
|||
import { component$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
/* eslint-disable qwik/jsx-img */
|
||||
/* eslint-disable qwik/no-use-visible-task */
|
||||
import { component$, Signal, useComputed$, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
|
||||
import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city";
|
||||
import Controlbar from "~/components/dashboard/Controlbar";
|
||||
// import Dropzone from "~/components/Dropzone";
|
||||
import File from "~/components/dashboard/File";
|
||||
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/misc/Icons";
|
||||
import Actionbar from "~/components/dashboard/Actionbar";
|
||||
import Settings from "~/components/dashboard/Settings";
|
||||
import Thumbhash from "~/components/dashboard/Thumbhash";
|
||||
import Titlebar from "~/components/dashboard/Titlebar";
|
||||
import { SvgSpinnersBarsRotateFade } from "~/components/misc/Icons";
|
||||
import { useNanostore$ } from "~/hooks/nanostores";
|
||||
// import Dropzone from "~/components/Dropzone";
|
||||
import { api } from "~/lib/api";
|
||||
import { areFilesLoaded, dashboardFiles } from "~/lib/stores";
|
||||
import { debounce } from "~/lib/misc";
|
||||
import { loadedFiles } from "~/lib/stores";
|
||||
import { StereoFile } from "~/lib/types";
|
||||
|
||||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r }) => {
|
||||
export const useAuthenticated = routeLoader$(({ cookie, redirect: r }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
if (jwt) return {};
|
||||
throw r(302, "/");
|
||||
|
||||
throw r(302, "/api/auth/login");
|
||||
});
|
||||
|
||||
export default component$(() => {
|
||||
const files = useNanostore$<StereoFile[]>(dashboardFiles);
|
||||
const loaded = useNanostore$<boolean>(areFilesLoaded);
|
||||
|
||||
useVisibleTask$(async () => {
|
||||
loaded.value = false;
|
||||
files.value = await api.list();
|
||||
console.log("Files loaded:", files.value);
|
||||
loaded.value = true;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Dropzone /> */}
|
||||
<Controlbar />
|
||||
{!loaded.value ? (
|
||||
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
|
||||
<p class="text-gray-500 text-8xl font-bold"><SvgSpinnersBarsRotateFade /></p>
|
||||
<p class="text-gray-700 text-2xl font-light italic">loading your files...</p>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">please wait... <span class="animate-spin">⏳</span></span>
|
||||
</div>
|
||||
) : (
|
||||
files.value.length === 0 ? (
|
||||
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
|
||||
<p class="text-gray-500 text-8xl font-bold">{
|
||||
[
|
||||
"┻━┻︵ \\(°□°)/ ︵ ┻━┻",
|
||||
"┻━┻︵ヽ(`Д´)ノ︵ ┻━┻",
|
||||
"ʕノ•ᴥ•ʔノ ︵ ┻━┻",
|
||||
"(╯°Д°)╯︵ /(.□ . \\)",
|
||||
"┬─┬ ︵ /(.□. \\)",
|
||||
"(/ .□.)\\ ︵╰(゜Д゜)╯︵ /(.□. \\)"
|
||||
].sort(() => Math.random() - 0.5)[0]
|
||||
}</p>
|
||||
<p class="text-gray-700 text-2xl font-light italic">you haven't uploaded any files yet!</p>
|
||||
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">click the <span class="animate-bounce p-0.5 bg-gray-500 rounded-sm"><SolarUploadLinear /></span> button to get started</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div class="grid xl:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-3 p-6 mb-14">
|
||||
{files.value.map((file) => (
|
||||
<File key={file.Name} file={file} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<Settings />
|
||||
<div class="flex flex-col w-full h-screen p-8 gap-6 bg-gradient-to-b from-stereo/20 to-transparent justify-self-end">
|
||||
<Titlebar />
|
||||
<Files />
|
||||
<Actionbar />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
}
|
||||
|
||||
const Files = component$(() => {
|
||||
const File = component$(({ file }: { file: StereoFile }) => {
|
||||
const Preview = component$(() => {
|
||||
type FileType = "image" | "video" | "audio" | "other";
|
||||
const fileType: Signal<FileType> = useSignal<FileType>("other");
|
||||
const type = file.Mime.split("/")[1];
|
||||
|
||||
useTask$(() => {
|
||||
if (["jpeg", "jpg", "png", "gif", "webp"].includes(type)) fileType.value = "image";
|
||||
else if (["mp4", "webm", "ogg", "avi", "mov"].includes(type)) fileType.value = "video";
|
||||
else if (["mp3", "wav", "flac", "aac"].includes(type)) fileType.value = "audio";
|
||||
else fileType.value = "other";
|
||||
});
|
||||
|
||||
const loaded = useSignal(false);
|
||||
return (
|
||||
<div class="w-full h-max object-cover flex-grow relative overflow-clip">
|
||||
{fileType.value === "image" && (
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: (file.Width && file.Height) ? `${file.Width} / ${file.Height}` : "16 / 9",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{file.Hash && !loaded.value && (
|
||||
<Thumbhash hash={file.Hash} width={file.Width || 0} height={file.Height || 0} class="absolute w-full h-full" />
|
||||
)}
|
||||
|
||||
<img
|
||||
width={400}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full min-h-30 object-cover flex-grow hover:scale-[102.5%] transition-all duration-500"
|
||||
onLoad$={() => loaded.value = true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{fileType.value === "video" && (
|
||||
<video
|
||||
width={400}
|
||||
src={`/api/${file.ID}`}
|
||||
controls
|
||||
class="w-full min-h-30 object-cover flex-grow hover:scale-[102.5%] transition-all duration-500"
|
||||
/>
|
||||
)}
|
||||
{fileType.value === "audio" && (
|
||||
<audio
|
||||
src={`/api/${file.ID}`}
|
||||
controls
|
||||
class="w-full min-h-30 object-cover flex-grow hover:scale-[102.5%] transition-all duration-500"
|
||||
/>
|
||||
)}
|
||||
{fileType.value === "other" && (
|
||||
<div class="w-full min-h-30 flex items-center justify-center bg-gray-200 text-gray-500">
|
||||
<p>unsupported file type</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(180deg, rgba(255, 38, 78, 0.00) 57.21%, rgba(255, 38, 78, 0.17) 100%); ",
|
||||
boxShadow: "0px 4px 21.2px 2px rgba(255, 38, 78, 0.05)",
|
||||
}}
|
||||
class="transition-all rounded-3xl flex flex-col overflow-clip items-center justify-center mb-4 break-inside-avoid"
|
||||
>
|
||||
<Preview />
|
||||
<div class="flex flex-col items-center justify-center text-center w-full h-full p-5">
|
||||
<p class="text-xl truncate w-full">{file.Name}</p>
|
||||
<p class="text-stereo/50 text-lg">
|
||||
{formatSize(file.Size)}
|
||||
<span class="text-stereo/40"> • </span>
|
||||
Uploaded on {new Date(file.CreatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const loadingMore = useSignal(false);
|
||||
const hasMore = useSignal(true);
|
||||
const sentinel = useSignal<HTMLDivElement>();
|
||||
|
||||
const files = useNanostore$<StereoFile[]>(loadedFiles, [])
|
||||
const sortedFiles = useComputed$(() => {
|
||||
return files.value.slice().sort((a, b) => new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime());
|
||||
})
|
||||
const page = useSignal(1);
|
||||
|
||||
// TODO: make it load enough images to fill the viewport instead
|
||||
useVisibleTask$(({ cleanup }) => {
|
||||
if (!sentinel.value) return;
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
const entry = entries[0];
|
||||
if (
|
||||
entry.isIntersecting &&
|
||||
!loadingMore.value &&
|
||||
hasMore.value
|
||||
) {
|
||||
loadingMore.value = true;
|
||||
|
||||
console.log("Loading more files...");
|
||||
|
||||
debounce(async () => {
|
||||
const newFiles = await api.list(page.value, 12);
|
||||
if (newFiles.length === 0) {
|
||||
hasMore.value = false;
|
||||
if (sentinel.value) observer.unobserve(sentinel.value);
|
||||
} else {
|
||||
//files.value = [...files.value, ...newFiles].sort((a, b) => new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime());
|
||||
files.value = [...files.value, ...newFiles]
|
||||
page.value++;
|
||||
}
|
||||
loadingMore.value = false;
|
||||
}, 50)();
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(sentinel.value);
|
||||
|
||||
cleanup(() => {
|
||||
if (observer && sentinel.value) observer.unobserve(sentinel.value);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="px-2 mb-6 flex-grow overflow-y-auto mask-clip-content rounded-3xl">
|
||||
<div class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 space-y-2 relative">
|
||||
<div>
|
||||
{sortedFiles.value.filter((_, index) => index % 4 === 0).map((file) => (
|
||||
<File key={file.ID} file={file} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{sortedFiles.value.filter((_, index) => index % 4 === 1).map((file) => (
|
||||
<File key={file.ID} file={file} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{sortedFiles.value.filter((_, index) => index % 4 === 2).map((file) => (
|
||||
<File key={file.ID} file={file} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{sortedFiles.value.filter((_, index) => index % 4 === 3).map((file) => (
|
||||
<File key={file.ID} file={file} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{hasMore.value && (
|
||||
<div
|
||||
ref={sentinel}
|
||||
class="absolute bottom-0 left-0 h-1/2 flex items-end justify-center pb-16 w-full"
|
||||
>
|
||||
{loadingMore.value && (
|
||||
<SvgSpinnersBarsRotateFade class="text-stereo text-3xl"/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!hasMore.value && <p class="text-center font-medium text-xl text-stereo/35 mb-16 mt-8">thats all folks!!!</p>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const head: DocumentHead = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable qwik/jsx-img */
|
||||
import { component$ } from "@builder.io/qwik";
|
||||
import { DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
|
||||
import { DocumentHead } from "@builder.io/qwik-city";
|
||||
import CallToAction from "~/components/landing/CallToAction";
|
||||
import Footer from "~/components/landing/Footer";
|
||||
import Hero from "~/components/landing/Hero";
|
||||
|
@ -8,16 +8,6 @@ import Navbar from "~/components/landing/Navbar";
|
|||
import Stats from "~/components/landing/Stats";
|
||||
import Testimonials from "~/components/landing/Testimonials";
|
||||
|
||||
export const useAuthCheck = routeLoader$(({ cookie, redirect: r, query }) => {
|
||||
const jwt = cookie.get("jwt");
|
||||
const set = Boolean(query.get("jwt_set"));
|
||||
|
||||
if (jwt && set) {
|
||||
throw r(302, "/dashboard");
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
export default component$(() => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import { $, component$, Slot, useOnDocument } from '@builder.io/qwik';
|
||||
import { $, component$, Slot, useOnDocument, useVisibleTask$ } from '@builder.io/qwik';
|
||||
import AOS from 'aos';
|
||||
import 'aos/dist/aos.css';
|
||||
import { useNanostore$ } from '~/hooks/nanostores';
|
||||
import { api } from '~/lib/api';
|
||||
import { userInfo } from '~/lib/stores';
|
||||
import { StereoUser } from '~/lib/types';
|
||||
|
||||
export default component$(() => {
|
||||
useOnDocument("DOMContentLoaded", $(() => {
|
||||
const info = useNanostore$<StereoUser>(userInfo);
|
||||
|
||||
useVisibleTask$(async () => {
|
||||
try {
|
||||
info.value = await api.me();
|
||||
} catch (err) {
|
||||
console.error("failed to fetch user info:", err);
|
||||
}
|
||||
})
|
||||
|
||||
useOnDocument("DOMContentLoaded", $(async () => {
|
||||
|
||||
AOS.init({
|
||||
once: true,
|
||||
duration: 1000,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue