@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,224 @@
1
+ ---
2
+ name: ecommerce
3
+ description: Build an ecommerce system on Quark. Use when the user wants products, catalog, cart, checkout, orders, inventory, or storefront.
4
+ ---
5
+
6
+ # Ecommerce Skill
7
+
8
+ Build an ecommerce system on Quark's infrastructure. This skill gives you the domain context, the Quark framework patterns, and the end-result shape. You generate the code that fits the user's exact requirements.
9
+
10
+ ## Context
11
+
12
+ An ecommerce system manages a product catalog, shopping cart, checkout flow, orders, and inventory. The core entities and concerns:
13
+
14
+ - **Product** — the sellable item with name, description, pricing, and images.
15
+ - **ProductVariant** — a specific option of a product (size, color, material) with its own SKU, price, and stock.
16
+ - **Category** — hierarchical grouping for browsing and filtering.
17
+ - **Cart** — a session-linked or user-linked collection of items before purchase.
18
+ - **Order** — a completed purchase with line items, shipping address, payment status, and fulfillment state.
19
+ - **Inventory** — stock levels, reserved vs. available counts, atomic decrement on order.
20
+
21
+ ## Framework context (build on Quark)
22
+
23
+ - **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
24
+ - **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
25
+ - **Server Actions** (preferred for mutations) use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@techstream/quark-core/errors`.
26
+ - **Admin views** live under `apps/web/src/app/admin/` (see the admin-dashboard skill for product/order management).
27
+ - **Auth** via `getCurrentSession` from `@techstream/quark-core`.
28
+ - **Payment** integration follows the payment skill for Stripe Checkout or similar processors.
29
+
30
+ ## Workflow
31
+
32
+ 1. Read `CLAUDE.md` and `MAIN.md` for project conventions.
33
+ 2. Clarify the user's requirements: product types, variants, categories, checkout flow, payment provider.
34
+ 3. Design the Prisma models (Product, Variant, Category, Cart, CartItem, Order, OrderItem) and add query helpers.
35
+ 4. Build the catalog endpoints: CRUD for products, variants, categories with admin guards.
36
+ 5. Build the cart: session-based or user-linked, add/remove/update actions, cart total calculation.
37
+ 6. Build checkout: address collection → shipping method → payment → order creation with inventory decrement.
38
+ 7. Add tests near the changed code.
39
+
40
+ ## Example: Prisma models
41
+
42
+ ```prisma
43
+ enum OrderStatus {
44
+ PENDING
45
+ PAID
46
+ PROCESSING
47
+ FULFILLED
48
+ DELIVERED
49
+ REFUNDED
50
+ CANCELLED
51
+ }
52
+
53
+ model Product {
54
+ id String @id @default(cuid())
55
+ name String
56
+ slug String @unique
57
+ description String? @db.Text
58
+ basePrice Decimal @db.Decimal(10, 2)
59
+ currency String @default("USD")
60
+ images String[]
61
+ active Boolean @default(true)
62
+ categoryId String?
63
+ category Category? @relation(fields: [categoryId], references: [id])
64
+ variants ProductVariant[]
65
+ createdAt DateTime @default(now())
66
+ updatedAt DateTime @updatedAt
67
+
68
+ @@index([slug])
69
+ @@index([categoryId])
70
+ @@index([active])
71
+ }
72
+
73
+ model ProductVariant {
74
+ id String @id @default(cuid())
75
+ productId String
76
+ product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
77
+ sku String @unique
78
+ name String
79
+ price Decimal @db.Decimal(10, 2)
80
+ stock Int @default(0)
81
+ reserved Int @default(0)
82
+ createdAt DateTime @default(now())
83
+ updatedAt DateTime @updatedAt
84
+
85
+ @@index([productId])
86
+ @@index([sku])
87
+ }
88
+
89
+ model Category {
90
+ id String @id @default(cuid())
91
+ name String
92
+ slug String @unique
93
+ description String? @db.Text
94
+ parentId String?
95
+ parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
96
+ children Category[] @relation("CategoryTree")
97
+ products Product[]
98
+ createdAt DateTime @default(now())
99
+ updatedAt DateTime @updatedAt
100
+
101
+ @@index([slug])
102
+ @@index([parentId])
103
+ }
104
+
105
+ model Cart {
106
+ id String @id @default(cuid())
107
+ userId String?
108
+ user User? @relation(fields: [userId], references: [id])
109
+ sessionId String? @unique
110
+ items CartItem[]
111
+ createdAt DateTime @default(now())
112
+ updatedAt DateTime @updatedAt
113
+
114
+ @@index([userId])
115
+ @@index([sessionId])
116
+ }
117
+
118
+ model CartItem {
119
+ id String @id @default(cuid())
120
+ cartId String
121
+ cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
122
+ variantId String
123
+ variant ProductVariant @relation(fields: [variantId], references: [id])
124
+ quantity Int @default(1)
125
+ createdAt DateTime @default(now())
126
+ updatedAt DateTime @updatedAt
127
+
128
+ @@unique([cartId, variantId])
129
+ }
130
+
131
+ model Order {
132
+ id String @id @default(cuid())
133
+ userId String?
134
+ user User? @relation(fields: [userId], references: [id])
135
+ status OrderStatus @default(PENDING)
136
+ email String
137
+ shippingName String
138
+ shippingAddress String @db.Text
139
+ shippingCity String
140
+ shippingZip String
141
+ shippingCountry String
142
+ total Decimal @db.Decimal(10, 2)
143
+ currency String @default("USD")
144
+ stripeSessionId String? @unique
145
+ paidAt DateTime?
146
+ fulfilledAt DateTime?
147
+ deliveredAt DateTime?
148
+ createdAt DateTime @default(now())
149
+ updatedAt DateTime @updatedAt
150
+ items OrderItem[]
151
+
152
+ @@index([userId, status])
153
+ @@index([status])
154
+ @@index([stripeSessionId])
155
+ @@index([createdAt])
156
+ }
157
+
158
+ model OrderItem {
159
+ id String @id @default(cuid())
160
+ orderId String
161
+ order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
162
+ variantId String
163
+ variant ProductVariant @relation(fields: [variantId], references: [id])
164
+ quantity Int
165
+ unitPrice Decimal @db.Decimal(10, 2)
166
+ createdAt DateTime @default(now())
167
+ updatedAt DateTime @updatedAt
168
+
169
+ @@index([orderId])
170
+ }
171
+ ```
172
+
173
+ ## Example: Zod validation
174
+
175
+ ```js
176
+ import { z } from "zod";
177
+
178
+ export const addToCartSchema = z.object({
179
+ cartId: z.string().min(1),
180
+ variantId: z.string().min(1),
181
+ quantity: z.coerce.number().int().positive().default(1),
182
+ });
183
+
184
+ export const checkoutSchema = z.object({
185
+ cartId: z.string().min(1),
186
+ email: z.string().email(),
187
+ shippingName: z.string().min(1),
188
+ shippingAddress: z.string().min(1),
189
+ shippingCity: z.string().min(1),
190
+ shippingZip: z.string().min(1),
191
+ shippingCountry: z.string().min(2).max(2),
192
+ });
193
+ ```
194
+
195
+ ## Sub-skills
196
+
197
+ The ecommerce domain is split into focused sub-skills. Load the one that matches your current task:
198
+
199
+ | Sub-skill | Path | Covers |
200
+ |---|---|---|
201
+ | ecommerce-catalog | `ecommerce-catalog/SKILL.md` | Product, variant, and category models + CRUD. |
202
+ | ecommerce-cart | `ecommerce-cart/SKILL.md` | Cart model, add/remove/update, session linking, totals. |
203
+ | ecommerce-checkout | `ecommerce-checkout/SKILL.md` | Checkout flow, order lifecycle, inventory, integration. |
204
+
205
+ ## Integration points
206
+
207
+ ### Admin dashboard
208
+
209
+ Use the admin-dashboard skill to build product management screens:
210
+ - `apps/web/src/app/admin/products/` — list, create, edit products and variants.
211
+ - `apps/web/src/app/admin/orders/` — list orders, update status, view details.
212
+ - `apps/web/src/app/admin/categories/` — manage category tree.
213
+
214
+ ### Payment
215
+
216
+ Reference the payment skill for Stripe Checkout integration:
217
+ 1. Create a Checkout Session with cart line items.
218
+ 2. Store `stripeSessionId` on the Order.
219
+ 3. Handle the `checkout.session.completed` webhook to transition to `PAID`.
220
+ 4. Handle refunds via the Stripe API, transitioning to `REFUNDED`.
221
+
222
+ ## End result
223
+
224
+ A working ecommerce system where users can browse a product catalog, add items to a cart, complete checkout with payment, and have orders tracked through a fulfillment lifecycle — with inventory management, admin screens, and Quark conventions throughout.
@@ -0,0 +1,193 @@
1
+ ---
2
+ name: ecommerce-cart
3
+ description: Shopping cart for Quark ecommerce. Use when building cart add/remove/update, session linking, or cart totals.
4
+ ---
5
+
6
+ # Ecommerce Cart Skill
7
+
8
+ Build the shopping cart for an ecommerce system on Quark. This sub-skill covers the cart model, item operations, session linking, and total calculation.
9
+
10
+ ## Context
11
+
12
+ The cart is a temporary collection of items a user intends to purchase. Key concerns:
13
+
14
+ - **Session-based or user-linked** — anonymous carts use a session ID; authenticated carts link to a user.
15
+ - **Add/remove/update** — atomic operations with stock validation.
16
+ - **Cart totals** — computed from item quantities and variant prices.
17
+
18
+ ## Framework context (build on Quark)
19
+
20
+ - **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
21
+ - **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
22
+ - **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@techstream/quark-core/errors`.
23
+ - **Auth** via `getCurrentSession` from `@techstream/quark-core`.
24
+
25
+ ## Models
26
+
27
+ ```prisma
28
+ model Cart {
29
+ id String @id @default(cuid())
30
+ userId String?
31
+ user User? @relation(fields: [userId], references: [id])
32
+ sessionId String? @unique
33
+ items CartItem[]
34
+ createdAt DateTime @default(now())
35
+ updatedAt DateTime @updatedAt
36
+
37
+ @@index([userId])
38
+ @@index([sessionId])
39
+ }
40
+
41
+ model CartItem {
42
+ id String @id @default(cuid())
43
+ cartId String
44
+ cart Cart @relation(fields: [cartId], references: [id], onDelete: Cascade)
45
+ variantId String
46
+ variant ProductVariant @relation(fields: [variantId], references: [id])
47
+ quantity Int @default(1)
48
+ createdAt DateTime @default(now())
49
+ updatedAt DateTime @updatedAt
50
+
51
+ @@unique([cartId, variantId])
52
+ }
53
+ ```
54
+
55
+ ## Query helpers
56
+
57
+ ```js
58
+ // packages/db/src/queries.js additions
59
+
60
+ export async function getCartBySession(sessionId) {
61
+ return prisma.cart.findUnique({
62
+ where: { sessionId },
63
+ include: { items: { include: { variant: { include: { product: true } } } } },
64
+ });
65
+ }
66
+
67
+ export async function getCartByUser(userId) {
68
+ return prisma.cart.findFirst({
69
+ where: { userId },
70
+ include: { items: { include: { variant: { include: { product: true } } } } },
71
+ });
72
+ }
73
+
74
+ export async function getCartTotal(cartId) {
75
+ const items = await prisma.cartItem.findMany({
76
+ where: { cartId },
77
+ include: { variant: true },
78
+ });
79
+ return items.reduce((sum, item) => sum + Number(item.variant.price) * item.quantity, 0);
80
+ }
81
+ ```
82
+
83
+ ## Cart operations
84
+
85
+ ### Adding to cart
86
+
87
+ Use a Prisma transaction to prevent race conditions. Check stock availability before adding.
88
+
89
+ ```js
90
+ import { prisma } from "@__QUARK_SCOPE__/db";
91
+ import { AppError } from "@techstream/quark-core/errors";
92
+
93
+ export async function addToCart(cartId, variantId, quantity = 1) {
94
+ return prisma.$transaction(async (tx) => {
95
+ const variant = await tx.productVariant.findUnique({ where: { id: variantId } });
96
+ if (!variant) throw new AppError("Variant not found", 404, "NOT_FOUND");
97
+
98
+ const available = variant.stock - variant.reserved;
99
+ if (available < quantity) {
100
+ throw new AppError("Insufficient stock", 409, "OUT_OF_STOCK");
101
+ }
102
+
103
+ const existing = await tx.cartItem.findUnique({
104
+ where: { cartId_variantId: { cartId, variantId } },
105
+ });
106
+
107
+ if (existing) {
108
+ return tx.cartItem.update({
109
+ where: { id: existing.id },
110
+ data: { quantity: existing.quantity + quantity },
111
+ });
112
+ }
113
+
114
+ return tx.cartItem.create({
115
+ data: { cartId, variantId, quantity },
116
+ });
117
+ });
118
+ }
119
+ ```
120
+
121
+ ### Updating quantity
122
+
123
+ ```js
124
+ export async function updateCartItemQuantity(cartItemId, quantity) {
125
+ if (quantity < 1) {
126
+ return prisma.cartItem.delete({ where: { id: cartItemId } });
127
+ }
128
+ return prisma.cartItem.update({
129
+ where: { id: cartItemId },
130
+ data: { quantity },
131
+ });
132
+ }
133
+ ```
134
+
135
+ ### Removing from cart
136
+
137
+ ```js
138
+ export async function removeFromCart(cartItemId) {
139
+ return prisma.cartItem.delete({ where: { id: cartItemId } });
140
+ }
141
+ ```
142
+
143
+ ## Session linking
144
+
145
+ For anonymous users, create a cart with a random `sessionId` cookie. On login, merge the session cart into the user's cart:
146
+
147
+ ```js
148
+ export async function mergeCarts(sessionId, userId) {
149
+ const sessionCart = await prisma.cart.findUnique({
150
+ where: { sessionId },
151
+ include: { items: true },
152
+ });
153
+ if (!sessionCart) return;
154
+
155
+ let userCart = await prisma.cart.findFirst({ where: { userId } });
156
+ if (!userCart) {
157
+ userCart = await prisma.cart.create({ data: { userId } });
158
+ }
159
+
160
+ for (const item of sessionCart.items) {
161
+ const existing = await prisma.cartItem.findUnique({
162
+ where: { cartId_variantId: { cartId: userCart.id, variantId: item.variantId } },
163
+ });
164
+ if (existing) {
165
+ await prisma.cartItem.update({
166
+ where: { id: existing.id },
167
+ data: { quantity: existing.quantity + item.quantity },
168
+ });
169
+ } else {
170
+ await prisma.cartItem.create({
171
+ data: { cartId: userCart.id, variantId: item.variantId, quantity: item.quantity },
172
+ });
173
+ }
174
+ }
175
+
176
+ await prisma.cartItem.deleteMany({ where: { cartId: sessionCart.id } });
177
+ await prisma.cart.delete({ where: { id: sessionCart.id } });
178
+ }
179
+ ```
180
+
181
+ ## Example validation schema
182
+
183
+ ```js
184
+ const addToCartSchema = z.object({
185
+ cartId: z.string().min(1),
186
+ variantId: z.string().min(1),
187
+ quantity: z.coerce.number().int().positive().default(1),
188
+ });
189
+ ```
190
+
191
+ ## End result
192
+
193
+ A cart where users can add, remove, and update items — with stock validation, session merging on login, and computed totals ready for checkout.
@@ -0,0 +1,179 @@
1
+ ---
2
+ name: ecommerce-catalog
3
+ description: Product catalog models and CRUD for Quark ecommerce. Use when managing products, variants, categories, or pricing.
4
+ ---
5
+
6
+ # Ecommerce Catalog Skill
7
+
8
+ Manage the product catalog for an ecommerce system on Quark. This sub-skill covers product, variant, and category models plus CRUD operations.
9
+
10
+ ## Context
11
+
12
+ The catalog is the foundation of an ecommerce system. It defines what you sell, how it's organized, and how it's priced.
13
+
14
+ - **Product** — the sellable item with name, description, pricing, and images.
15
+ - **ProductVariant** — a specific option of a product (size, color, material) with its own SKU, price, and stock.
16
+ - **Category** — hierarchical grouping for browsing and filtering.
17
+
18
+ ## Framework context (build on Quark)
19
+
20
+ - **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
21
+ - **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
22
+ - **API routes** live in `apps/web/src/app/api/<resource>/`. Guard with `requireRole`, validate with Zod via `validateBody`, wrap mutations in `withCsrfProtection`. Follow `apps/web/src/app/api/users/route.js` as the reference shape.
23
+ - **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@techstream/quark-core/errors`.
24
+ - **Admin views** live under `apps/web/src/app/admin/products/` and `apps/web/src/app/admin/categories/`.
25
+
26
+ ## Models
27
+
28
+ ### Category
29
+
30
+ ```prisma
31
+ model Category {
32
+ id String @id @default(cuid())
33
+ name String
34
+ slug String @unique
35
+ description String? @db.Text
36
+ parentId String?
37
+ parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
38
+ children Category[] @relation("CategoryTree")
39
+ products Product[]
40
+ createdAt DateTime @default(now())
41
+ updatedAt DateTime @updatedAt
42
+
43
+ @@index([slug])
44
+ @@index([parentId])
45
+ }
46
+ ```
47
+
48
+ ### Product
49
+
50
+ ```prisma
51
+ model Product {
52
+ id String @id @default(cuid())
53
+ name String
54
+ slug String @unique
55
+ description String? @db.Text
56
+ basePrice Decimal @db.Decimal(10, 2)
57
+ currency String @default("USD")
58
+ images String[]
59
+ active Boolean @default(true)
60
+ categoryId String?
61
+ category Category? @relation(fields: [categoryId], references: [id])
62
+ variants ProductVariant[]
63
+ createdAt DateTime @default(now())
64
+ updatedAt DateTime @updatedAt
65
+
66
+ @@index([slug])
67
+ @@index([categoryId])
68
+ @@index([active])
69
+ }
70
+ ```
71
+
72
+ ### ProductVariant
73
+
74
+ ```prisma
75
+ model ProductVariant {
76
+ id String @id @default(cuid())
77
+ productId String
78
+ product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
79
+ sku String @unique
80
+ name String // e.g. "Large / Blue"
81
+ price Decimal @db.Decimal(10, 2)
82
+ stock Int @default(0)
83
+ reserved Int @default(0)
84
+ createdAt DateTime @default(now())
85
+ updatedAt DateTime @updatedAt
86
+
87
+ @@index([productId])
88
+ @@index([sku])
89
+ }
90
+ ```
91
+
92
+ ## Query helpers
93
+
94
+ ```js
95
+ // packages/db/src/queries.js additions
96
+
97
+ export async function getProductBySlug(slug) {
98
+ return prisma.product.findUnique({
99
+ where: { slug },
100
+ include: { category: true, variants: true },
101
+ });
102
+ }
103
+
104
+ export async function listActiveProducts({ categoryId, limit = 20, offset = 0 } = {}) {
105
+ const where = { active: true };
106
+ if (categoryId) where.categoryId = categoryId;
107
+ return prisma.product.findMany({
108
+ where,
109
+ include: { category: true, variants: true },
110
+ orderBy: { createdAt: "desc" },
111
+ take: limit,
112
+ skip: offset,
113
+ });
114
+ }
115
+
116
+ export async function getCategoryTree() {
117
+ return prisma.category.findMany({
118
+ where: { parentId: null },
119
+ include: { children: true, _count: { select: { products: true } } },
120
+ });
121
+ }
122
+ ```
123
+
124
+ ## Admin CRUD actions
125
+
126
+ ```js
127
+ "use server";
128
+ import { z } from "zod";
129
+ import { prisma } from "@__QUARK_SCOPE__/db";
130
+ import { ValidationError, AppError } from "@techstream/quark-core/errors";
131
+ import { createLogger } from "@techstream/quark-core";
132
+
133
+ const log = createLogger("action:product");
134
+
135
+ const createProductSchema = z.object({
136
+ name: z.string().min(1).max(255),
137
+ slug: z.string().min(1).regex(/^[a-z0-9-]+$/),
138
+ description: z.string().optional(),
139
+ basePrice: z.coerce.number().positive(),
140
+ categoryId: z.string().optional(),
141
+ images: z.array(z.string().url()).optional(),
142
+ });
143
+
144
+ export async function createProduct(formData) {
145
+ const parsed = createProductSchema.safeParse(Object.fromEntries(formData));
146
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
147
+
148
+ const existing = await prisma.product.findUnique({ where: { slug: parsed.data.slug } });
149
+ if (existing) throw new AppError("Slug already in use", 409, "CONFLICT");
150
+
151
+ const product = await prisma.product.create({ data: parsed.data });
152
+ log.info("product created", { id: product.id, slug: product.slug });
153
+ return product;
154
+ }
155
+
156
+ const createVariantSchema = z.object({
157
+ productId: z.string().min(1),
158
+ sku: z.string().min(1).max(100),
159
+ name: z.string().min(1).max(200),
160
+ price: z.coerce.number().positive(),
161
+ stock: z.coerce.number().int().nonnegative(),
162
+ });
163
+
164
+ export async function createVariant(formData) {
165
+ const parsed = createVariantSchema.safeParse(Object.fromEntries(formData));
166
+ if (!parsed.success) throw new ValidationError(parsed.error.flatten());
167
+
168
+ const existing = await prisma.productVariant.findUnique({ where: { sku: parsed.data.sku } });
169
+ if (existing) throw new AppError("SKU already in use", 409, "CONFLICT");
170
+
171
+ const variant = await prisma.productVariant.create({ data: parsed.data });
172
+ log.info("variant created", { id: variant.id, sku: variant.sku });
173
+ return variant;
174
+ }
175
+ ```
176
+
177
+ ## End result
178
+
179
+ A catalog where admins can manage products, variants, and categories — with slug-based lookups, hierarchical categories, and query helpers for the storefront.