From b4528920dab831eae6a27a9e6decfe61bba5b05b Mon Sep 17 00:00:00 2001 From: space Date: Sun, 1 Mar 2026 13:21:19 +0100 Subject: [PATCH] refactor: update TABS to include hideInNav property and filter in BottomNav; enhance image popup styling and loading indicator --- .github/copilot-instructions.md | 60 +++++++++++++++++++++++++++++ mobile/App.tsx | 41 ++------------------ mobile/src/components/BottomNav.tsx | 2 +- tv/src/App.tsx | 14 +++++-- tv/src/index.css | 11 +++++- 5 files changed, 84 insertions(+), 44 deletions(-) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..a3dfdee --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,60 @@ +# Copilot Instructions + +## Architecture Overview + +Three-component system for remote-controlling a TV display from a mobile phone: + +- **`mobile/`** — Expo React Native app (the "remote control"). Sends commands to the backend. +- **`tv/`** — Vite + React web app (the TV display). Runs in a browser, polls backend every 5s when fullscreen. +- **`functions/control/`** — Python serverless function deployed on `shsf-api.reversed.dev`. Acts as state store; persists to `/app/state.json` on the container. + +## API / Backend + +All communication goes through a single endpoint: +``` +https://shsf-api.reversed.dev/api/exec/15/1c44d8b5-4065-4a54-b259-748561021329/{route} +``` + +Routes (all `POST` except pull routes which are `GET` implicitly via `fetch`): +- `push_text` — body: `{ title: string }` +- `push_dismiss_text` +- `push_image` — body: `{ image_b64: string, caption: string }`; uploads to MinIO, stores public URL +- `push_dismiss_image` +- `pull_full` — returns full state `{ text, image_popup }` + +Images are stored in MinIO at `content2.reversed.dev`, bucket `tv-control`. + +## Adding a New Content Type + +1. Add state shape to `DEFAULT_STATE` in `functions/control/main.py` +2. Add `push_*` / `pull_*` handlers in `main(args)` in the same file +3. Add a TypeScript interface and `useState` in `tv/src/App.tsx`; render the popup in the fullscreen branch +4. Add a page in `mobile/src/pages/` +5. Register it in the `TABS` array in `mobile/App.tsx` with `hideInNav: true` (prevents it appearing in `BottomNav`, navigation is triggered programmatically via `navigate()`) + +## Mobile App Conventions + +- Uses a **custom in-memory router** (`mobile/src/router.tsx`) — no expo-router or react-navigation. `Route` type is a string union: `"home" | "text" | "image"`. +- `TABS` in `mobile/App.tsx` is the single source of truth for routes and pages. `BottomNav` imports `TABS` directly from `App.tsx`. +- Tabs with `hideInNav: true` are reachable only via `navigate(route)`, not from the bottom bar. +- Styling uses React Native `StyleSheet` throughout — no CSS or Tailwind. + +## TV App Conventions + +- Uses **Tailwind CSS v4** via `@tailwindcss/vite` (no `tailwind.config.js` — config lives in `vite.config.ts`). +- Polling only starts when the browser enters fullscreen (`screenStatus === "fullscreen"`). +- State shape mirrors `DEFAULT_STATE` from the Python function exactly. + +## Dev Workflows + +```bash +# TV display +cd tv && pnpm dev + +# Mobile app +cd mobile && pnpm start # Expo Go / dev server +cd mobile && pnpm android # Android emulator +cd mobile && pnpm ios # iOS simulator +``` + +Package manager: **pnpm** for both `tv/` and `mobile/`. Python function has no local runner — deploy changes directly. diff --git a/mobile/App.tsx b/mobile/App.tsx index 6381c94..500fbfb 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -10,11 +10,12 @@ interface Tab { label: string; route: Route; page: React.ComponentType; + hideInNav?: boolean; } const TABS: Tab[] = [ { label: "Home", route: "home", page: IndexPage }, - { label: "Text", route: "text", page: TextPage }, - { label: "Image", route: "image", page: ImagePage }, + { label: "Text", route: "text", page: TextPage, hideInNav: true }, + { label: "Image", route: "image", page: ImagePage, hideInNav: true }, ]; export { TABS, type Tab }; @@ -52,39 +53,3 @@ const styles = StyleSheet.create({ backgroundColor: "#fff", }, }); - - -function Screen() { - const { route } = useRouter(); - return ( - <> - {TABS.map((tab) => { - if (tab.route === route) { - const Page = tab.page; - return ; - } - return null; - })} - {!TABS.some((tab) => tab.route === route) && } - - ); -} - -export default function App() { - return ( - - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#fff", - }, -}); diff --git a/mobile/src/components/BottomNav.tsx b/mobile/src/components/BottomNav.tsx index 88af390..562e723 100644 --- a/mobile/src/components/BottomNav.tsx +++ b/mobile/src/components/BottomNav.tsx @@ -8,7 +8,7 @@ export function BottomNav() { return ( - {TABS.map((tab) => { + {TABS.filter((tab) => !tab.hideInNav).map((tab) => { const active = tab.route === route; return ( -
+
+
{imagePopup.caption && (

{imagePopup.caption} @@ -133,7 +133,8 @@ function App() { TV display

@@ -142,7 +143,12 @@ function App() { {/* Background idle state */} {!textState.showing && !imagePopup.showing && (
-

Waiting for content...

+

+ Waiting for content + . + . + . +

)}
diff --git a/tv/src/index.css b/tv/src/index.css index 31db226..25f34b8 100644 --- a/tv/src/index.css +++ b/tv/src/index.css @@ -2,4 +2,13 @@ body { @apply bg-gray-900 text-white h-screen w-screen flex items-center justify-center; -} \ No newline at end of file +} + +@keyframes dot-blink { + 0%, 80%, 100% { opacity: 0; } + 40% { opacity: 1; } +} + +.dot-1 { animation: dot-blink 1.4s infinite 0s; } +.dot-2 { animation: dot-blink 1.4s infinite 0.2s; } +.dot-3 { animation: dot-blink 1.4s infinite 0.4s; } \ No newline at end of file