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:appThis 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
VITE_API_BASE_URL=http://localhost:8000
VITE_APP_LAYOUT=navbar
VITE_DEFAULT_CURRENCY=USDLeaving 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
| Screen | Built 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 date | canFilter: true in the schemas |
| Create forms with grouped rows, an amount control and an uploader | group, fieldHint in the schemas |
| A rental form whose customer and equipment fields are searchable pickers | ref / refLabel / refFilters |
| An edit form showing only the status | updateSchema: …pick({ status: true }) |
| No delete button on rentals | disable: { del: true } |
| Each role seeing only its own modules | the 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:
import type { TCardsOverride } from "@/core/types"
import RentalsBoard from "@/pages/rentals/RentalsBoard"
export const cards: TCardsOverride = { rentals: RentalsBoard }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:
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
viewsoverride - detail components fetch their own data - Enable
VITE_ENABLE_WALLETif deposits should run through thunder-core's wallet - Ship it to staff phones -
android/andios/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.