Search
Code1,917
A personal, interactive dashboard for organizing your [Val Town](https://www.val.town) vals into collections. Built as a Val Town HTTP val with a React frontend.- Add / remove vals from collections- Click a collection in the sidebar to filter the main list to only the vals in that collection- "Add more vals" section appears when viewing a specific collection (lists vals not yet in it)- Optimistic UI updates for instant feedback (no full page reloads on add/remove)- Fully server-rendered initial HTML shell + client-side React for interactivity- Works with your real private data (no demo mode)### Creating a Collection- Use the form in the sidebar (under "COLLECTIONS").- Type a name and submit. The new collection appears immediately (optimistic) and is auto-selected.### Adding / Removing Vals- Click a collection in the sidebar → the grid filters to only its current members (with "Remove" buttons).- Below the filtered list you'll see an "Add more vals" section with the rest of your vals (each with a "+ Add" button).### Tips- The sidebar always shows live counts.- Cards link to the actual val on Val Town.- `index.ts` – Hono HTTP handler. Serves the HTML shell, the React client at `/client.js`, and all `/api/*` endpoints. Also owns SDK + SQLite logic.- `Dashboard.tsx` – Tiny HTML shell (CDNs + root div + script tag).- `client.ts` – Exports the full client React app as a string (served as `/client.js`). This is where the interactive UI lives (sidebar filtering, forms, optimistic updates, card rendering, etc.).- Collection rows are filtered by OAuth user ID, so stale or copied rows from another user are not shown.- Feel free to customize the React UI in `client.ts`, add more API routes, improve the server rendering in the handler, etc.- **Backend**: Val Town + Hono + `std/oauth`. Vals loaded via the official SDK (`me.vals.list`) with the logged-in user's OAuth access token. Collections + membership live in `std/sqlite`, scoped by OAuth user ID.- **Frontend**: React 18 (loaded via UMD CDN for zero-build). Talks to the same val's `/api/*` routes. State is managed client-side with optimistic updates for add/remove/create.- **Why this works per-user**: OAuth provides the current user's access token and user ID. The SDK call uses that token, and SQLite queries filter by that user ID.- **Sections** (presentational grouping)- **Hybrid** (sidebar collections + quick filters with filtering)These were the original static HTML mocks before the live React version.## StatusThis is an exploration / study project (originally based on a saved copy of the Val Town dashboard UI). It is **working** (you can create collections and move vals around with a reasonable React UX) but "not perfect yet" – some polish, edge cases, and possible additional features are still open.A simple community message board application built with React Router 7, demonstrating a full-stack approach with server-side rendering and client-side navigation.- **Server-Side Rendering**: All pages are server-rendered when accessed directly- **Client-Side Navigation**: After initial load, navigation happens client-side- **Topic Management**: Create and view discussion topics│ ├── index.html # HTML template│ └── index.tsx # Client entry point├── routes/### Server-Side RenderingThe application uses React Router 7's `createStaticHandler`, `createStaticRouter`, and `StaticRouterProvider` to render pages on the server. When a user directly accesses a URL:2. It runs the appropriate loaders/actions for the matched route3. The page is rendered with the data and sent to the client4. The client hydrates the page with `createBrowserRouter` and `RouterProvider`### Client-Side NavigationAfter the initial page load, navigation happens entirely on the client:1. React Router handles link clicks and form submissions2. Data is fetched asynchronously using loaders and actions- **Hono**: For server-side routing and handling- **React Router 7**: For both server and client-side routing- **SQLite**: For data persistenceThe code is structured to be idiomatic and simple, serving as a reference for building full-stack applications with React Router 7. The project uses a Remix-style file naming convention with co-located components, loaders, and actions in the `/routes` directory.# React Router StarterMinimal React Router starter example for Val TownThe `http.tsx` file is the main server that responds with an HTML stream.The HTML includes a `<script type="module">` tag that loads the `client.tsx` file to hydrate the appwith client-side routing provided by React Router's declarative mode.This val is a client-side-rendered React app that makes requests to @stevekrouse/cors_example_backend. The backend is in a different val because CORS applies to requests on different domains. The backend has examples of the default permissive CORS behavior and disabled CORS. /** @jsxImportSource https://esm.sh/react@19.2.0 */// kit.tsx — Small shared pieces reused across every info page: the top-level// page layout (PageShell/Panel), a reference link row, and a "try the live// API" card (button + output area wired by client.ts via id convention// `${id}-btn` / `${id}-out`). * width at 1120px and centers it — AppShell's content area spans the full * window between the sidebar and the screen edge, which reads as * un-anchored on wide monitors. Vertical rhythm between a page's top-level */export function PageShell({ children }: { children: React.ReactNode }) { return ( * un-padded edge rather than lining up with its siblings (confirmed via * getBoundingClientRect — every page here nests it inside a padded * container, so every Section-framed block sat ~20-30px left of everything */export function Panel({ children, id }: { children: React.ReactNode; id?: string }) { return (/** @jsxImportSource https://esm.sh/react@19.2.0 */// app.tsx — SSR root: one Astryx AppShell, one page per request, no client// router. Theme mode is resolved from a cookie server-side — zero-FOUC, no JS.import { AppShell, neutralTheme, renderToString, Theme } from "./astryx/components.ts";import { AppSideNav } from "./astryx/shell.tsx";import { clientJs } from "./client.ts";import type { AppProps } from "./types.ts";const PAGES: Record<string, (props: AppProps) => React.ReactNode> = { home: HomePage, { /* Single expression child only — React 19's <title> hoisting silently drops all content when given multiple children (e.g. an /* Utility CSS Astryx doesn't ship, or can't reach without a build step: .hidden — client.ts toggles it to show/hide demo tables before their first successful fetch; .field-input — styles the field radius, border, background, and focus-ring tokens instead of looking like unstyled browser chrome; .astryx-side-nav / .astryx-side-nav-item — Astryx's own stable component classes (see `astryx theme targets side-nav`), given real padding/gap here because neither is exposed as a SideNav prop and this repo has no `astryx theme build` step to compile a defineTheme page's reading width (see pages/kit.tsx's PageShell); .theme-* — the sidebar's theme-mode <details>/<summary> dropdown (see shell.tsx's ThemeForm) styled to match Button/Card chrome. */outline-offset:var(--focus-outline-offset)}.astryx-side-nav{padding:8px}.astryx-side-nav-item{margin-block:2px}.page-shell{max-width:1120px;margin-inline:auto} <AppShell sideNav={ <AppSideNav appName={props.appName} </Theme> <script dangerouslySetInnerHTML={{ __html: clientJs }} /> </body>/** @jsxImportSource https://esm.sh/react@19.2.0 */// page-reminders.tsx — Reminders: the flagship live demo. A shared list every// signed-in visitor can add to and check off — the "family app" this whole// Quickstart is sized for. Rows are rendered client-side (client.ts) from// /api/reminders, cloned from the hidden <template> below so every row gets// full Astryx Button/Badge chrome without needing React hydration. { /* Hidden flag client.ts reads to decide whether to render per-row action buttons — SSR knows the signed-in state, the client-side row renderer doesn't (rows arrive from a JSON fetch). */ { /* Real Astryx-rendered row, never displayed — client.ts clones its <tr> for every reminder so dynamically-inserted rows get full Button/Badge component chrome without hydrating React. */ }/** @jsxImportSource https://esm.sh/react@19.2.0 */// shell.tsx — Side navigation, user section, and the theme-mode switcher.// Real <a href> navigation (server-rendered, per-route) — no client router. HStack, SideNav, SideNavHeading, SideNavItem, SideNavSection, Text,// Docs — three small titled sections instead of one collapsible tree. This// page has zero React hydration (see app.tsx), and Astryx's SideNavItem// collapse toggle is driven by internal onClick state — a collapsed group// would render a toggle that does nothing when clicked. SideNavSection is// always-expanded and purely static, so it's the only grouping primitive// ── UserSection ───────────────────────────────────────────────────────────────// Signed out: a real primary Button (not a bare link) — the sidebar's single// most important action when there's no session yet.// a single closed trigger ("Theme: Auto ⌄") that expands upward (the trigger// sits at the bottom of the sidebar, so the panel opens above it, never off// -screen). <details> is fully keyboard/screen-reader operable with zero// client JS — no React hydration exists on this page (see app.tsx), so a// component that depends on onClick-driven open state (Astryx's own// ── AppSideNav ────────────────────────────────────────────────────────────────export function AppSideNav( { appName, page, mode, user }: { return ( <SideNav header={<SideNavHeading heading={appName} headingHref="/" />} footer={ {NAV.map((item) => ( <SideNavItem key={item.page} {DOCS_SECTIONS.map((section) => ( <SideNavSection key={section.title} title={section.title}> {section.items.map((item) => ( <SideNavItem key={item.page} ))} </SideNavSection> ))} </SideNav> ); index.html — HTML shell (loads twind + index.tsx) index.tsx — React entrypoint, renders <App/> favicon.svg lib/auth-client.ts — Better Auth client-side SDK components/ a sign-in form if not logged in, then the panel.- **Client-side routing** — `App.tsx` has a simple `pushState`/`popstate` router. No React Router dependency. The Hono catch-all serves `index.html` for all non-API GET routes so deep links work.- The `signIn.magicLink` client call needs `callbackURL` set to `/me` (not `/`) so users land on their registration page after clicking the magic link.import { createAuthClient } from "https://esm.sh/better-auth@1.6.9/react?deps=react@18.2.0,react-dom@18.2.0";import { magicLinkClient } from "https://esm.sh/better-auth@1.6.9/client/plugins?deps=react@18.2.0,react-dom@18.2.0";// Base URL is the same origin as the page, so we don't need to set it.// Plugins must mirror the server-side plugins enabled in backend/auth.ts.export const authClient = createAuthClient({ plugins: [magicLinkClient()],});export const { useSession, signIn, signOut } = authClient;- React 18 (client-side)- Hono (backend API) index.html ← HTML shell index.tsx ← React entrypoint favicon.svg ← Uznik Group iconA personal, interactive dashboard for organizing your [Val Town](https://www.val.town) vals into collections. Built as a Val Town HTTP val with a React frontend.- Add / remove vals from collections- Click a collection in the sidebar to filter the main list to only the vals in that collection- "Add more vals" section appears when viewing a specific collection (lists vals not yet in it)- Optimistic UI updates for instant feedback (no full page reloads on add/remove)- Fully server-rendered initial HTML shell + client-side React for interactivity- Works with your real private data (no demo mode)### Creating a Collection- Use the form in the sidebar (under "COLLECTIONS").- Type a name and submit. The new collection appears immediately (optimistic) and is auto-selected.### Adding / Removing Vals- Click a collection in the sidebar → the grid filters to only its current members (with "Remove" buttons).- Below the filtered list you'll see an "Add more vals" section with the rest of your vals (each with a "+ Add" button).### Tips- The sidebar always shows live counts.- Cards link to the actual val on Val Town.The val follows the standard Val Town `react-hono` layout — real `.tsx` files transpiled to the browser on the fly (no build step):- `frontend/index.html` – Static HTML shell. Loads Twind + `std/catch`, then `<script type="module" src="/frontend/index.tsx">`.- `frontend/index.tsx` – Client entry point. Mounts `<App />` into `#root`.- `frontend/components/` – React components: `App.tsx` (state + data + optimistic mutations), `ValCard.tsx`, `Sidebar.tsx`.- `shared/types.ts` – Types (`ValSummary`, `Collection`) imported by both backend and frontend.React is loaded from esm.sh pinned to `18.2.0`, and JSX uses the `@jsxImportSource` pragma — so the browser loads the components directly, transpiled by the platform.- Collection rows are filtered by OAuth user ID, so stale or copied rows from another user are not shown.- Feel free to customize the React UI in `frontend/components/`, add more API routes, etc.- **Backend**: Val Town + Hono + `std/oauth`. Vals loaded via the official SDK (`me.vals.list`) with the logged-in user's OAuth access token. Collections + membership live in `std/sqlite`, scoped by OAuth user ID.- **Frontend**: React 18 (pinned via esm.sh, Twind for styling, zero build step). Real `.tsx` components served transpiled by `std/utils`. Talks to the same val's `/api/*` routes. State is managed client-side with optimistic updates for add/remove/create.- **Why this works per-user**: OAuth provides the current user's access token and user ID. The SDK call uses that token, and SQLite queries filter by that user ID.- **Sections** (presentational grouping)- **Hybrid** (sidebar collections + quick filters with filtering)These were the original static HTML mocks before the live React version.## StatusThis is an exploration / study project (originally based on a saved copy of the Val Town dashboard UI). It is **working** (you can create collections and move vals around with a reasonable React UX) but "not perfect yet" – some polish, edge cases, and possible additional features are still open.Val Town can't accept **incoming** WebSocket connections, and the Automerge syncprotocol runs over WebSocket. The trick: Val Town only serves a static client, andthe **browser** opens the WebSocket directly to your sync server. This sidesteps theplatform's WebSocket limitation entirely.- `server.ts` — HTTP handler. Serves `index.html` and transpiles `client.tsx` → `/client.js`.- `index.html` — page shell (Twind for styling, mounts the React app).- `client.tsx` — the whole app: WASM init, repo connection, JSON editor.3. Either paste an existing document URL (`automerge:...`) and click **Open document**, or click **New document** to create one. Share the generated URL with another client to collaborate.4. Edit the JSON and click **Save changes** — edits merge into the document and sync to the server. Changes from other clients appear live.| --- | --- || Interactive web app (dashboard, SaaS, CRUD, form, anything with client-side state) | `templates/react-hono-starter` || Static page (landing page, link-in-bio, simple docs) | `templates/basic-html-starter` |/** @jsxImportSource https://esm.sh/react@18.2.0 *//** Server-side rendering utility. * * Renders a React component to HTML string with styled-components CSS extraction. * Used by route handlers in main.ts / mcp/auth.ts to produce complete HTML pages. */import { renderToString } from "https://esm.sh/react-dom@18.2.0/server";import { ServerStyleSheet } from "https://esm.sh/styled-components@6?deps=react@18.2.0,react-dom@18.2.0";import type { ReactElement } from "https://esm.sh/react@18.2.0";import { shell, type ShellOptions } from "./shell.ts";export interface RenderPageOptions { /** The React element to render */ element: ReactElement; /** Path to the client hydration script (e.g. "/frontend/pages/recover/client.tsx") */ entry: string; /** Props to serialize for client hydration */ props?: Record<string, unknown>;/** Render a React page to a full HTML Response with SSR + styled-components CSS. */export function renderPage(opts: RenderPageOptions): Response {