@weft-js/create 0.1.0 → 0.1.2

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 +60 -10
  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,193 @@
1
+ import { mutation, query } from '@weft-js/start'
2
+ import { and, asc, count, desc, eq, ilike, inArray, sql } from 'drizzle-orm'
3
+ import { z } from 'zod'
4
+ import { customers, orderItems, orders, products } from '#/db/schema'
5
+ import { PAGE_SIZE, sortKeys, statuses } from '#/lib/orders'
6
+
7
+ const status = z.enum(statuses)
8
+ const sortColumns = {
9
+ id: orders.id,
10
+ customer: customers.name,
11
+ status: orders.status,
12
+ total: orders.totalCents,
13
+ created: orders.createdAt,
14
+ }
15
+ const like = (text: string) => `%${text.replace(/[\\%_]/g, '\\$&')}%`
16
+
17
+ const listed = {
18
+ id: orders.id,
19
+ customer: customers.name,
20
+ status: orders.status,
21
+ totalCents: orders.totalCents,
22
+ createdAt: orders.createdAt,
23
+ }
24
+ const orderView = z.object({
25
+ page: z.number().int().min(1),
26
+ sort: z.enum(sortKeys),
27
+ dir: z.enum(['asc', 'desc']),
28
+ status: status.or(z.literal('')),
29
+ q: z.string().max(100),
30
+ })
31
+
32
+ const onTheWire = <T extends { createdAt: Date }>(row: T) => ({
33
+ ...row,
34
+ createdAt: row.createdAt.toISOString(),
35
+ })
36
+
37
+ export type OrderRow = ReturnType<
38
+ typeof onTheWire<{
39
+ id: number
40
+ customer: string
41
+ status: string
42
+ totalCents: number
43
+ createdAt: Date
44
+ }>
45
+ >
46
+
47
+ // Aggregates over every order: worth keeping for a minute, and a status change clears them through the tag.
48
+ export const storeStats = query({
49
+ tags: ['orders', 'customers'],
50
+ auth: 'user',
51
+ cache: { life: 'minutes', scope: 'user' },
52
+ run: async ({ ctx }) => {
53
+ const [[sales], customerCount] = await Promise.all([
54
+ ctx.db
55
+ .select({
56
+ revenue:
57
+ sql<number>`coalesce(sum(${orders.totalCents}) filter (where ${orders.status} <> 'refunded'), 0)`.mapWith(
58
+ Number,
59
+ ),
60
+ refunded:
61
+ sql<number>`coalesce(sum(${orders.totalCents}) filter (where ${orders.status} = 'refunded'), 0)`.mapWith(
62
+ Number,
63
+ ),
64
+ orders: count(),
65
+ })
66
+ .from(orders),
67
+ ctx.db.$count(customers),
68
+ ])
69
+ return { ...sales!, customers: customerCount }
70
+ },
71
+ })
72
+
73
+ export const revenueByMonth = query({
74
+ tags: ['orders'],
75
+ auth: 'user',
76
+ cache: { life: 'minutes', scope: 'user' },
77
+ run: async ({ ctx }) => {
78
+ const month = sql`date_trunc('month', ${orders.createdAt} at time zone 'UTC')`
79
+ const rows = await ctx.db
80
+ .select({
81
+ month: sql<string>`to_char(${month}, 'Mon YYYY')`,
82
+ cents: sql<number>`sum(${orders.totalCents})`.mapWith(Number),
83
+ })
84
+ .from(orders)
85
+ .where(sql`${orders.status} <> 'refunded'`)
86
+ .groupBy(month)
87
+ .orderBy(desc(month))
88
+ .limit(12)
89
+ return rows.reverse().map((row) => ({ month: row.month, revenue: row.cents / 100 }))
90
+ },
91
+ })
92
+
93
+ export const latestOrders = query({
94
+ tags: ['orders', 'customers'],
95
+ auth: 'user',
96
+ run: async ({ ctx }) => {
97
+ const rows = await ctx.db
98
+ .select(listed)
99
+ .from(orders)
100
+ .innerJoin(customers, eq(customers.id, orders.customerId))
101
+ .orderBy(desc(orders.createdAt), desc(orders.id))
102
+ .limit(10)
103
+ return rows.map(onTheWire)
104
+ },
105
+ })
106
+
107
+ export const listOrders = query({
108
+ input: orderView,
109
+ tags: ['orders', 'customers'],
110
+ auth: 'user',
111
+ run: async ({ input, ctx }) => {
112
+ const where = and(
113
+ input.status ? eq(orders.status, input.status) : undefined,
114
+ input.q ? ilike(customers.name, like(input.q)) : undefined,
115
+ )
116
+ const direction = input.dir === 'asc' ? asc : desc
117
+ const [rows, [found]] = await Promise.all([
118
+ ctx.db
119
+ .select(listed)
120
+ .from(orders)
121
+ .innerJoin(customers, eq(customers.id, orders.customerId))
122
+ .where(where)
123
+ .orderBy(direction(sortColumns[input.sort]), direction(orders.id))
124
+ .limit(PAGE_SIZE)
125
+ .offset((input.page - 1) * PAGE_SIZE),
126
+ input.q
127
+ ? ctx.db
128
+ .select({ total: count() })
129
+ .from(orders)
130
+ .innerJoin(customers, eq(customers.id, orders.customerId))
131
+ .where(where)
132
+ : ctx.db.select({ total: count() }).from(orders).where(where),
133
+ ])
134
+ return { rows: rows.map(onTheWire), total: found!.total }
135
+ },
136
+ })
137
+
138
+ export const getOrder = query({
139
+ input: z.object({ id: z.number().int().positive() }),
140
+ tags: ['orders', 'customers', 'order_items', 'products'],
141
+ auth: 'user',
142
+ run: async ({ input, ctx }) => {
143
+ const [[order], items] = await Promise.all([
144
+ ctx.db
145
+ .select({ ...listed, email: customers.email, city: customers.city })
146
+ .from(orders)
147
+ .innerJoin(customers, eq(customers.id, orders.customerId))
148
+ .where(eq(orders.id, input.id)),
149
+ ctx.db
150
+ .select({
151
+ id: orderItems.id,
152
+ product: products.name,
153
+ quantity: orderItems.quantity,
154
+ priceCents: orderItems.priceCents,
155
+ })
156
+ .from(orderItems)
157
+ .innerJoin(products, eq(products.id, orderItems.productId))
158
+ .where(eq(orderItems.orderId, input.id))
159
+ .orderBy(asc(orderItems.id)),
160
+ ])
161
+ return order ? { ...onTheWire(order), items } : null
162
+ },
163
+ })
164
+
165
+ export const setOrderStatus = mutation({
166
+ input: z.object({ id: z.number().int().positive(), status }),
167
+ auth: 'user',
168
+ invalidates: ['orders'],
169
+ optimistic: ({ input }, cache) =>
170
+ cache.update(getOrder, { id: input.id }, (order) => {
171
+ if (order) order.status = input.status
172
+ }),
173
+ run: async ({ input, ctx }) => {
174
+ await ctx.db.update(orders).set({ status: input.status }).where(eq(orders.id, input.id))
175
+ },
176
+ })
177
+
178
+ export const markShipped = mutation({
179
+ // `view` names the list on screen, for the optimistic update; the server only reads `ids`.
180
+ input: z.object({
181
+ ids: z.array(z.number().int().positive()).min(1).max(PAGE_SIZE),
182
+ view: orderView,
183
+ }),
184
+ auth: 'user',
185
+ invalidates: ['orders'],
186
+ optimistic: ({ input }, cache) =>
187
+ cache.update(listOrders, input.view, (list) => {
188
+ for (const row of list.rows) if (input.ids.includes(row.id)) row.status = 'shipped'
189
+ }),
190
+ run: async ({ input, ctx }) => {
191
+ await ctx.db.update(orders).set({ status: 'shipped' }).where(inArray(orders.id, input.ids))
192
+ },
193
+ })
@@ -0,0 +1,92 @@
1
+ import { query } from '@weft-js/start'
2
+ import { count, desc, eq, sql } from 'drizzle-orm'
3
+ import { z } from 'zod'
4
+ import { customers, orders } from '#/db/schema'
5
+
6
+ const revenue = sql<number>`coalesce(sum(${orders.totalCents}), 0)`.mapWith(Number)
7
+
8
+ /** The whole store, for the band above the regions. */
9
+ export const reach = query({
10
+ tags: ['customers'],
11
+ auth: 'user',
12
+ run: async ({ ctx }) => {
13
+ const [row] = await ctx.db
14
+ .select({
15
+ customers: count(),
16
+ cities: sql<number>`count(distinct ${customers.city})`.mapWith(Number),
17
+ })
18
+ .from(customers)
19
+ return row!
20
+ },
21
+ })
22
+
23
+ export const topCities = query({
24
+ tags: ['orders', 'customers'],
25
+ auth: 'user',
26
+ run: ({ ctx }) =>
27
+ ctx.db
28
+ .select({ city: customers.city, orders: count(), revenue })
29
+ .from(orders)
30
+ .innerJoin(customers, eq(customers.id, orders.customerId))
31
+ .groupBy(customers.city)
32
+ .orderBy(desc(revenue)),
33
+ })
34
+
35
+ export const biggestCustomers = query({
36
+ tags: ['orders', 'customers'],
37
+ auth: 'user',
38
+ run: ({ ctx }) =>
39
+ ctx.db
40
+ .select({ id: customers.id, name: customers.name, city: customers.city, revenue })
41
+ .from(orders)
42
+ .innerJoin(customers, eq(customers.id, orders.customerId))
43
+ .groupBy(customers.id)
44
+ .orderBy(desc(revenue))
45
+ .limit(5),
46
+ })
47
+
48
+ export const recentOrderIds = query({
49
+ tags: ['orders'],
50
+ auth: 'user',
51
+ run: async ({ ctx }) => {
52
+ const rows = await ctx.db
53
+ .select({
54
+ id: orders.id,
55
+ customerId: orders.customerId,
56
+ status: orders.status,
57
+ totalCents: orders.totalCents,
58
+ createdAt: orders.createdAt,
59
+ })
60
+ .from(orders)
61
+ .orderBy(desc(orders.createdAt), desc(orders.id))
62
+ .limit(50)
63
+ return rows.map((row) => ({ ...row, createdAt: row.createdAt.toISOString() }))
64
+ },
65
+ })
66
+
67
+ export const getCustomer = query({
68
+ input: z.object({ id: z.number().int().positive() }),
69
+ tags: ['customers'],
70
+ auth: 'user',
71
+ run: async ({ input, ctx }) => {
72
+ const [customer] = await ctx.db
73
+ .select({ name: customers.name, city: customers.city })
74
+ .from(customers)
75
+ .where(eq(customers.id, input.id))
76
+ return customer ?? null
77
+ },
78
+ })
79
+
80
+ // The year's books take a while to add up.
81
+ export const yearReport = query({
82
+ tags: ['orders'],
83
+ auth: 'user',
84
+ run: async ({ ctx }) => {
85
+ await ctx.db.execute(sql`select pg_sleep(2)`)
86
+ return ctx.db
87
+ .select({ status: orders.status, orders: count(), revenue })
88
+ .from(orders)
89
+ .groupBy(orders.status)
90
+ .orderBy(desc(revenue))
91
+ },
92
+ })
@@ -0,0 +1,42 @@
1
+ import { mutation, query, redirect } from '@weft-js/start'
2
+ import { forward } from '@weft-js/start/auth'
3
+ import { z } from 'zod'
4
+ import auth from '#/auth.server'
5
+
6
+ const credentials = z.object({ email: z.email(), password: z.string().min(8) })
7
+
8
+ /** Who is signed in, or null. */
9
+ export const me = query({
10
+ tags: ['session'],
11
+ run: ({ ctx }) =>
12
+ ctx.user ? { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email } : null,
13
+ })
14
+
15
+ export const signUp = mutation({
16
+ input: credentials.extend({ name: z.string().trim().min(1) }),
17
+ run: async ({ input, ctx }) => {
18
+ await forward(
19
+ ctx,
20
+ auth.api.signUpEmail({ body: input, headers: ctx.request.headers, asResponse: true }),
21
+ )
22
+ redirect('/admin')
23
+ },
24
+ })
25
+
26
+ export const signIn = mutation({
27
+ input: credentials,
28
+ run: async ({ input, ctx }) => {
29
+ await forward(
30
+ ctx,
31
+ auth.api.signInEmail({ body: input, headers: ctx.request.headers, asResponse: true }),
32
+ )
33
+ redirect('/admin')
34
+ },
35
+ })
36
+
37
+ export const signOut = mutation({
38
+ run: async ({ ctx }) => {
39
+ await forward(ctx, auth.api.signOut({ headers: ctx.request.headers, asResponse: true }))
40
+ redirect('/login')
41
+ },
42
+ })
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Backroom's own CSS, over warp's tokens. The indigo, the cool greys, the compact density and the soft corners are warp()
3
+ * options in weft.config.ts; this file is the layout of the pages and the tabular figures. Change those options first.
4
+ */
5
+ body { margin: 0; }
6
+
7
+ /* AuthScreen fills its parent, and here its parent is the page */
8
+ [data-scope="auth"], [data-scope="auth"] > [data-part="layout"] { min-block-size: 100dvh; }
9
+
10
+ /* the brand panel only shows from 1000 up, so narrower pages carry the name themselves */
11
+ .auth-mark { display: flex; align-items: center; gap: var(--warp-space-tight); font-weight: var(--warp-text-heading-weight); }
12
+ @container auth (min-width: 1000px) { .auth-mark { display: none; } }
13
+
14
+ .logo { display: block; inline-size: 1.5rem; block-size: 1.5rem; flex: none; }
15
+
16
+ .form-error { margin: 0; color: var(--warp-color-critical-text); font-size: var(--warp-text-caption-size); }
17
+ .form-error:empty { display: none; }
18
+
19
+ .page {
20
+ inline-size: 100%;
21
+ max-inline-size: 76rem;
22
+ margin-inline: auto;
23
+ padding-block: var(--warp-space-group) var(--warp-space-page);
24
+ }
25
+
26
+ .flow { display: grid; gap: var(--warp-space-group); }
27
+
28
+ .section-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--warp-space-related); }
29
+
30
+ .stat { font-variant-numeric: tabular-nums; }
31
+
32
+ /* the status words are lower case in the data and the select: this is only how they read */
33
+ [data-scope="badge"], .filters option, [data-scope="field"] select { text-transform: capitalize; }
34
+
35
+ [data-scope="table"] { font-variant-numeric: tabular-nums; }
36
+
37
+ .filters { display: grid; grid-template-columns: minmax(0, 24rem) minmax(9rem, 12rem); gap: var(--warp-space-related); align-items: end; }
38
+ @media (max-width: 560px) { .filters { grid-template-columns: 1fr; } }
39
+
40
+ .pager { display: flex; align-items: center; justify-content: space-between; gap: var(--warp-space-related); }
41
+ .pager [aria-disabled="true"] { color: var(--warp-color-text-subtle); }
42
+
43
+ .system-page { display: grid; place-items: center; min-block-size: 100dvh; background: var(--warp-color-page); }
@@ -0,0 +1,31 @@
1
+ import { defineConfig } from '@weft-js/core/config'
2
+ import { warp } from '@weft-js/warp/plugin'
3
+
4
+ export default defineConfig({
5
+ site: {
6
+ name: 'Backroom',
7
+ locale: 'en_US',
8
+ title: '%s · Backroom',
9
+ description: 'Backroom is where you run the shop: orders, customers and revenue in one place.',
10
+ head: '<link rel="icon" href="/favicon.svg" type="image/svg+xml">',
11
+ },
12
+ plugins: [
13
+ warp({
14
+ seed: { accent: '#4f46e5' },
15
+ dials: { expression: 'balanced', density: 'compact' },
16
+ radius: 'soft',
17
+ fields: 'classic',
18
+ fonts: {
19
+ display: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
20
+ body: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
21
+ mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
22
+ },
23
+ tokens: {
24
+ '--warp-color-page': 'light-dark(#eceef5, #0e1020)',
25
+ '--warp-color-surface': 'light-dark(#fbfbfe, #161a2e)',
26
+ '--warp-color-surface-raised': 'light-dark(#ffffff, #1b2036)',
27
+ '--warp-color-surface-sunken': 'light-dark(#f1f2f8, #0a0c18)',
28
+ },
29
+ }),
30
+ ],
31
+ })
@@ -0,0 +1,22 @@
1
+ {
2
+ "requires": { "db": "postgres", "auth": true },
3
+ "pages": ["/", "/login", "/signup", "/admin", "/admin/orders", "/admin/orders/[id]", "/admin/nested", "/admin/per-row", "/admin/slow"],
4
+ "dependencies": { "@weft-js/warp-icons": "0.0.0" },
5
+ "warp": {
6
+ "seed": { "accent": "#4f46e5" },
7
+ "dials": { "expression": "balanced", "density": "compact" },
8
+ "radius": "soft",
9
+ "fields": "classic",
10
+ "fonts": {
11
+ "display": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
12
+ "body": "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif",
13
+ "mono": "ui-monospace, \"SF Mono\", Menlo, Consolas, monospace"
14
+ },
15
+ "tokens": {
16
+ "--warp-color-page": "light-dark(#eceef5, #0e1020)",
17
+ "--warp-color-surface": "light-dark(#fbfbfe, #161a2e)",
18
+ "--warp-color-surface-raised": "light-dark(#ffffff, #1b2036)",
19
+ "--warp-color-surface-sunken": "light-dark(#f1f2f8, #0a0c18)"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1 @@
1
+ 811665ce6d284c37
@@ -0,0 +1,47 @@
1
+ CREATE TABLE IF NOT EXISTS "cart_items" (
2
+ "cart_id" uuid NOT NULL,
3
+ "product_id" integer NOT NULL,
4
+ "quantity" integer NOT NULL,
5
+ CONSTRAINT "cart_items_cart_id_product_id_pk" PRIMARY KEY("cart_id","product_id")
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE IF NOT EXISTS "carts" (
9
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
10
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
11
+ );
12
+ --> statement-breakpoint
13
+ CREATE TABLE IF NOT EXISTS "categories" (
14
+ "id" serial PRIMARY KEY NOT NULL,
15
+ "slug" text NOT NULL,
16
+ "name" text NOT NULL,
17
+ CONSTRAINT "categories_slug_unique" UNIQUE("slug")
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE TABLE IF NOT EXISTS "products" (
21
+ "id" serial PRIMARY KEY NOT NULL,
22
+ "slug" text NOT NULL,
23
+ "name" text NOT NULL,
24
+ "description" text NOT NULL,
25
+ "category_id" integer NOT NULL,
26
+ "price_cents" integer NOT NULL,
27
+ "stock" integer NOT NULL,
28
+ "featured" boolean DEFAULT false NOT NULL,
29
+ CONSTRAINT "products_slug_unique" UNIQUE("slug")
30
+ );
31
+ --> statement-breakpoint
32
+ DO $$ BEGIN
33
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"cart_items"'::regclass AND c.contype = 'f' AND a.attname = 'cart_id') THEN
34
+ ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_cart_id_carts_id_fk" FOREIGN KEY ("cart_id") REFERENCES "public"."carts"("id") ON DELETE cascade ON UPDATE no action;
35
+ END IF;
36
+ END $$;--> statement-breakpoint
37
+ DO $$ BEGIN
38
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"cart_items"'::regclass AND c.contype = 'f' AND a.attname = 'product_id') THEN
39
+ ALTER TABLE "cart_items" ADD CONSTRAINT "cart_items_product_id_products_id_fk" FOREIGN KEY ("product_id") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;
40
+ END IF;
41
+ END $$;--> statement-breakpoint
42
+ DO $$ BEGIN
43
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"products"'::regclass AND c.contype = 'f' AND a.attname = 'category_id') THEN
44
+ ALTER TABLE "products" ADD CONSTRAINT "products_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE no action ON UPDATE no action;
45
+ END IF;
46
+ END $$;--> statement-breakpoint
47
+ CREATE INDEX IF NOT EXISTS "products_category" ON "products" USING btree ("category_id","price_cents","id");