Compare commits

..

28 commits

Author SHA1 Message Date
grngxd
d7512bc151 fix: update scrollbar colors for consistency and improve loading spinner visibility 2025-08-09 12:14:17 +01:00
grngxd
82bda3850f style: update text color for better visibility in StorageAndPlan component
style: enhance global scrollbar styling
2025-08-08 23:20:41 +01:00
grngxd
30b3ae5044 enhance embed editor 2025-08-08 23:01:23 +01:00
grngxd
393f08129b embeds 2025-08-08 22:32:02 +01:00
grngxd
8860f4a5ac remove hardcoding in proxy 2025-08-01 13:08:23 +01:00
grngxd
3905155ab7 refactor: simplify file sorting logic 2025-08-01 11:01:58 +01:00
grngxd
ae74906217 use thumbhash instead 2025-08-01 10:46:58 +01:00
grngxd
972c8930cd refactor: sort files by creation date and improve file display layout 2025-08-01 08:33:11 +01:00
grngxd
aaaebc20a5 file upload 2025-07-31 23:05:55 +01:00
grngxd
1599371df5 oops 2025-07-31 12:32:38 +01:00
grngxd
31a8d62f1d refactor: clean up unused imports and improve file loading logic 2025-07-31 12:29:37 +01:00
grngxd
4f54880400 Merge branch 'state' into dashboard-refresh 2025-07-31 11:31:29 +01:00
grngxd
60cc3e958c logging out 2025-07-31 10:57:45 +01:00
grngxd
5e5fc372cc um 2025-07-31 10:25:40 +01:00
grngxd
9240263490 style: enhance hover effect on media elements with smoother transitions 2025-07-30 11:36:14 +01:00
grngxd
dcfd3df713 companion commit to state branch on backend 2025-07-30 11:18:02 +01:00
grngxd
d5dfeac9c0 add description to settings pages 2025-07-30 10:56:09 +01:00
grngxd
5f7885a6bd style: update header color for selected category in settings 2025-07-29 23:12:36 +01:00
grngxd
f25e096063 refactor: replace OSBar with Actionbar and update TitleBar to Titlebar with more personalized greetings 2025-07-29 23:08:18 +01:00
grngxd
c0563b844a new masonry layout for dashboard 2025-07-29 23:02:40 +01:00
grngxd
361c4e0b62 new settings layout 2025-07-29 22:19:56 +01:00
grngxd
8e5dff01c0 chore: update dependencies and improve dashboard settings
- Updated Qwik and Qwik City to version 1.15.0
- Updated Tailwind CSS and related plugins to latest versions
- Added a new Settings component to manage user settings
- Integrated settings management into the OSBar component
- Improved file display in the dashboard with better responsiveness
- Enhanced error handling for user info fetching
- Removed unused id from Testimonials component
2025-07-29 21:27:49 +01:00
grngxd
f843155394 Add file preview component and enhance file size formatting in dashboard 2025-06-21 22:37:32 +01:00
grngxd
3217373024 q icon to osbar 2025-06-21 21:58:33 +01:00
grngxd
b36d3b76a1 Enhance dashboard components with new icons, layout adjustments, and improved file display 2025-06-21 20:27:32 +01:00
grngxd
f164351011 top bar 2025-06-21 17:44:55 +01:00
grngxd
f47a1b2226 templating dashboard & add user route to api client 2025-06-21 17:02:52 +01:00
grngxd
0d29d59937 change oauth link 2025-06-21 16:15:33 +01:00
29 changed files with 948 additions and 451 deletions

1
.env.local.example Normal file
View file

@ -0,0 +1 @@
BACKEND_URL=

View file

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

View file

@ -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=="],

View file

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

@ -1 +1 @@
Subproject commit a4f5725fbaf2db1053be198d81d08e1ea0c3e843
Subproject commit ffdd93af120e3bec48736bfc8a2ae7824c941cfa

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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[]>([]);

View file

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

View file

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

View file

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

View file

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

View file

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