diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..c4b41f6 --- /dev/null +++ b/agents.md @@ -0,0 +1,136 @@ +# Static Testing Strategy for UI Changes + +## Overview +This document outlines the static testing strategy to ensure code quality and build integrity after UI changes. Run these checks from the `web` directory to validate TypeScript and ESLint rules. + +## Commands + +### 1. Run All Checks +```bash +cd /home/sschuhmann/dev/rehearshalhub/web +npm run check +``` +This command runs both TypeScript and ESLint checks sequentially. + +### 2. Run TypeScript Check +```bash +cd /home/sschuhmann/dev/rehearshalhub/web +npm run typecheck +``` +Validates TypeScript types and catches unused variables/imports. + +### 3. Run ESLint +```bash +cd /home/sschuhmann/dev/rehearshalhub/web +npm run lint +``` +Enforces code style, formatting, and best practices. + +## When to Run +- **After Every UI Change**: Run `npm run check` to ensure no regressions. +- **Before Commits**: Add a pre-commit hook to automate checks. +- **Before Deployment**: Verify build integrity. + +## Common Issues and Fixes + +### 1. Unused Imports +**Error**: `TS6133: 'X' is declared but its value is never read.` +**Fix**: Remove the unused import or variable. + +**Example**: +```ts +// Before +import { useQuery } from "@tanstack/react-query"; // Unused + +// After +// Removed unused import +``` + +### 2. Missing Imports +**Error**: `TS2304: Cannot find name 'X'.` +**Fix**: Import the missing dependency. + +**Example**: +```ts +// Before +function Component() { + useEffect(() => {}, []); // Error: useEffect not imported +} + +// After +import { useEffect } from "react"; +function Component() { + useEffect(() => {}, []); +} +``` + +### 3. Formatting Issues +**Error**: ESLint formatting rules violated. +**Fix**: Use consistent indentation (2 spaces) and semicolons. + +**Example**: +```ts +// Before +function Component(){return
Hello
} + +// After +function Component() { + return
Hello
; +} +``` + +## Pre-Commit Hook +Automate checks using Husky: + +### Setup +1. Install Husky: +```bash +cd /home/sschuhmann/dev/rehearshalhub/web +npm install husky --save-dev +npx husky install +``` + +2. Add Pre-Commit Hook: +```bash +npx husky add .husky/pre-commit "npm run check" +``` + +### How It Works +- Before each commit, Husky runs `npm run check`. +- If checks fail, the commit is aborted. + +## Best Practices +1. **Run Checks Locally**: Always run `npm run check` before pushing code. +2. **Fix Warnings**: Address all warnings to maintain code quality. +3. **Review Changes**: Use `git diff` to review changes before committing. + +## Example Workflow +1. Make UI changes (e.g., update `AppShell.tsx`). +2. Run static checks: + ```bash + cd /home/sschuhmann/dev/rehearshalhub/web + npm run check + ``` +3. Fix any errors/warnings. +4. Commit changes: + ```bash + git add . + git commit -m "Update UI layout" + ``` + +## Troubleshooting +- **`tsc` not found**: Install TypeScript: + ```bash + npm install typescript --save-dev + ``` +- **ESLint errors**: Fix formatting or disable rules if necessary. +- **Build failures**: Check `npm run check` output for details. + +## Responsibilities +- **Developers**: Run checks before committing. +- **Reviewers**: Verify checks pass during PR reviews. +- **CI/CD**: Integrate `npm run check` into the pipeline. + +## Notes +- This strategy ensures UI changes are type-safe and follow best practices. +- Static checks do not replace manual testing (e.g., responsiveness, usability). diff --git a/api/uv.lock b/api/uv.lock index 1952b1b..2518e95 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -450,6 +450,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -785,6 +797,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] +[[package]] +name = "limits" +version = "5.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/69/826a5d1f45426c68d8f6539f8d275c0e4fcaa57f0c017ec3100986558a41/limits-5.8.0.tar.gz", hash = "sha256:c9e0d74aed837e8f6f50d1fcebcf5fd8130957287206bc3799adaee5092655da", size = 226104, upload-time = "2026-02-05T07:17:35.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/98/cb5ca20618d205a09d5bec7591fbc4130369c7e6308d9a676a28ff3ab22c/limits-5.8.0-py3-none-any.whl", hash = "sha256:ae1b008a43eb43073c3c579398bd4eb4c795de60952532dc24720ab45e1ac6b8", size = 60954, upload-time = "2026-02-05T07:17:34.425Z" }, +] + [[package]] name = "mako" version = "1.3.10" @@ -920,6 +946,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -1227,11 +1322,13 @@ dependencies = [ { name = "bcrypt" }, { name = "fastapi" }, { name = "httpx" }, + { name = "pillow" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-settings" }, { name = "python-jose", extra = ["cryptography"] }, { name = "python-multipart" }, { name = "redis", extra = ["hiredis"] }, + { name = "slowapi" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "uvicorn", extra = ["standard"] }, ] @@ -1264,6 +1361,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115" }, { name = "httpx", specifier = ">=0.27" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, + { name = "pillow", specifier = ">=10.0" }, { name = "pydantic", extras = ["email"], specifier = ">=2.7" }, { name = "pydantic-settings", specifier = ">=2.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, @@ -1273,6 +1371,7 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.9" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, + { name = "slowapi", specifier = ">=0.1.9" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0" }, { name = "testcontainers", extras = ["postgres"], marker = "extra == 'dev'", specifier = ">=4.7" }, { name = "types-python-jose", marker = "extra == 'dev'" }, @@ -1348,6 +1447,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "slowapi" +version = "0.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "limits" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/99/adfc7f94ca024736f061257d39118e1542bade7a52e86415a4c4ae92d8ff/slowapi-0.1.9.tar.gz", hash = "sha256:639192d0f1ca01b1c6d95bf6c71d794c3a9ee189855337b4821f7f457dddad77", size = 14028, upload-time = "2024-02-05T12:11:52.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/bb/f71c4b7d7e7eb3fc1e8c0458a8979b912f40b58002b9fbf37729b8cb464b/slowapi-0.1.9-py3-none-any.whl", hash = "sha256:cfad116cfb84ad9d763ee155c1e5c5cbf00b0d47399a769b227865f5df576e36", size = 14670, upload-time = "2024-02-05T12:11:50.898Z" }, +] + [[package]] name = "sqlalchemy" version = "2.0.48" diff --git a/docker-compose. b/docker-compose. new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 8df15b0..91e696a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -126,14 +126,17 @@ services: ports: - "8080:80" networks: + - frontend - rh_net depends_on: - api restart: unless-stopped networks: + frontend: + external: + name: proxy rh_net: - driver: bridge volumes: pg_data: diff --git a/web/package-lock.json b/web/package-lock.json index 4b7f05e..927c6b6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -28,7 +28,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "jsdom": "^25.0.0", - "typescript": "^5.5.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.57.2", "vite": "^5.4.1", "vitest": "^2.1.1" @@ -1667,14 +1667,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -2488,7 +2488,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-urls": { @@ -4301,7 +4301,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/web/package.json b/web/package.json index 0ac2b1b..c714c3f 100644 --- a/web/package.json +++ b/web/package.json @@ -34,7 +34,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "jsdom": "^25.0.0", - "typescript": "^5.5.3", + "typescript": "^5.9.3", "typescript-eslint": "^8.57.2", "vite": "^5.4.1", "vitest": "^2.1.1" diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx index 01994b4..5167dbd 100644 --- a/web/src/components/AppShell.tsx +++ b/web/src/components/AppShell.tsx @@ -1,629 +1,5 @@ -import { useRef, useEffect, useState } from "react"; -import { useLocation, useNavigate, matchPath } from "react-router-dom"; -import { useQuery } from "@tanstack/react-query"; -import { listBands } from "../api/bands"; -import { api } from "../api/client"; -import { logout } from "../api/auth"; -import type { MemberRead } from "../api/auth"; - -// ── Helpers ────────────────────────────────────────────────────────────────── - -function getInitials(name: string): string { - return name - .split(/\s+/) - .map((w) => w[0]) - .join("") - .toUpperCase() - .slice(0, 2); -} - -// ── Icons (inline SVG) ──────────────────────────────────────────────────────── - -function IconWaveform() { - return ( - - - - - - ); -} - -function IconLibrary() { - return ( - - - - ); -} - -function IconPlay() { - return ( - - - - ); -} - -function IconSettings() { - return ( - - - - - ); -} - -function IconMembers() { - return ( - - - - - - - ); -} - -function IconStorage() { - return ( - - - - - - - ); -} - -function IconChevron() { - return ( - - - - ); -} - -function IconSignOut() { - return ( - - - - - ); -} - -// ── NavItem ─────────────────────────────────────────────────────────────────── - -interface NavItemProps { - icon: React.ReactNode; - label: string; - active: boolean; - onClick: () => void; - disabled?: boolean; -} - -function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) { - const [hovered, setHovered] = useState(false); - - const color = active - ? "#e8a22a" - : disabled - ? "rgba(255,255,255,0.18)" - : hovered - ? "rgba(255,255,255,0.7)" - : "rgba(255,255,255,0.35)"; - - const bg = active - ? "rgba(232,162,42,0.12)" - : hovered && !disabled - ? "rgba(255,255,255,0.045)" - : "transparent"; - - return ( - - ); -} - -// ── AppShell ────────────────────────────────────────────────────────────────── +import { ResponsiveLayout } from "./ResponsiveLayout"; export function AppShell({ children }: { children: React.ReactNode }) { - const navigate = useNavigate(); - const location = useLocation(); - const [dropdownOpen, setDropdownOpen] = useState(false); - const dropdownRef = useRef(null); - - const { data: bands } = useQuery({ queryKey: ["bands"], queryFn: listBands }); - const { data: me } = useQuery({ - queryKey: ["me"], - queryFn: () => api.get("/auth/me"), - }); - - // Derive active band from the current URL - const bandMatch = - matchPath("/bands/:bandId/*", location.pathname) ?? - matchPath("/bands/:bandId", location.pathname); - const activeBandId = bandMatch?.params?.bandId ?? null; - const activeBand = bands?.find((b) => b.id === activeBandId) ?? null; - - // Nav active states - const isLibrary = !!( - matchPath({ path: "/bands/:bandId", end: true }, location.pathname) || - matchPath("/bands/:bandId/sessions/:sessionId", location.pathname) || - matchPath("/bands/:bandId/sessions/:sessionId/*", location.pathname) - ); - const isPlayer = !!matchPath("/bands/:bandId/songs/:songId", location.pathname); - const isSettings = location.pathname.startsWith("/settings"); - const isBandSettings = !!matchPath("/bands/:bandId/settings/*", location.pathname); - const bandSettingsPanel = matchPath("/bands/:bandId/settings/:panel", location.pathname)?.params?.panel ?? null; - - // Close dropdown on outside click - useEffect(() => { - if (!dropdownOpen) return; - function handleClick(e: MouseEvent) { - if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { - setDropdownOpen(false); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [dropdownOpen]); - - const border = "rgba(255,255,255,0.06)"; - - return ( -
- {/* ── Sidebar ─────────────────────────────────────────── */} - - - {/* ── Main content ─────────────────────────────────────── */} -
- {children} -
-
- ); -} - -function SectionLabel({ - children, - style, -}: { - children: React.ReactNode; - style?: React.CSSProperties; -}) { - return ( -
- {children} -
- ); + return {children}; } diff --git a/web/src/components/BottomNavBar.tsx b/web/src/components/BottomNavBar.tsx new file mode 100644 index 0000000..2b877ce --- /dev/null +++ b/web/src/components/BottomNavBar.tsx @@ -0,0 +1,130 @@ +import { useNavigate, useLocation, matchPath } from "react-router-dom"; + +// ── Icons (inline SVG) ────────────────────────────────────────────────────── +function IconLibrary() { + return ( + + + + ); +} + +function IconPlay() { + return ( + + + + ); +} + +function IconSettings() { + return ( + + + + + ); +} + +function IconMembers() { + return ( + + + + + + + ); +} + +// ── NavItem ───────────────────────────────────────────────────────────────── +interface NavItemProps { + icon: React.ReactNode; + label: string; + active: boolean; + onClick: () => void; + disabled?: boolean; +} + +function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) { + const color = active ? "#e8a22a" : "rgba(255,255,255,0.5)"; + + return ( + + ); +} + +// ── BottomNavBar ──────────────────────────────────────────────────────────── +export function BottomNavBar() { + const navigate = useNavigate(); + const location = useLocation(); + + + // Derive active states + const isLibrary = !!matchPath("/bands/:bandId", location.pathname); + const isPlayer = !!matchPath("/bands/:bandId/songs/:songId", location.pathname); + const isSettings = location.pathname.startsWith("/settings"); + + return ( + + ); +} diff --git a/web/src/components/ResponsiveLayout.tsx b/web/src/components/ResponsiveLayout.tsx new file mode 100644 index 0000000..2079f41 --- /dev/null +++ b/web/src/components/ResponsiveLayout.tsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from "react"; +import { BottomNavBar } from "./BottomNavBar"; +import { Sidebar } from "./Sidebar"; + +export function ResponsiveLayout({ children }: { children: React.ReactNode }) { + const [isMobile, setIsMobile] = useState(false); + + // Check screen size on mount and resize + useEffect(() => { + const checkScreenSize = () => { + setIsMobile(window.innerWidth < 768); + }; + + // Initial check + checkScreenSize(); + + // Add event listener + window.addEventListener("resize", checkScreenSize); + + // Cleanup + return () => window.removeEventListener("resize", checkScreenSize); + }, []); + + return isMobile ? ( + <> +
+ {children} +
+ + + ) : ( + {children} + ); +} diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx new file mode 100644 index 0000000..43645e7 --- /dev/null +++ b/web/src/components/Sidebar.tsx @@ -0,0 +1,625 @@ +import { useRef, useState, useEffect } from "react"; +import { useNavigate, useLocation, matchPath } from "react-router-dom"; +import { useQuery } from "@tanstack/react-query"; +import { listBands } from "../api/bands"; +import { api } from "../api/client"; +import { logout } from "../api/auth"; +import type { MemberRead } from "../api/auth"; + +// ── Helpers ──────────────────────────────────────────────────────────────── +function getInitials(name: string): string { + return name + .split(/\s+/) + .map((w) => w[0]) + .join("") + .toUpperCase() + .slice(0, 2); +} + +// ── Icons (inline SVG) ────────────────────────────────────────────────────── +function IconWaveform() { + return ( + + + + + + ); +} + +function IconLibrary() { + return ( + + + + ); +} + +function IconPlay() { + return ( + + + + ); +} + +function IconSettings() { + return ( + + + + + ); +} + +function IconMembers() { + return ( + + + + + + + ); +} + +function IconStorage() { + return ( + + + + + + + ); +} + +function IconChevron() { + return ( + + + + ); +} + +function IconSignOut() { + return ( + + + + + ); +} + +// ── NavItem ───────────────────────────────────────────────────────────────── +interface NavItemProps { + icon: React.ReactNode; + label: string; + active: boolean; + onClick: () => void; + disabled?: boolean; +} + +function NavItem({ icon, label, active, onClick, disabled }: NavItemProps) { + const [hovered, setHovered] = useState(false); + + const color = active + ? "#e8a22a" + : disabled + ? "rgba(255,255,255,0.18)" + : hovered + ? "rgba(255,255,255,0.7)" + : "rgba(255,255,255,0.35)"; + + const bg = active + ? "rgba(232,162,42,0.12)" + : hovered && !disabled + ? "rgba(255,255,255,0.045)" + : "transparent"; + + return ( + + ); +} + +// ── Sidebar ──────────────────────────────────────────────────────────────── +export function Sidebar({ children }: { children: React.ReactNode }) { + const navigate = useNavigate(); + const location = useLocation(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + const { data: bands } = useQuery({ queryKey: ["bands"], queryFn: listBands }); + const { data: me } = useQuery({ + queryKey: ["me"], + queryFn: () => api.get("/auth/me"), + }); + + // Derive active band from the current URL + const bandMatch = + matchPath("/bands/:bandId/*", location.pathname) ?? + matchPath("/bands/:bandId", location.pathname); + const activeBandId = bandMatch?.params?.bandId ?? null; + const activeBand = bands?.find((b) => b.id === activeBandId) ?? null; + + // Nav active states + const isLibrary = !!( + matchPath({ path: "/bands/:bandId", end: true }, location.pathname) || + matchPath("/bands/:bandId/sessions/:sessionId", location.pathname) || + matchPath("/bands/:bandId/sessions/:sessionId/*", location.pathname) + ); + const isPlayer = !!matchPath("/bands/:bandId/songs/:songId", location.pathname); + const isSettings = location.pathname.startsWith("/settings"); + const isBandSettings = !!matchPath("/bands/:bandId/settings/*", location.pathname); + const bandSettingsPanel = matchPath("/bands/:bandId/settings/:panel", location.pathname)?.params?.panel ?? null; + + // Close dropdown on outside click + useEffect(() => { + if (!dropdownOpen) return; + function handleClick(e: MouseEvent) { + if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { + setDropdownOpen(false); + } + } + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [dropdownOpen]); + + const border = "rgba(255,255,255,0.06)"; + + return ( +
+ {/* ── Sidebar ──────────────────────────────────────────────────── */} + + + {/* ── Main content ──────────────────────────────────────────────── */} +
+ {children} +
+
+ ); +} + +function SectionLabel({ + children, + style, +}: { + children: React.ReactNode; + style?: React.CSSProperties; +}) { + return ( +
+ {children} +
+ ); +} diff --git a/web/src/index.css b/web/src/index.css index fc2a6cc..d6d0850 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -34,3 +34,39 @@ input, textarea, button, select { --danger: #e07070; --danger-bg: rgba(220,80,80,0.1); } + +/* ── Responsive Layout ──────────────────────────────────────────────────── */ +@media (max-width: 768px) { + /* Ensure main content doesn't overlap bottom nav */ + body { + padding-bottom: 60px; /* Height of bottom nav */ + } +} + +/* Bottom Navigation Bar */ +nav[style*="position: fixed"] { + display: flex; + background: #0b0b0e; + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding: 8px 16px; +} + +/* Bottom Nav Items */ +button[style*="flex-direction: column"] { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 8px 4px; + background: transparent; + border: none; + cursor: pointer; + color: rgba(255, 255, 255, 0.5); + font-size: 10px; + transition: color 0.12s; +} + +button[style*="flex-direction: column"][style*="color: rgb(232, 162, 42)"] { + color: #e8a22a; +}