@weft-js/create 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +393 -19
  2. package/bin/create-weft.mjs +62 -14
  3. package/package.json +26 -16
  4. package/scripts/tarballs.mjs +56 -6
  5. package/src/data.mjs +90 -18
  6. package/src/index.mjs +210 -51
  7. package/src/stack/pages.mjs +151 -0
  8. package/src/stack/schema.mjs +195 -0
  9. package/src/stack.mjs +102 -313
  10. package/src/templates.mjs +174 -0
  11. package/src/wizard.mjs +65 -16
  12. package/templates/admin/files/drizzle/.schema-hash +1 -0
  13. package/templates/admin/files/drizzle/0000_admin.sql +136 -0
  14. package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
  15. package/templates/admin/files/drizzle/meta/_journal.json +13 -0
  16. package/templates/admin/files/public/favicon.svg +1 -0
  17. package/templates/admin/files/src/auth.server.ts +24 -0
  18. package/templates/admin/files/src/components/AuthPage.tsx +33 -0
  19. package/templates/admin/files/src/components/Failed.tsx +19 -0
  20. package/templates/admin/files/src/components/Logo.tsx +16 -0
  21. package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
  22. package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
  23. package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
  24. package/templates/admin/files/src/components/Overview.tsx +113 -0
  25. package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
  26. package/templates/admin/files/src/db/schema.ts +68 -0
  27. package/templates/admin/files/src/lib/orders.ts +60 -0
  28. package/templates/admin/files/src/routes/404.tsx +20 -0
  29. package/templates/admin/files/src/routes/_error.tsx +20 -0
  30. package/templates/admin/files/src/routes/_layout.tsx +6 -0
  31. package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
  32. package/templates/admin/files/src/routes/admin/index.tsx +45 -0
  33. package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
  34. package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
  35. package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
  36. package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
  37. package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
  38. package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
  39. package/templates/admin/files/src/routes/index.tsx +13 -0
  40. package/templates/admin/files/src/routes/login.tsx +53 -0
  41. package/templates/admin/files/src/routes/signup.tsx +60 -0
  42. package/templates/admin/files/src/server/orders.ts +193 -0
  43. package/templates/admin/files/src/server/reports.ts +92 -0
  44. package/templates/admin/files/src/server/session.ts +42 -0
  45. package/templates/admin/files/src/theme.css +43 -0
  46. package/templates/admin/files/weft.config.ts +31 -0
  47. package/templates/admin/template.json +22 -0
  48. package/templates/catalog/files/drizzle/.schema-hash +1 -0
  49. package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
  50. package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
  51. package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
  52. package/templates/catalog/files/public/favicon.svg +1 -0
  53. package/templates/catalog/files/seed.sql +13 -0
  54. package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
  55. package/templates/catalog/files/src/components/Aisles.tsx +31 -0
  56. package/templates/catalog/files/src/components/CartLines.tsx +96 -0
  57. package/templates/catalog/files/src/components/CartLink.tsx +29 -0
  58. package/templates/catalog/files/src/components/Failed.tsx +19 -0
  59. package/templates/catalog/files/src/components/Featured.tsx +19 -0
  60. package/templates/catalog/files/src/components/Filters.tsx +40 -0
  61. package/templates/catalog/files/src/components/Logo.tsx +16 -0
  62. package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
  63. package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
  64. package/templates/catalog/files/src/components/ProductList.tsx +65 -0
  65. package/templates/catalog/files/src/components/Related.tsx +19 -0
  66. package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
  67. package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
  68. package/templates/catalog/files/src/db/schema.ts +53 -0
  69. package/templates/catalog/files/src/lib/catalog.ts +35 -0
  70. package/templates/catalog/files/src/routes/404.tsx +18 -0
  71. package/templates/catalog/files/src/routes/_error.tsx +18 -0
  72. package/templates/catalog/files/src/routes/_layout.tsx +14 -0
  73. package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
  74. package/templates/catalog/files/src/routes/cart.tsx +29 -0
  75. package/templates/catalog/files/src/routes/index.tsx +84 -0
  76. package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
  77. package/templates/catalog/files/src/server/cart.ts +125 -0
  78. package/templates/catalog/files/src/server/catalog.ts +107 -0
  79. package/templates/catalog/files/src/theme.css +159 -0
  80. package/templates/catalog/files/weft.config.ts +30 -0
  81. package/templates/catalog/template.json +20 -0
  82. package/templates/landing/files/.weft/plugins.lock.json +42 -0
  83. package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
  84. package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
  85. package/templates/landing/files/assets/hero.webp +0 -0
  86. package/templates/landing/files/drizzle/.schema-hash +1 -0
  87. package/templates/landing/files/drizzle/0000_landing.sql +6 -0
  88. package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
  89. package/templates/landing/files/drizzle/meta/_journal.json +13 -0
  90. package/templates/landing/files/public/favicon.svg +1 -0
  91. package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
  92. package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
  93. package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
  94. package/templates/landing/files/src/db/schema.ts +7 -0
  95. package/templates/landing/files/src/routes/404.tsx +24 -0
  96. package/templates/landing/files/src/routes/_error.tsx +24 -0
  97. package/templates/landing/files/src/routes/_layout.tsx +14 -0
  98. package/templates/landing/files/src/routes/index.tsx +130 -0
  99. package/templates/landing/files/src/routes/joined.tsx +30 -0
  100. package/templates/landing/files/src/routes/pricing.tsx +131 -0
  101. package/templates/landing/files/src/routes/privacy.tsx +56 -0
  102. package/templates/landing/files/src/server/waitlist.ts +13 -0
  103. package/templates/landing/files/src/theme.css +108 -0
  104. package/templates/landing/files/weft.config.ts +44 -0
  105. package/templates/landing/template.json +17 -0
  106. package/templates/todo/files/drizzle/.schema-hash +1 -0
  107. package/templates/todo/files/drizzle/0000_todo.sql +82 -0
  108. package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
  109. package/templates/todo/files/drizzle/meta/_journal.json +13 -0
  110. package/templates/todo/files/public/favicon.svg +1 -0
  111. package/templates/todo/files/src/components/AuthPage.tsx +33 -0
  112. package/templates/todo/files/src/components/Logo.tsx +14 -0
  113. package/templates/todo/files/src/components/NewTodo.tsx +27 -0
  114. package/templates/todo/files/src/components/TodoList.tsx +101 -0
  115. package/templates/todo/files/src/db/schema.ts +18 -0
  116. package/templates/todo/files/src/lib/views.ts +13 -0
  117. package/templates/todo/files/src/routes/404.tsx +20 -0
  118. package/templates/todo/files/src/routes/_error.tsx +20 -0
  119. package/templates/todo/files/src/routes/_layout.tsx +6 -0
  120. package/templates/todo/files/src/routes/index.tsx +13 -0
  121. package/templates/todo/files/src/routes/login.tsx +53 -0
  122. package/templates/todo/files/src/routes/signup.tsx +57 -0
  123. package/templates/todo/files/src/routes/todos.tsx +87 -0
  124. package/templates/todo/files/src/server/todos.ts +98 -0
  125. package/templates/todo/files/src/theme.css +71 -0
  126. package/templates/todo/files/weft.config.ts +30 -0
  127. package/templates/todo/template.json +18 -0
@@ -0,0 +1,20 @@
1
+ import { Link, type Meta } from '@weft-js/start'
2
+ import { Center, Heading, Stack, Text } from '@weft-js/warp'
3
+
4
+ export const meta: Meta = { title: 'Something went wrong' }
5
+
6
+ export default function ServerError() {
7
+ return (
8
+ <main class="system-page">
9
+ <Center max="narrow" gutter>
10
+ <Stack gap="group" align="center">
11
+ <Heading level={1} role="title">
12
+ Something went wrong on our side
13
+ </Heading>
14
+ <Text subtle>Your orders are safe. Give it a minute and try again.</Text>
15
+ <Link href="/admin">Back to the overview</Link>
16
+ </Stack>
17
+ </Center>
18
+ </main>
19
+ )
20
+ }
@@ -0,0 +1,6 @@
1
+ import '../theme.css'
2
+ import type { RouteProps } from '@weft-js/start'
3
+
4
+ export default function Root(props: RouteProps) {
5
+ return <>{props.children}</>
6
+ }
@@ -0,0 +1,89 @@
1
+ import {
2
+ BarChartIcon,
3
+ ClipboardListIcon,
4
+ HomeIcon,
5
+ ListIcon,
6
+ LogOutIcon,
7
+ StoreIcon,
8
+ } from '@weft-js/warp-icons'
9
+ import { redirect, type LoadArgs, type RouteProps } from '@weft-js/start'
10
+ import { Shell, Toaster, toast } from '@weft-js/warp'
11
+ import { Logo } from '#/components/Logo'
12
+ import { signOut } from '#/server/session'
13
+
14
+ const pages = {
15
+ '/admin': { id: 'overview', label: 'Overview' },
16
+ '/admin/orders': { id: 'orders', label: 'Orders' },
17
+ '/admin/nested': { id: 'regions', label: 'Regions' },
18
+ '/admin/per-row': { id: 'buyers', label: 'Order feed' },
19
+ '/admin/slow': { id: 'report', label: 'Year report' },
20
+ }
21
+
22
+ // The guard for every page under /admin; each page's own queries are `auth: 'user'` as well.
23
+ export const load = ({ ctx, request }: LoadArgs) => {
24
+ if (!ctx.user) redirect('/login')
25
+ const path = new URL(request.url).pathname.replace(/\/$/, '')
26
+ const detail = path.match(/^\/admin\/orders\/(.+)$/)
27
+ const page = pages[(detail ? '/admin/orders' : path) as keyof typeof pages] ?? pages['/admin']
28
+ const crumbs = [
29
+ { label: 'Backroom', href: '/admin' },
30
+ ...(detail
31
+ ? [{ label: 'Orders', href: '/admin/orders' }, { label: `#${detail[1]}` }]
32
+ : [{ label: page.label }]),
33
+ ]
34
+ return { name: ctx.user.name, email: ctx.user.email, current: page.id, crumbs }
35
+ }
36
+
37
+ const nav = [
38
+ {
39
+ items: [
40
+ { id: 'overview', label: 'Overview', href: '/admin', icon: HomeIcon },
41
+ { id: 'orders', label: 'Orders', href: '/admin/orders', icon: ClipboardListIcon },
42
+ ],
43
+ },
44
+ {
45
+ label: 'Reports',
46
+ items: [
47
+ { id: 'regions', label: 'Regions', href: '/admin/nested', icon: StoreIcon },
48
+ { id: 'buyers', label: 'Order feed', href: '/admin/per-row', icon: ListIcon },
49
+ { id: 'report', label: 'Year report', href: '/admin/slow', icon: BarChartIcon },
50
+ ],
51
+ },
52
+ ]
53
+
54
+ const leave = () =>
55
+ signOut().catch(() =>
56
+ toast.show({
57
+ tone: 'critical',
58
+ title: "You're still signed in",
59
+ description: "That didn't go through. Try again in a moment.",
60
+ }),
61
+ )
62
+ const initials = (name: string) =>
63
+ name
64
+ .split(/\s+/)
65
+ .filter(Boolean)
66
+ .slice(0, 2)
67
+ .map((word) => word[0])
68
+ .join('')
69
+ .toUpperCase()
70
+
71
+ export default function Admin(props: RouteProps<typeof load>) {
72
+ return (
73
+ <Shell
74
+ brand={{ name: 'Backroom', href: '/admin', mark: <Logo /> }}
75
+ nav={nav}
76
+ current={props.data.current}
77
+ crumbs={props.data.crumbs}
78
+ account={{
79
+ name: props.data.name,
80
+ email: props.data.email,
81
+ initials: initials(props.data.name),
82
+ items: [{ label: 'Sign out', icon: LogOutIcon, onSelect: leave }],
83
+ }}
84
+ overlays={<Toaster />}
85
+ >
86
+ <div class="page">{props.children}</div>
87
+ </Shell>
88
+ )
89
+ }
@@ -0,0 +1,45 @@
1
+ import type { Meta } from '@weft-js/start'
2
+ import { Heading, Stack, Text } from '@weft-js/warp'
3
+ import { Boundary } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import {
6
+ LatestOrders,
7
+ LatestOrdersLoading,
8
+ RevenueChart,
9
+ RevenueChartLoading,
10
+ Stats,
11
+ StatsLoading,
12
+ } from '#/components/Overview'
13
+
14
+ export const meta: Meta = { title: 'Overview' }
15
+
16
+ export default function Overview() {
17
+ return (
18
+ <Stack gap="group">
19
+ <Stack gap="tight">
20
+ <Heading level={1} role="title">
21
+ Overview
22
+ </Heading>
23
+ <Text subtle>How the shop is doing, all time.</Text>
24
+ </Stack>
25
+ <Boundary
26
+ pending={<StatsLoading />}
27
+ error={(_, retry) => <Failed what="The totals" retry={retry} />}
28
+ >
29
+ <Stats />
30
+ </Boundary>
31
+ <Boundary
32
+ pending={<RevenueChartLoading />}
33
+ error={(_, retry) => <Failed what="The chart" retry={retry} />}
34
+ >
35
+ <RevenueChart />
36
+ </Boundary>
37
+ <Boundary
38
+ pending={<LatestOrdersLoading />}
39
+ error={(_, retry) => <Failed what="The latest orders" retry={retry} />}
40
+ >
41
+ <LatestOrders />
42
+ </Boundary>
43
+ </Stack>
44
+ )
45
+ }
@@ -0,0 +1,35 @@
1
+ import type { RouteProps } from '@weft-js/start'
2
+ import { Heading, Skeleton, Stack, Text } from '@weft-js/warp'
3
+ import { Boundary } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import { count } from '#/lib/orders'
6
+ import { reach } from '#/server/reports'
7
+
8
+ function Reach() {
9
+ const store = reach()
10
+ return (
11
+ <Text subtle>
12
+ {count(store.value?.customers ?? 0)} customers across {count(store.value?.cities ?? 0)}{' '}
13
+ cities.
14
+ </Text>
15
+ )
16
+ }
17
+
18
+ export default function Regions(props: RouteProps) {
19
+ return (
20
+ <Stack gap="group">
21
+ <Stack gap="tight">
22
+ <Heading level={1} role="title">
23
+ Regions
24
+ </Heading>
25
+ <Boundary
26
+ pending={<Skeleton width="16rem" aria-label="Loading the reach" />}
27
+ error={(_, retry) => <Failed what="The reach" retry={retry} />}
28
+ >
29
+ <Reach />
30
+ </Boundary>
31
+ </Stack>
32
+ {props.children}
33
+ </Stack>
34
+ )
35
+ }
@@ -0,0 +1,77 @@
1
+ import type { Meta } from '@weft-js/start'
2
+ import { Card, Grid, Heading, Skeleton, Stack, Table, Text, type Column } from '@weft-js/warp'
3
+ import { Boundary, derived } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import { count, money } from '#/lib/orders'
6
+ import { biggestCustomers, topCities } from '#/server/reports'
7
+
8
+ export const meta: Meta = { title: 'Regions' }
9
+
10
+ type City = { city: string; orders: number; revenue: number }
11
+
12
+ const columns: Column<City>[] = [
13
+ { id: 'city', header: 'City', rowHeader: true },
14
+ { id: 'orders', header: 'Orders', numeric: true, value: (row) => count(row.orders) },
15
+ { id: 'revenue', header: 'Revenue', numeric: true, value: (row) => money(row.revenue) },
16
+ ]
17
+
18
+ function Cities() {
19
+ const cities = topCities()
20
+ const rows = derived(() => cities.value ?? [])
21
+ return (
22
+ <Table
23
+ caption="Cities"
24
+ rows={rows}
25
+ columns={columns}
26
+ rowKey="city"
27
+ picker={false}
28
+ empty={{ title: 'No cities yet', text: 'Each city appears with its first order.' }}
29
+ />
30
+ )
31
+ }
32
+
33
+ function BiggestCustomers() {
34
+ const biggest = biggestCustomers()
35
+ const people = derived(() => biggest.value ?? [])
36
+ return (
37
+ <div>
38
+ {people.value.length > 0 ? (
39
+ <Grid min="12rem" gap="related">
40
+ {people.value.map((person) => (
41
+ <Card key={person.id} eyebrow={person.city}>
42
+ <Stack gap="tight">
43
+ <strong>{person.name}</strong>
44
+ <Text subtle>{money(person.revenue)}</Text>
45
+ </Stack>
46
+ </Card>
47
+ ))}
48
+ </Grid>
49
+ ) : (
50
+ <Text subtle>Your best customers will show up here once orders come in.</Text>
51
+ )}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ // Each part reads its own data inside a Boundary, so the page's first byte waits for none of them.
57
+ export default function RegionTable() {
58
+ return (
59
+ <Stack gap="group">
60
+ <Boundary
61
+ pending={<Skeleton lines={6} height="1.75rem" aria-label="Loading the cities" />}
62
+ error={(_, retry) => <Failed what="The cities" retry={retry} />}
63
+ >
64
+ <Cities />
65
+ </Boundary>
66
+ <Stack gap="related">
67
+ <Heading level={2}>Biggest customers</Heading>
68
+ <Boundary
69
+ pending={<Skeleton height="5.25rem" aria-label="Loading the biggest customers" />}
70
+ error={(_, retry) => <Failed what="The biggest customers" retry={retry} />}
71
+ >
72
+ <BiggestCustomers />
73
+ </Boundary>
74
+ </Stack>
75
+ </Stack>
76
+ )
77
+ }
@@ -0,0 +1,24 @@
1
+ import { notFound, type LoadArgs, type Meta, type RouteProps } from '@weft-js/start'
2
+ import { Boundary } from '@weft-js/core'
3
+ import { Failed } from '#/components/Failed'
4
+ import { OrderDetail, OrderLoading } from '#/components/OrderDetail'
5
+ import { getOrder } from '#/server/orders'
6
+
7
+ export const load = async ({ params }: LoadArgs<'/admin/orders/[id]'>) => {
8
+ const id = Number(params.id)
9
+ if (!Number.isInteger(id) || id < 1) return notFound()
10
+ return (await getOrder.fetch({ id })) ? { id } : notFound()
11
+ }
12
+
13
+ export const meta: Meta<typeof load> = ({ data }) => ({ title: `Order #${data.id}` })
14
+
15
+ export default function Order(props: RouteProps<typeof load, '/admin/orders/[id]'>) {
16
+ return (
17
+ <Boundary
18
+ pending={<OrderLoading />}
19
+ error={(_, retry) => <Failed what="This order" retry={retry} />}
20
+ >
21
+ <OrderDetail id={props.data.id} />
22
+ </Boundary>
23
+ )
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { LoadArgs, Meta, RouteProps } from '@weft-js/start'
2
+ import { Heading, Stack } from '@weft-js/warp'
3
+ import { Boundary } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import { OrdersFilters } from '#/components/OrdersFilters'
6
+ import { OrdersLoading, OrdersTable } from '#/components/OrdersTable'
7
+ import { viewOf } from '#/lib/orders'
8
+
9
+ export const load = ({ request }: LoadArgs) => ({ view: viewOf(request.url) })
10
+
11
+ export const meta: Meta = { title: 'Orders' }
12
+
13
+ export default function Orders(props: RouteProps<typeof load>) {
14
+ return (
15
+ <Stack gap="group">
16
+ <Heading level={1} role="title">
17
+ Orders
18
+ </Heading>
19
+ <OrdersFilters view={props.data.view} />
20
+ <Boundary
21
+ pending={<OrdersLoading />}
22
+ error={(_, retry) => <Failed what="The orders" retry={retry} />}
23
+ >
24
+ <OrdersTable view={props.data.view} />
25
+ </Boundary>
26
+ </Stack>
27
+ )
28
+ }
@@ -0,0 +1,81 @@
1
+ import type { Meta } from '@weft-js/start'
2
+ import { Heading, Link, Skeleton, Stack, Table, Text, type Column } from '@weft-js/warp'
3
+ import { Boundary, derived } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import { StatusBadge } from '#/components/StatusBadge'
6
+ import { date, money } from '#/lib/orders'
7
+ import { getCustomer, recentOrderIds } from '#/server/reports'
8
+
9
+ export const meta: Meta = { title: 'Order feed' }
10
+
11
+ type FeedRow = {
12
+ id: number
13
+ customerId: number
14
+ status: string
15
+ totalCents: number
16
+ createdAt: string
17
+ }
18
+
19
+ function CustomerCell({ id }: { id: number }) {
20
+ const customer = getCustomer(() => ({ id }))
21
+ return (
22
+ <>
23
+ {customer.value?.name} <Text subtle>{customer.value?.city}</Text>
24
+ </>
25
+ )
26
+ }
27
+
28
+ const columns: Column<FeedRow>[] = [
29
+ {
30
+ id: 'id',
31
+ header: 'Order',
32
+ rowHeader: true,
33
+ value: (row) => `Order ${row.id}`,
34
+ cell: (row) => <Link href={`/admin/orders/${row.id}`}>#{row.id}</Link>,
35
+ },
36
+ { id: 'customer', header: 'Customer', cell: (row) => <CustomerCell id={row.customerId} /> },
37
+ {
38
+ id: 'status',
39
+ header: 'Status',
40
+ card: 'badge',
41
+ cell: (row) => <StatusBadge status={row.status} />,
42
+ },
43
+ { id: 'total', header: 'Total', numeric: true, value: (row) => money(row.totalCents) },
44
+ { id: 'date', header: 'Date', value: (row) => date(row.createdAt) },
45
+ ]
46
+
47
+ function Feed() {
48
+ const recent = recentOrderIds()
49
+ const rows = derived(() => recent.value ?? [])
50
+ return (
51
+ <Table
52
+ caption="Recent orders"
53
+ captionHidden
54
+ rows={rows}
55
+ columns={columns}
56
+ picker={false}
57
+ empty={{ title: 'Quiet so far', text: 'New orders land here as they come in.' }}
58
+ />
59
+ )
60
+ }
61
+
62
+ // A plain box, not warp's Stack: there the Boundary would be part of an island that waits for a click, and a stream that stops
63
+ // early would leave the skeleton up. On its own the island runs as the page shows, and fills in from what the page carries.
64
+ export default function OrderFeed() {
65
+ return (
66
+ <div class="flow">
67
+ <Stack gap="tight">
68
+ <Heading level={1} role="title">
69
+ Order feed
70
+ </Heading>
71
+ <Text subtle>The last 50 orders, each with who placed it.</Text>
72
+ </Stack>
73
+ <Boundary
74
+ pending={<Skeleton lines={12} height="1.75rem" aria-label="Loading the feed" />}
75
+ error={(_, retry) => <Failed what="The feed" retry={retry} />}
76
+ >
77
+ <Feed />
78
+ </Boundary>
79
+ </div>
80
+ )
81
+ }
@@ -0,0 +1,74 @@
1
+ import type { Meta } from '@weft-js/start'
2
+ import { Button, Heading, Skeleton, Stack, Table, Text, type Column } from '@weft-js/warp'
3
+ import { Boundary, derived, state } from '@weft-js/core'
4
+ import { Failed } from '#/components/Failed'
5
+ import { StatusBadge } from '#/components/StatusBadge'
6
+ import { count, money } from '#/lib/orders'
7
+ import { yearReport } from '#/server/reports'
8
+
9
+ export const meta: Meta = { title: 'Year report' }
10
+
11
+ type Line = { status: string; orders: number; revenue: number }
12
+
13
+ const columns: Column<Line>[] = [
14
+ {
15
+ id: 'status',
16
+ header: 'Status',
17
+ rowHeader: true,
18
+ cell: (row) => <StatusBadge status={row.status} />,
19
+ },
20
+ { id: 'orders', header: 'Orders', numeric: true, value: (row) => count(row.orders) },
21
+ { id: 'revenue', header: 'Revenue', numeric: true, value: (row) => money(row.revenue) },
22
+ ]
23
+
24
+ function Report() {
25
+ const report = yearReport()
26
+ const lines = derived(() => report.value ?? [])
27
+ return (
28
+ <Table
29
+ caption="The year by status"
30
+ captionHidden
31
+ rows={lines}
32
+ columns={columns}
33
+ rowKey="status"
34
+ picker={false}
35
+ empty={{ title: 'Nothing to report', text: 'The report fills in once orders come in.' }}
36
+ />
37
+ )
38
+ }
39
+
40
+ function Tally() {
41
+ const counted = state(0)
42
+ return (
43
+ <div class="section-head">
44
+ <span role="status">Count: {counted.value}</span>
45
+ <Button emphasis="outline" onClick={() => counted.value++}>
46
+ Count
47
+ </Button>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ export default function YearReport() {
53
+ return (
54
+ <Stack gap="group">
55
+ <Stack gap="tight">
56
+ <Heading level={1} role="title">
57
+ Year report
58
+ </Heading>
59
+ <Text subtle>
60
+ The books take a moment to add up. Keep working: the page is yours while they do.
61
+ </Text>
62
+ </Stack>
63
+ <Tally />
64
+ <section aria-label="Report">
65
+ <Boundary
66
+ pending={<Skeleton lines={5} height="1.75rem" aria-label="Adding up the year" />}
67
+ error={(_, retry) => <Failed what="The report" retry={retry} />}
68
+ >
69
+ <Report />
70
+ </Boundary>
71
+ </section>
72
+ </Stack>
73
+ )
74
+ }
@@ -0,0 +1,13 @@
1
+ import { redirect, type Meta } from '@weft-js/start'
2
+
3
+ // Only ever seen as a redirect, but a link to the bare domain still gets a title and a card.
4
+ export const meta: Meta = {
5
+ title: 'Backroom',
6
+ description: 'Backroom runs the shop: orders, customers and revenue in one place.',
7
+ }
8
+
9
+ export const load = () => redirect('/admin')
10
+
11
+ export default function Home() {
12
+ return null
13
+ }
@@ -0,0 +1,53 @@
1
+ import { Form, Link, redirect, type FormApi, type LoadArgs, type Meta } from '@weft-js/start'
2
+ import { Alert, Button, Input, Stack, Text } from '@weft-js/warp'
3
+ import { AuthPage } from '#/components/AuthPage'
4
+ import { signIn } from '#/server/session'
5
+
6
+ export const load = ({ ctx }: LoadArgs) => {
7
+ if (ctx.user) redirect('/admin')
8
+ }
9
+
10
+ export const meta: Meta = {
11
+ title: 'Sign in',
12
+ description: 'Backroom is where you run the shop: orders, customers and revenue in one place.',
13
+ }
14
+
15
+ export default function Login() {
16
+ return (
17
+ <AuthPage title="Back to work" lede="Sign in to see what came in overnight.">
18
+ <Form of={signIn}>
19
+ {(form: FormApi) => (
20
+ <Stack gap="related">
21
+ {form.message.value && (
22
+ <Alert tone="critical" title="We couldn't sign you in" live>
23
+ {form.message.value}
24
+ </Alert>
25
+ )}
26
+ <Input
27
+ label="Email"
28
+ type="email"
29
+ autocomplete="username"
30
+ required
31
+ {...form.field('email', 'email')}
32
+ />
33
+ <form.Error name="email" class="form-error" />
34
+ <Input
35
+ label="Password"
36
+ type="password"
37
+ name="password"
38
+ autocomplete="current-password"
39
+ required
40
+ />
41
+ <form.Error name="password" class="form-error" />
42
+ <Button type="submit" full loading={form.pending.value}>
43
+ Sign in
44
+ </Button>
45
+ </Stack>
46
+ )}
47
+ </Form>
48
+ <Text subtle>
49
+ Setting up a new store? <Link href="/signup">Create the owner account</Link>
50
+ </Text>
51
+ </AuthPage>
52
+ )
53
+ }
@@ -0,0 +1,60 @@
1
+ import { Form, Link, redirect, type FormApi, type LoadArgs, type Meta } from '@weft-js/start'
2
+ import { Alert, Button, Input, Stack, Text } from '@weft-js/warp'
3
+ import { AuthPage } from '#/components/AuthPage'
4
+ import { signUp } from '#/server/session'
5
+
6
+ export const load = ({ ctx }: LoadArgs) => {
7
+ if (ctx.user) redirect('/admin')
8
+ }
9
+
10
+ export const meta: Meta = {
11
+ title: 'Create the owner account',
12
+ description: 'The first account owns the store. Sign-up closes after it.',
13
+ }
14
+
15
+ export default function Signup() {
16
+ return (
17
+ <AuthPage
18
+ title="Open up the backroom"
19
+ lede="The first account owns the store. Sign-up closes after it."
20
+ >
21
+ <Form of={signUp}>
22
+ {(form: FormApi) => (
23
+ <Stack gap="related">
24
+ {form.message.value && (
25
+ <Alert tone="critical" title="We couldn't create your account" live>
26
+ {form.message.value}
27
+ </Alert>
28
+ )}
29
+ <Input label="Name" autocomplete="name" required {...form.field('name', 'text')} />
30
+ <form.Error name="name" class="form-error" />
31
+ <Input
32
+ label="Email"
33
+ type="email"
34
+ autocomplete="username"
35
+ required
36
+ {...form.field('email', 'email')}
37
+ />
38
+ <form.Error name="email" class="form-error" />
39
+ <Input
40
+ label="Password"
41
+ type="password"
42
+ name="password"
43
+ autocomplete="new-password"
44
+ required
45
+ minlength={8}
46
+ hint="At least 8 characters."
47
+ />
48
+ <form.Error name="password" class="form-error" />
49
+ <Button type="submit" full loading={form.pending.value}>
50
+ Create account
51
+ </Button>
52
+ </Stack>
53
+ )}
54
+ </Form>
55
+ <Text subtle>
56
+ Already have an account? <Link href="/login">Sign in</Link>
57
+ </Text>
58
+ </AuthPage>
59
+ )
60
+ }