Dashboard

Generate the Thunder UI dashboard from everything you just built, then replace one screen with your own.

Everything is in place. One command turns it into an admin app.

Generate the app

deno task generate:app

This clones Thunder UI into public/app/, generates an SDK from your routes and schemas into public/www/sdk@<version>/, links the two, installs and builds.

Configure it

public/app/.env
VITE_API_BASE_URL=http://localhost:8000
VITE_APP_LAYOUT=navbar
VITE_DEFAULT_CURRENCY=USD

Leaving VITE_OAUTH_CLIENT_ID unset selects thunder-core's cookie-session strategy, which is what you want here. The wallet, users and notification features stay off unless you enable them. See Environment Variables.

Sign in

Open the app, pick a tenant, and you land on the overview. The Rental Desk group is in the sidebar with your three modules under it.

What You Got Without Writing Frontend Code

ScreenBuilt from
Sidebar group "Rental Desk" with three modules.group("Rental Desk") on the three routers
A list page per module, with filters on status, email and start datecanFilter: true in the schemas
Create forms with grouped rows, an amount control and an uploadergroup, fieldHint in the schemas
A rental form whose customer and equipment fields are searchable pickersref / refLabel / refFilters
An edit form showing only the statusupdateSchema: …pick({ status: true })
No delete button on rentalsdisable: { del: true }
Each role seeing only its own modulesthe policies from Permissions

Changed a schema since generating? The UI reads the SDK, not your backend. Run deno task generate:sdk -v <version> --overwrite and rebuild - see Two Kinds of Update. "I added a field and it isn't showing" is almost always this.

Replacing One Screen

The generated table is right for equipment and customers. For rentals, a board of cards reads better. Register a card view:

public/app/src/overrides/crud/cards.tsx
import type { TCardsOverride } from "@/core/types"
import RentalsBoard from "@/pages/rentals/RentalsBoard"

export const cards: TCardsOverride = { rentals: RentalsBoard }
public/app/src/pages/rentals/RentalsBoard.tsx
import React from "react"
import z from "zod"
import type { TCardProps } from "@/core/types"
import { zodToMongoProjection } from "@/core/lib/zodToMongoProjection"
import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"

const rentalCard = z.object({
  _id: z.string(),
  status: z.string(),
  totalCents: z.number(),
})

export default function RentalsBoard({ data, isLoading, fetcher }: TCardProps) {
  // REQUIRED: card mode does not fetch on its own.
  React.useEffect(() => {
    fetcher(zodToMongoProjection(rentalCard))
  }, [fetcher])

  if (isLoading) return <p className="p-4">Loading…</p>

  const rentals = rentalCard.array().parse(data)

  return (
    <div className="grid gap-3 p-2 sm:grid-cols-2 xl:grid-cols-3">
      {rentals.map((rental) => (
        <Card key={rental._id}>
          <CardContent className="flex items-center justify-between p-4">
            <span>{(rental.totalCents / 100).toFixed(2)}</span>
            <Badge variant="secondary">{rental.status}</Badge>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}

Registering a card view adds a table/cards toggle to the page header and makes cards the default. The table is still one click away.

Tidying the Navigation

Put rentals first and hide the thunder-core admin modules your staff never need:

public/app/src/overrides/routes.tsx
import type { TRoutesOverride } from "@/core/types"

export const routes: TRoutesOverride = {
  // order within the group
  rentals:   { merge: true, priority: 0 },
  customers: { merge: true, priority: 1 },
  equipment: { merge: true, priority: 2 },

  // remove plugin admin screens from this app entirely
  apiKeys:       { display: false },
  accessControl: { display: false },
}

merge: true keeps the generated route and changes only what you name. Without it, your object replaces the whole route - which is exactly what makes { display: false } remove a module outright. See Route Overrides.

Where to Take It

  • Put a real landing page on overview - it ships as a placeholder meant to be replaced
  • Add a read-only detail page via the views override - detail components fetch their own data
  • Enable VITE_ENABLE_WALLET if deposits should run through thunder-core's wallet
  • Ship it to staff phones - android/ and ios/ are already there, see Mobile

Keep src/core/ untouched. Everything above lives in src/overrides/ and src/pages/, so deno task generate:app --forceSync will bring you security patches without touching a line of your work.


On this page