@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.
- package/README.md +393 -19
- package/bin/create-weft.mjs +62 -14
- package/package.json +26 -16
- package/scripts/tarballs.mjs +56 -6
- package/src/data.mjs +90 -18
- package/src/index.mjs +210 -51
- package/src/stack/pages.mjs +151 -0
- package/src/stack/schema.mjs +195 -0
- package/src/stack.mjs +102 -313
- package/src/templates.mjs +174 -0
- package/src/wizard.mjs +65 -16
- package/templates/admin/files/drizzle/.schema-hash +1 -0
- package/templates/admin/files/drizzle/0000_admin.sql +136 -0
- package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
- package/templates/admin/files/drizzle/meta/_journal.json +13 -0
- package/templates/admin/files/public/favicon.svg +1 -0
- package/templates/admin/files/src/auth.server.ts +24 -0
- package/templates/admin/files/src/components/AuthPage.tsx +33 -0
- package/templates/admin/files/src/components/Failed.tsx +19 -0
- package/templates/admin/files/src/components/Logo.tsx +16 -0
- package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
- package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
- package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
- package/templates/admin/files/src/components/Overview.tsx +113 -0
- package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
- package/templates/admin/files/src/db/schema.ts +68 -0
- package/templates/admin/files/src/lib/orders.ts +60 -0
- package/templates/admin/files/src/routes/404.tsx +20 -0
- package/templates/admin/files/src/routes/_error.tsx +20 -0
- package/templates/admin/files/src/routes/_layout.tsx +6 -0
- package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
- package/templates/admin/files/src/routes/admin/index.tsx +45 -0
- package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
- package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
- package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
- package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
- package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
- package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
- package/templates/admin/files/src/routes/index.tsx +13 -0
- package/templates/admin/files/src/routes/login.tsx +53 -0
- package/templates/admin/files/src/routes/signup.tsx +60 -0
- package/templates/admin/files/src/server/orders.ts +193 -0
- package/templates/admin/files/src/server/reports.ts +92 -0
- package/templates/admin/files/src/server/session.ts +42 -0
- package/templates/admin/files/src/theme.css +43 -0
- package/templates/admin/files/weft.config.ts +31 -0
- package/templates/admin/template.json +22 -0
- package/templates/catalog/files/drizzle/.schema-hash +1 -0
- package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
- package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
- package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
- package/templates/catalog/files/public/favicon.svg +1 -0
- package/templates/catalog/files/seed.sql +13 -0
- package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
- package/templates/catalog/files/src/components/Aisles.tsx +31 -0
- package/templates/catalog/files/src/components/CartLines.tsx +96 -0
- package/templates/catalog/files/src/components/CartLink.tsx +29 -0
- package/templates/catalog/files/src/components/Failed.tsx +19 -0
- package/templates/catalog/files/src/components/Featured.tsx +19 -0
- package/templates/catalog/files/src/components/Filters.tsx +40 -0
- package/templates/catalog/files/src/components/Logo.tsx +16 -0
- package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
- package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
- package/templates/catalog/files/src/components/ProductList.tsx +65 -0
- package/templates/catalog/files/src/components/Related.tsx +19 -0
- package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
- package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
- package/templates/catalog/files/src/db/schema.ts +53 -0
- package/templates/catalog/files/src/lib/catalog.ts +35 -0
- package/templates/catalog/files/src/routes/404.tsx +18 -0
- package/templates/catalog/files/src/routes/_error.tsx +18 -0
- package/templates/catalog/files/src/routes/_layout.tsx +14 -0
- package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
- package/templates/catalog/files/src/routes/cart.tsx +29 -0
- package/templates/catalog/files/src/routes/index.tsx +84 -0
- package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
- package/templates/catalog/files/src/server/cart.ts +125 -0
- package/templates/catalog/files/src/server/catalog.ts +107 -0
- package/templates/catalog/files/src/theme.css +159 -0
- package/templates/catalog/files/weft.config.ts +30 -0
- package/templates/catalog/template.json +20 -0
- package/templates/landing/files/.weft/plugins.lock.json +42 -0
- package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
- package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
- package/templates/landing/files/assets/hero.webp +0 -0
- package/templates/landing/files/drizzle/.schema-hash +1 -0
- package/templates/landing/files/drizzle/0000_landing.sql +6 -0
- package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
- package/templates/landing/files/drizzle/meta/_journal.json +13 -0
- package/templates/landing/files/public/favicon.svg +1 -0
- package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
- package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
- package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
- package/templates/landing/files/src/db/schema.ts +7 -0
- package/templates/landing/files/src/routes/404.tsx +24 -0
- package/templates/landing/files/src/routes/_error.tsx +24 -0
- package/templates/landing/files/src/routes/_layout.tsx +14 -0
- package/templates/landing/files/src/routes/index.tsx +130 -0
- package/templates/landing/files/src/routes/joined.tsx +30 -0
- package/templates/landing/files/src/routes/pricing.tsx +131 -0
- package/templates/landing/files/src/routes/privacy.tsx +56 -0
- package/templates/landing/files/src/server/waitlist.ts +13 -0
- package/templates/landing/files/src/theme.css +108 -0
- package/templates/landing/files/weft.config.ts +44 -0
- package/templates/landing/template.json +17 -0
- package/templates/todo/files/drizzle/.schema-hash +1 -0
- package/templates/todo/files/drizzle/0000_todo.sql +82 -0
- package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
- package/templates/todo/files/drizzle/meta/_journal.json +13 -0
- package/templates/todo/files/public/favicon.svg +1 -0
- package/templates/todo/files/src/components/AuthPage.tsx +33 -0
- package/templates/todo/files/src/components/Logo.tsx +14 -0
- package/templates/todo/files/src/components/NewTodo.tsx +27 -0
- package/templates/todo/files/src/components/TodoList.tsx +101 -0
- package/templates/todo/files/src/db/schema.ts +18 -0
- package/templates/todo/files/src/lib/views.ts +13 -0
- package/templates/todo/files/src/routes/404.tsx +20 -0
- package/templates/todo/files/src/routes/_error.tsx +20 -0
- package/templates/todo/files/src/routes/_layout.tsx +6 -0
- package/templates/todo/files/src/routes/index.tsx +13 -0
- package/templates/todo/files/src/routes/login.tsx +53 -0
- package/templates/todo/files/src/routes/signup.tsx +57 -0
- package/templates/todo/files/src/routes/todos.tsx +87 -0
- package/templates/todo/files/src/server/todos.ts +98 -0
- package/templates/todo/files/src/theme.css +71 -0
- package/templates/todo/files/weft.config.ts +30 -0
- 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");
|