@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.
- package/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- 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.
|