@rovela-ai/sdk 0.19.4 → 0.19.5
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/docs/changelog/README.md +53 -0
- package/docs/changelog/epoch-01-chrome-consent.md +63 -0
- package/docs/changelog/epoch-02-platform-commerce.md +108 -0
- package/docs/changelog/epoch-03-i18n-files.md +72 -0
- package/docs/changelog/epoch-04-emails.md +65 -0
- package/docs/changelog/epoch-05-admin-experience.md +98 -0
- package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
- package/docs/design/admin-ledger-visual-universe.html +1961 -0
- package/docs/design/admin-sidebar-and-band.html +542 -0
- package/package.json +3 -1
- package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
- package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
- package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
- package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
- package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
- package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
- package/templates/store-template/.env.example +50 -0
- package/templates/store-template/.mcp.json +8 -0
- package/templates/store-template/.rovela/blueprint.example.json +190 -0
- package/templates/store-template/CLAUDE.md +14 -0
- package/templates/store-template/app/LayoutContent.tsx +106 -0
- package/templates/store-template/app/[...path]/page.tsx +79 -0
- package/templates/store-template/app/account/addresses/page.tsx +45 -0
- package/templates/store-template/app/account/layout.tsx +17 -0
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
- package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
- package/templates/store-template/app/account/orders/page.tsx +153 -0
- package/templates/store-template/app/account/page.tsx +184 -0
- package/templates/store-template/app/account/profile/page.tsx +434 -0
- package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
- package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
- package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
- package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
- package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
- package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
- package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
- package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
- package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
- package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
- package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
- package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
- package/templates/store-template/app/admin/layout.tsx +47 -0
- package/templates/store-template/app/admin/login/page.tsx +528 -0
- package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
- package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
- package/templates/store-template/app/api/account/addresses/route.ts +10 -0
- package/templates/store-template/app/api/account/change-password/route.ts +107 -0
- package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
- package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
- package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
- package/templates/store-template/app/api/account/orders/route.ts +29 -0
- package/templates/store-template/app/api/account/profile/route.ts +165 -0
- package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
- package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
- package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
- package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/route.ts +9 -0
- package/templates/store-template/app/api/admin/check/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/customers/route.ts +9 -0
- package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
- package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
- package/templates/store-template/app/api/admin/emails/route.ts +5 -0
- package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
- package/templates/store-template/app/api/admin/files/route.ts +8 -0
- package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
- package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
- package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/locations/route.ts +10 -0
- package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
- package/templates/store-template/app/api/admin/me/route.ts +11 -0
- package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/orders/route.ts +9 -0
- package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
- package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
- package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/admin/settings/route.ts +11 -0
- package/templates/store-template/app/api/admin/setup/route.ts +10 -0
- package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
- package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
- package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
- package/templates/store-template/app/api/admin/stats/route.ts +9 -0
- package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
- package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
- package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
- package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
- package/templates/store-template/app/api/admin/users/route.ts +12 -0
- package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
- package/templates/store-template/app/api/analytics/track/route.ts +12 -0
- package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
- package/templates/store-template/app/api/auth/register/route.ts +9 -0
- package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
- package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
- package/templates/store-template/app/api/categories/route.ts +10 -0
- package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
- package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
- package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
- package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
- package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
- package/templates/store-template/app/api/checkout/route.ts +10 -0
- package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
- package/templates/store-template/app/api/checkout/status/route.ts +10 -0
- package/templates/store-template/app/api/contact/route.ts +164 -0
- package/templates/store-template/app/api/media/presign/route.ts +7 -0
- package/templates/store-template/app/api/media/route.ts +7 -0
- package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
- package/templates/store-template/app/api/products/route.ts +10 -0
- package/templates/store-template/app/api/store/settings/route.ts +12 -0
- package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
- package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
- package/templates/store-template/app/auth/layout.tsx +17 -0
- package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
- package/templates/store-template/app/auth/signin/page.tsx +62 -0
- package/templates/store-template/app/auth/signup/page.tsx +38 -0
- package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
- package/templates/store-template/app/cart/page.tsx +137 -0
- package/templates/store-template/app/checkout/page.tsx +88 -0
- package/templates/store-template/app/checkout/success/page.tsx +50 -0
- package/templates/store-template/app/contact/layout.tsx +24 -0
- package/templates/store-template/app/contact/page.tsx +321 -0
- package/templates/store-template/app/faq/page.tsx +153 -0
- package/templates/store-template/app/globals.css +99 -0
- package/templates/store-template/app/layout.tsx +269 -0
- package/templates/store-template/app/not-found.tsx +35 -0
- package/templates/store-template/app/page.tsx +23 -0
- package/templates/store-template/app/privacy/page.tsx +281 -0
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
- package/templates/store-template/app/products/[slug]/page.tsx +149 -0
- package/templates/store-template/app/products/layout.tsx +26 -0
- package/templates/store-template/app/products/page.tsx +102 -0
- package/templates/store-template/app/robots.ts +25 -0
- package/templates/store-template/app/sitemap.ts +76 -0
- package/templates/store-template/app/terms/page.tsx +313 -0
- package/templates/store-template/components/PathSyncBridge.tsx +61 -0
- package/templates/store-template/components/Providers.tsx +37 -0
- package/templates/store-template/components/layout/Footer.tsx +150 -0
- package/templates/store-template/components/layout/Header.tsx +162 -0
- package/templates/store-template/components/sections/.gitkeep +7 -0
- package/templates/store-template/components/sections/CTA.tsx +51 -0
- package/templates/store-template/components/sections/Features.tsx +91 -0
- package/templates/store-template/components/sections/Hero.tsx +85 -0
- package/templates/store-template/eslint.config.mjs +18 -0
- package/templates/store-template/lib/store-config.ts +77 -0
- package/templates/store-template/lib/store-policy.ts +68 -0
- package/templates/store-template/lib/utils.ts +21 -0
- package/templates/store-template/next-env.d.ts +6 -0
- package/templates/store-template/next.config.js +105 -0
- package/templates/store-template/package.json +32 -0
- package/templates/store-template/postcss.config.js +6 -0
- package/templates/store-template/proxy.ts +150 -0
- package/templates/store-template/public/rovela-logo.png +0 -0
- package/templates/store-template/tailwind.config.ts +74 -0
- package/templates/store-template/tsconfig.json +41 -0
- package/templates/store-template/types/next-auth.d.ts +32 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-section
|
|
3
|
+
description: Use when the user wants to add a new section to the homepage or any page. Covers component creation, page.tsx integration, responsive layout, and content from blueprint.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add Homepage Section
|
|
7
|
+
|
|
8
|
+
> **Languages**: before writing any user-visible copy, check the store's
|
|
9
|
+
> languages (`SELECT default_locale, enabled_locales FROM store_settings`).
|
|
10
|
+
> Multilingual stores need every new string in ALL enabled languages via the
|
|
11
|
+
> store dictionary — see CLAUDE.md §5.5 "Languages & translations". Internal
|
|
12
|
+
> links always use `LocaleLink` from `@rovela-ai/sdk/core`, never raw
|
|
13
|
+
> `next/link`.
|
|
14
|
+
|
|
15
|
+
## Step-by-Step Process
|
|
16
|
+
|
|
17
|
+
### 1. Create the Component
|
|
18
|
+
|
|
19
|
+
Create a new file at `/app/components/sections/SectionName.tsx`:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
'use client';
|
|
23
|
+
|
|
24
|
+
export default function SectionName() {
|
|
25
|
+
return (
|
|
26
|
+
<section className="py-16 md:py-24 px-4">
|
|
27
|
+
<div className="max-w-7xl mx-auto">
|
|
28
|
+
{/* Section content */}
|
|
29
|
+
</div>
|
|
30
|
+
</section>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add `'use client'` only if the component uses hooks (`useState`, `useEffect`) or event handlers (`onClick`, `onSubmit`). Pure display sections don't need it.
|
|
36
|
+
|
|
37
|
+
### 2. Import in page.tsx
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// In /app/app/page.tsx
|
|
41
|
+
import SectionName from '@/components/sections/SectionName';
|
|
42
|
+
|
|
43
|
+
// Add below existing sections
|
|
44
|
+
<SectionName />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 3. Verify
|
|
48
|
+
|
|
49
|
+
Run error check (get_errors) to confirm no TypeScript issues.
|
|
50
|
+
|
|
51
|
+
## Content Sources
|
|
52
|
+
|
|
53
|
+
- Use exact content from `/app/.rovela/blueprint.json` for headlines, descriptions, testimonials
|
|
54
|
+
- Hardcode content directly in the component (not runtime reads)
|
|
55
|
+
- For images: call `mcp__r2__generate_image` directly in this session (never via Task/Agent — subagents don't inherit MCP). Write descriptive scene prompts (40-75 words) using photography terminology. Use `imageUrl` from blueprint if already populated. A CSS gradient is acceptable per-image ONLY when the tool returns `{ success: false }` (Gemini API error/safety block) for that call — never as a substitute for skipping
|
|
56
|
+
- Never hardcode external image URLs — always generate or use blueprint data
|
|
57
|
+
|
|
58
|
+
## Header Customization
|
|
59
|
+
|
|
60
|
+
The header is the FIRST thing visitors see. It MUST reflect the store's brand personality — never leave it as a generic white bar with gray text.
|
|
61
|
+
|
|
62
|
+
**Read the Visual Creative Brief in CLAUDE.md** before customizing the header.
|
|
63
|
+
|
|
64
|
+
### Solid Header (default)
|
|
65
|
+
|
|
66
|
+
Use when the hero does NOT have a full-bleed background image:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
70
|
+
<nav className="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
71
|
+
<div className="flex h-16 items-center justify-between">
|
|
72
|
+
{/* Logo: use font-heading, brand-appropriate weight */}
|
|
73
|
+
<Link href="/" className="text-xl font-bold font-heading tracking-tight">
|
|
74
|
+
{storeName}
|
|
75
|
+
</Link>
|
|
76
|
+
|
|
77
|
+
{/* Nav links: use brand-appropriate hover color */}
|
|
78
|
+
<div className="hidden md:flex items-center gap-8">
|
|
79
|
+
{navigation.map((item) => (
|
|
80
|
+
<Link key={item.name} href={item.href}
|
|
81
|
+
className="text-sm font-medium text-muted-foreground transition-colors hover:text-primary">
|
|
82
|
+
{item.name}
|
|
83
|
+
</Link>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Actions */}
|
|
88
|
+
<div className="flex items-center gap-3">
|
|
89
|
+
<CartIcon showCount />
|
|
90
|
+
{/* Mobile menu toggle */}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</nav>
|
|
94
|
+
</header>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Transparent Header (for hero with background image)
|
|
98
|
+
|
|
99
|
+
Use when hero has `imageUrl` and layout is `fullscreen` or `center`:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
'use client';
|
|
103
|
+
import { useState, useEffect } from 'react';
|
|
104
|
+
|
|
105
|
+
export function Header() {
|
|
106
|
+
const [scrolled, setScrolled] = useState(false);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const onScroll = () => setScrolled(window.scrollY > 50);
|
|
110
|
+
window.addEventListener('scroll', onScroll);
|
|
111
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<header className={cn(
|
|
116
|
+
'fixed top-0 z-50 w-full transition-all duration-300',
|
|
117
|
+
scrolled
|
|
118
|
+
? 'bg-background/95 backdrop-blur border-b border-border/40 shadow-sm'
|
|
119
|
+
: 'bg-transparent'
|
|
120
|
+
)}>
|
|
121
|
+
<nav className="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
122
|
+
<div className="flex h-16 items-center justify-between">
|
|
123
|
+
<Link href="/" className={cn(
|
|
124
|
+
'text-xl font-bold font-heading tracking-tight transition-colors',
|
|
125
|
+
scrolled ? 'text-foreground' : 'text-white'
|
|
126
|
+
)}>
|
|
127
|
+
{storeName}
|
|
128
|
+
</Link>
|
|
129
|
+
{/* Nav links: same transition pattern for text color */}
|
|
130
|
+
</div>
|
|
131
|
+
</nav>
|
|
132
|
+
</header>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
When using transparent header, add `pt-0` to the hero section (header overlaps it), NOT `mt-16`.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Hero Section Patterns
|
|
142
|
+
|
|
143
|
+
The hero is the most visually impactful section. Blueprint data is at `blueprint.homepage.hero`.
|
|
144
|
+
|
|
145
|
+
**Required data points to implement:**
|
|
146
|
+
- `headline.text` + `headline.emphasis[]` (words to style differently)
|
|
147
|
+
- `subheadline.text`
|
|
148
|
+
- `cta.primary` + `cta.secondary` (button text, variant, link)
|
|
149
|
+
- `imageUrl` (from blueprint, or generated via `mcp__r2__generate_image` called directly in this session — per-image gradient fallback ONLY if that specific call returns `{ success: false }`)
|
|
150
|
+
- `socialProof` (optional stats bar)
|
|
151
|
+
- `layout` (split, center, fullscreen)
|
|
152
|
+
|
|
153
|
+
### Split Layout (most common)
|
|
154
|
+
|
|
155
|
+
Two columns: text left, image right. Stacks on mobile.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<section className="relative py-20 lg:py-28">
|
|
159
|
+
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
|
160
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
|
|
161
|
+
{/* Text column */}
|
|
162
|
+
<div className="max-w-xl">
|
|
163
|
+
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold font-heading tracking-tight leading-[1.1]">
|
|
164
|
+
{/* Render emphasis words with accent color */}
|
|
165
|
+
Barefoot Shoes for{' '}
|
|
166
|
+
<span className="text-primary">Natural Movement</span>
|
|
167
|
+
</h1>
|
|
168
|
+
<p className="mt-6 text-lg text-muted-foreground leading-relaxed">
|
|
169
|
+
{subheadline}
|
|
170
|
+
</p>
|
|
171
|
+
<div className="mt-8 flex flex-wrap items-center gap-4">
|
|
172
|
+
<Link href={cta.primary.link} className="inline-flex items-center px-7 py-3.5 rounded-lg bg-primary text-primary-foreground font-semibold text-sm shadow-sm hover:bg-primary/90 transition-colors">
|
|
173
|
+
{cta.primary.text}
|
|
174
|
+
</Link>
|
|
175
|
+
{cta.secondary && (
|
|
176
|
+
<Link href={cta.secondary.link} className="inline-flex items-center px-7 py-3.5 rounded-lg border border-border font-semibold text-sm hover:bg-accent transition-colors">
|
|
177
|
+
{cta.secondary.text}
|
|
178
|
+
</Link>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Social proof (if exists in blueprint) */}
|
|
183
|
+
{socialProof && (
|
|
184
|
+
<div className="mt-10 flex items-center gap-8 border-t border-border/50 pt-8">
|
|
185
|
+
{socialProof.content.map((stat) => (
|
|
186
|
+
<div key={stat.label}>
|
|
187
|
+
<div className="text-2xl font-bold font-heading">{stat.value}</div>
|
|
188
|
+
<div className="text-xs text-muted-foreground">{stat.label}</div>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Image column */}
|
|
196
|
+
<div className="relative aspect-[4/5] lg:aspect-[3/4] rounded-2xl overflow-hidden">
|
|
197
|
+
{imageUrl ? (
|
|
198
|
+
<img src={imageUrl} alt={headline} className="absolute inset-0 w-full h-full object-cover" />
|
|
199
|
+
) : (
|
|
200
|
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 via-accent/10 to-secondary/20" />
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</section>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Center Layout
|
|
209
|
+
|
|
210
|
+
Full-width centered text. Image as subtle background or below text.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
<section className="relative py-24 lg:py-36">
|
|
214
|
+
{/* Optional background image with overlay */}
|
|
215
|
+
{imageUrl && (
|
|
216
|
+
<>
|
|
217
|
+
<img src={imageUrl} alt="" className="absolute inset-0 w-full h-full object-cover" />
|
|
218
|
+
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" />
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<div className="relative container mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
223
|
+
<h1 className="text-4xl sm:text-5xl lg:text-7xl font-bold font-heading tracking-tight leading-[1.1] max-w-4xl mx-auto">
|
|
224
|
+
{/* Emphasis words */}
|
|
225
|
+
Crafted for <span className="text-primary">Comfort</span>
|
|
226
|
+
</h1>
|
|
227
|
+
<p className="mt-6 text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
|
228
|
+
{subheadline}
|
|
229
|
+
</p>
|
|
230
|
+
<div className="mt-10 flex items-center justify-center gap-4">
|
|
231
|
+
<Link href={cta.primary.link} className="inline-flex items-center px-8 py-4 rounded-lg bg-primary text-primary-foreground font-semibold shadow-sm hover:bg-primary/90 transition-colors">
|
|
232
|
+
{cta.primary.text}
|
|
233
|
+
</Link>
|
|
234
|
+
{cta.secondary && (
|
|
235
|
+
<Link href={cta.secondary.link} className="inline-flex items-center px-8 py-4 rounded-lg border border-border font-semibold hover:bg-accent transition-colors">
|
|
236
|
+
{cta.secondary.text}
|
|
237
|
+
</Link>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{/* Social proof centered */}
|
|
242
|
+
{socialProof && (
|
|
243
|
+
<div className="mt-14 flex items-center justify-center gap-10 text-center">
|
|
244
|
+
{socialProof.content.map((stat) => (
|
|
245
|
+
<div key={stat.label}>
|
|
246
|
+
<div className="text-3xl font-bold font-heading">{stat.value}</div>
|
|
247
|
+
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
|
248
|
+
</div>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Fullscreen Layout
|
|
257
|
+
|
|
258
|
+
Viewport-height hero with background image. Use transparent header (see above).
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
<section className="relative min-h-[90vh] flex items-end pb-20 lg:pb-28">
|
|
262
|
+
{/* Background image */}
|
|
263
|
+
{imageUrl ? (
|
|
264
|
+
<>
|
|
265
|
+
<img src={imageUrl} alt="" className="absolute inset-0 w-full h-full object-cover" />
|
|
266
|
+
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent" />
|
|
267
|
+
</>
|
|
268
|
+
) : (
|
|
269
|
+
<div className="absolute inset-0 bg-gradient-to-br from-primary/30 via-background to-accent/20" />
|
|
270
|
+
)}
|
|
271
|
+
|
|
272
|
+
<div className="relative container mx-auto px-4 sm:px-6 lg:px-8">
|
|
273
|
+
<div className="max-w-2xl">
|
|
274
|
+
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold font-heading tracking-tight leading-[1.1] text-foreground">
|
|
275
|
+
{headline}
|
|
276
|
+
</h1>
|
|
277
|
+
<p className="mt-6 text-lg text-muted-foreground leading-relaxed">
|
|
278
|
+
{subheadline}
|
|
279
|
+
</p>
|
|
280
|
+
<div className="mt-8 flex flex-wrap items-center gap-4">
|
|
281
|
+
<Link href={cta.primary.link} className="inline-flex items-center px-7 py-3.5 rounded-lg bg-primary text-primary-foreground font-semibold shadow-sm hover:bg-primary/90 transition-colors">
|
|
282
|
+
{cta.primary.text}
|
|
283
|
+
</Link>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</section>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Emphasis Words
|
|
291
|
+
|
|
292
|
+
Style words from `headline.emphasis[]` to stand out. Pick ONE approach per store:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
{/* Option 1: Brand color */}
|
|
296
|
+
<span className="text-primary">Natural</span>
|
|
297
|
+
|
|
298
|
+
{/* Option 2: Gradient text */}
|
|
299
|
+
<span className="bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">Natural</span>
|
|
300
|
+
|
|
301
|
+
{/* Option 3: Underline accent */}
|
|
302
|
+
<span className="underline decoration-primary decoration-4 underline-offset-4">Natural</span>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Hero Image Fallback
|
|
306
|
+
|
|
307
|
+
ALWAYS handle missing `imageUrl`:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
{imageUrl ? (
|
|
311
|
+
<img src={imageUrl} alt={headline} className="w-full h-full object-cover" />
|
|
312
|
+
) : (
|
|
313
|
+
<div className="w-full h-full bg-gradient-to-br from-primary/20 via-accent/10 to-secondary/20" />
|
|
314
|
+
)}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Video in Sections
|
|
320
|
+
|
|
321
|
+
Any section that takes an `imageUrl` can take a video URL instead — the merchant can paste a `.mp4` / `.webm` / `.mov` URL into chat ("use this for the hero") or store it via the admin product gallery. When you receive a media URL, decide between `<img>` and `<video>` by file extension.
|
|
322
|
+
|
|
323
|
+
**Hero / background loop** — autoplay required, `muted` and `playsInline` are non-negotiable, `prefers-reduced-motion` falls back to a still:
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
{isVideoUrl(mediaUrl) ? (
|
|
327
|
+
<video
|
|
328
|
+
src={mediaUrl}
|
|
329
|
+
autoPlay
|
|
330
|
+
muted
|
|
331
|
+
loop
|
|
332
|
+
playsInline
|
|
333
|
+
preload="metadata"
|
|
334
|
+
className="absolute inset-0 w-full h-full object-cover motion-reduce:hidden"
|
|
335
|
+
/>
|
|
336
|
+
) : (
|
|
337
|
+
<img src={mediaUrl} alt={headline} className="absolute inset-0 w-full h-full object-cover" />
|
|
338
|
+
)}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Foreground / testimonial / product demo** — explicit user control:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
<video
|
|
345
|
+
src={mediaUrl}
|
|
346
|
+
controls
|
|
347
|
+
preload="metadata"
|
|
348
|
+
className="w-full rounded-lg"
|
|
349
|
+
/>
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The `isVideoUrl` helper is exported from `@rovela-ai/sdk/media`. Or inline it: `url.match(/\.(mp4|webm|mov)(\?|#|$)/i)`.
|
|
353
|
+
|
|
354
|
+
Never embed YouTube / Vimeo iframes — out of scope.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Other Section Layout Patterns
|
|
359
|
+
|
|
360
|
+
### Feature Grid (3-4 items)
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
364
|
+
{features.map((f) => (
|
|
365
|
+
<div key={f.title} className="p-6 rounded-lg bg-card border border-border">
|
|
366
|
+
<h3 className="text-lg font-semibold">{f.title}</h3>
|
|
367
|
+
<p className="mt-2 text-muted-foreground">{f.description}</p>
|
|
368
|
+
</div>
|
|
369
|
+
))}
|
|
370
|
+
</div>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### Testimonial Cards
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
377
|
+
{testimonials.map((t) => (
|
|
378
|
+
<blockquote key={t.author} className="p-6 bg-card rounded-lg border border-border">
|
|
379
|
+
<p className="text-foreground italic">"{t.quote}"</p>
|
|
380
|
+
<footer className="mt-4 text-sm text-muted-foreground">— {t.author}</footer>
|
|
381
|
+
</blockquote>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### CTA Banner
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
<div className="text-center py-16 bg-primary text-primary-foreground rounded-2xl">
|
|
390
|
+
<h2 className="text-3xl font-bold">{headline}</h2>
|
|
391
|
+
<p className="mt-4 text-primary-foreground/80 max-w-xl mx-auto">{subheadline}</p>
|
|
392
|
+
<a href="/products" className="mt-8 inline-block px-8 py-3 bg-background text-foreground rounded-lg font-medium">
|
|
393
|
+
{buttonText}
|
|
394
|
+
</a>
|
|
395
|
+
</div>
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Valid Routes for Links
|
|
399
|
+
|
|
400
|
+
Link only to pages that exist:
|
|
401
|
+
- Storefront: `/`, `/products`, `/cart`, `/checkout`
|
|
402
|
+
- Auth: `/auth/signin`, `/auth/signup`, `/account`
|
|
403
|
+
- Legal: `/contact`, `/privacy`, `/terms`
|
|
404
|
+
|
|
405
|
+
Pages that DO NOT exist: `/about`, `/gallery`, `/pricing`, `/faq`, `/blog`, `/services`, `/team`
|
|
406
|
+
|
|
407
|
+
Use anchor links (`#features`, `#testimonials`) for same-page navigation.
|
|
408
|
+
|
|
409
|
+
## Section Type Catalog (16 types — picked from the brief's `homepageStructure`)
|
|
410
|
+
|
|
411
|
+
When the merchant asks to "add a [section type]," look up the type in this catalog and use the matching skeleton. When they don't name a type, infer from intent ("add a process explanation" → `process-reveal`; "add an about-the-founders block" → `founders-note`; "add an editorial product spread" → `lookbook-spread`).
|
|
412
|
+
|
|
413
|
+
- **`hero`** — first-impression headline + subhead + CTA. Pattern per the brief's `Hero pattern:` callout (9 patterns).
|
|
414
|
+
- **`features`** — 3-4 card grid of brand benefits. `<section className="py-16 lg:py-24">` with `grid grid-cols-1 md:grid-cols-3 gap-8`. Each card: icon + h3 (≤4 words) + p (≤16 words).
|
|
415
|
+
- **`process-reveal`** — How it's made / roasted / cured. 3-5 process steps with large step numbers + h3 + 30-50 word description. Use real verbs.
|
|
416
|
+
- **`lookbook-spread`** — Editorial product spread (uses `editorial-2col` or `bento-grid` pattern). 3-6 products with prose copy.
|
|
417
|
+
- **`manifesto-block`** — Single-column long-form brand prose. `<section className="py-32 lg:py-48 bg-muted/30">` + h2 + 60-120 word paragraph in display voice.
|
|
418
|
+
- **`founders-note`** — Founder photo + signature quote. 5-col grid: photo (col-span-2) + quote (col-span-3, font-serif italic).
|
|
419
|
+
- **`values-strip`** — Compact icon + label row. 3-5 brand values, `min-h-[8vh]` short rhythm.
|
|
420
|
+
- **`how-it-works`** — Numbered steps for services/SaaS. 3 steps with big numbers + h3 + ≤20 word descriptions.
|
|
421
|
+
- **`testimonials`** — Customer quotes. 2-4 quotes max, ≤30 words each, name specific product attributes.
|
|
422
|
+
- **`social-proof`** — Press logos OR metric row ("4.9★ from 10K reviews"). Centered grid.
|
|
423
|
+
- **`guide-spread`** — Educational content. 80-120 word intro + 2-4 guide entries with inline images.
|
|
424
|
+
- **`faq`** — Accordion. 4-6 questions, ≤12 word question + ≤30 word answer.
|
|
425
|
+
- **`newsletter-cta`** — Newsletter signup. `<section className="py-20 lg:py-24 bg-muted/30 text-center">` + h2 + 30-word body + email input + button.
|
|
426
|
+
- **`trust`** — Trust badges row. 3-4 specific commitments with lucide icons + ≤8 word body.
|
|
427
|
+
- **`cta`** — Final call-to-action. Big headline + single primary button.
|
|
428
|
+
- **`product-showcase`** — Real products from DB. Pattern per the brief's `Product showcase:` callout (6 patterns).
|
|
429
|
+
|
|
430
|
+
## Rhythm Sizes (5 sizes — variable cadence per section)
|
|
431
|
+
|
|
432
|
+
- **`xs`** — `min-h-[8vh] py-4 lg:py-6` — strip, ribbon, pull-quote
|
|
433
|
+
- **`sm`** — `py-12 lg:py-16` — trust strips, newsletter, social proof
|
|
434
|
+
- **`md`** — `py-20 lg:py-24` — default for commerce sections
|
|
435
|
+
- **`lg`** — `min-h-screen` or `py-32 lg:py-40` — hero, CTA, manifesto
|
|
436
|
+
- **`xl`** — `min-h-[150vh]` or `py-40 lg:py-64` — editorial deep dives, lookbook spreads
|
|
437
|
+
|
|
438
|
+
When adding a section, pick a rhythm that VARIES the page cadence — if surrounding sections are `md-md-md`, an inserted `xs` (strip) or `lg` (deep) breaks the rhythm and reads as designed.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-feature
|
|
3
|
+
description: Use when the user asks for a new custom feature or module (booking, reviews, loyalty, wishlist, blog, etc.) that requires new database tables, API routes, and pages.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build Custom Feature
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
1. **Create database tables** via `getSql()` and `execute_sql` MCP tool
|
|
11
|
+
2. **Build API routes** in `/app/api/` using `getSql()` for queries
|
|
12
|
+
3. **Build frontend pages** (customer-facing and/or admin)
|
|
13
|
+
4. **Update navigation** (Header, Footer, admin sidebar)
|
|
14
|
+
5. **Verify** with `get_errors` after each file
|
|
15
|
+
|
|
16
|
+
## Database Access
|
|
17
|
+
|
|
18
|
+
**CRITICAL**: The SDK's `db` export is a Drizzle ORM instance. It does NOT have `.query()`, `.execute()`, or `.run()` methods.
|
|
19
|
+
|
|
20
|
+
### For custom tables, use `getSql()`:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { getSql } from '@rovela-ai/sdk/core/server'
|
|
24
|
+
|
|
25
|
+
const sql = getSql()
|
|
26
|
+
|
|
27
|
+
// Results are arrays directly — NO .rows wrapper
|
|
28
|
+
const rows = await sql`SELECT * FROM my_table WHERE status = ${status}`
|
|
29
|
+
// rows = [{id: '...', name: '...'}, ...]
|
|
30
|
+
|
|
31
|
+
// Parameterized (safe from SQL injection)
|
|
32
|
+
await sql`INSERT INTO my_table (name, value) VALUES (${name}, ${value}) RETURNING *`
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### For existing tables, use SDK query helpers:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { findProducts, findOrders, findCustomerByEmail } from '@rovela-ai/sdk/core/server'
|
|
39
|
+
|
|
40
|
+
const products = await findProducts({ status: 'active' })
|
|
41
|
+
const order = await findOrderById(orderId)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Create Tables
|
|
45
|
+
|
|
46
|
+
Use `execute_sql` MCP tool first, then verify:
|
|
47
|
+
|
|
48
|
+
```sql
|
|
49
|
+
CREATE TABLE IF NOT EXISTS my_feature (
|
|
50
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
status TEXT DEFAULT 'active',
|
|
53
|
+
data JSONB DEFAULT '{}',
|
|
54
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
55
|
+
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then confirm with `list_objects` and `get_object_details`.
|
|
60
|
+
|
|
61
|
+
## Custom API Route Template
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// app/api/my-feature/route.ts
|
|
65
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
66
|
+
import { getSql } from '@rovela-ai/sdk/core/server'
|
|
67
|
+
|
|
68
|
+
export async function GET(request: NextRequest) {
|
|
69
|
+
try {
|
|
70
|
+
const sql = getSql()
|
|
71
|
+
const { searchParams } = new URL(request.url)
|
|
72
|
+
const status = searchParams.get('status') || 'active'
|
|
73
|
+
|
|
74
|
+
const rows = await sql`
|
|
75
|
+
SELECT * FROM my_feature WHERE status = ${status}
|
|
76
|
+
ORDER BY created_at DESC
|
|
77
|
+
`
|
|
78
|
+
return NextResponse.json({ data: rows })
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('[MyFeature] GET error:', error)
|
|
81
|
+
return NextResponse.json({ error: 'Failed to load' }, { status: 500 })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function POST(request: NextRequest) {
|
|
86
|
+
try {
|
|
87
|
+
const body = await request.json()
|
|
88
|
+
if (!body.name) {
|
|
89
|
+
return NextResponse.json({ error: 'Name is required' }, { status: 400 })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sql = getSql()
|
|
93
|
+
const result = await sql`
|
|
94
|
+
INSERT INTO my_feature (name, data)
|
|
95
|
+
VALUES (${body.name}, ${JSON.stringify(body.data || {})}::jsonb)
|
|
96
|
+
RETURNING *
|
|
97
|
+
`
|
|
98
|
+
return NextResponse.json({ data: result[0] }, { status: 201 })
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('[MyFeature] POST error:', error)
|
|
101
|
+
return NextResponse.json({ error: 'Failed to create' }, { status: 500 })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Admin Page Pattern
|
|
107
|
+
|
|
108
|
+
Admin pages go in `app/admin/(dashboard)/`. **The Admin Design Contract (CLAUDE.md §7.6) is BINDING here** — use the SDK admin kit, never rebuild it. Full visual canon (only if §7.6 leaves a question open): Grep sections of `node_modules/@rovela-ai/sdk/docs/design/admin-ledger-visual-universe.html` — never read it whole.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// app/admin/(dashboard)/my-feature/page.tsx
|
|
112
|
+
'use client'
|
|
113
|
+
|
|
114
|
+
import { useState, useEffect } from 'react'
|
|
115
|
+
import { AdminPageHeader, AdminEmptyState, adminToast } from '@rovela-ai/sdk/admin'
|
|
116
|
+
import { Calendar } from 'lucide-react'
|
|
117
|
+
|
|
118
|
+
export default function AdminMyFeaturePage() {
|
|
119
|
+
const [data, setData] = useState<MyRow[]>([])
|
|
120
|
+
const [loading, setLoading] = useState(true)
|
|
121
|
+
const [loadError, setLoadError] = useState<string | null>(null)
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
fetch('/api/admin/my-feature')
|
|
125
|
+
.then(res => res.ok ? res.json() : Promise.reject(res))
|
|
126
|
+
.then(json => setData(json.data || []))
|
|
127
|
+
.catch(() => setLoadError('Could not load. Refresh to retry.'))
|
|
128
|
+
.finally(() => setLoading(false))
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-6">
|
|
133
|
+
{/* AdminPageHeader owns the title + primary action — never a raw <h1> */}
|
|
134
|
+
<AdminPageHeader title="My feature" subtitle="One line on what this manages." />
|
|
135
|
+
<div className="admin-card admin-card-body">
|
|
136
|
+
{loadError && <div className="admin-alert-inline admin-alert-inline-error mb-4">{loadError}</div>}
|
|
137
|
+
{loading ? (
|
|
138
|
+
<div className="flex justify-center p-12"><span className="admin-spinner admin-spinner-lg" /></div>
|
|
139
|
+
) : data.length === 0 ? (
|
|
140
|
+
<AdminEmptyState icon={<Calendar />} title="Nothing here yet" description="Create your first item to get started." />
|
|
141
|
+
) : (
|
|
142
|
+
/* Tables: wrap in <div className="overflow-x-auto">, mark secondary
|
|
143
|
+
columns admin-col-optional. 2-4 column card lists: stacked rows,
|
|
144
|
+
not a table. Admin tokens only (hsl(var(--admin-*)) / admin-*
|
|
145
|
+
classes) — NEVER storefront tokens like text-muted-foreground. */
|
|
146
|
+
<div className="overflow-x-auto">{/* rows */}</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Non-negotiables on every admin page you build:
|
|
155
|
+
- **Spinner**: the `admin-spinner` class — never `Loader2` or any animated lucide icon.
|
|
156
|
+
- **Events → `adminToast.success/error`** (deleted, sent, saved-transactionally, failed); **states → inline `admin-alert`**. Never both for one outcome.
|
|
157
|
+
- **Dialogs → `AdminModal`; deletes → `DeleteConfirmDialog`** — never native `confirm()`/`alert()`, never a hand-rolled fixed overlay, never inline "Delete? Yes/No".
|
|
158
|
+
- **Document-shaped forms** (settings-like editing of an existing entity) save through `AdminSaveBar` + `useDirtyState`; transactional create/edit keeps inline buttons.
|
|
159
|
+
- **Money**: decimal currency units (`29.99`), never cents. **Dates**: `useAdminDates` (store timezone). **Orders**: display in `order.currency`.
|
|
160
|
+
- **375px**: responsive grid prefixes, no fixed widths over ~340px.
|
|
161
|
+
|
|
162
|
+
## Admin Sidebar Navigation
|
|
163
|
+
|
|
164
|
+
When your feature has an admin page, you MUST add it to the admin sidebar.
|
|
165
|
+
|
|
166
|
+
Edit `app/admin/(dashboard)/layout.tsx` to pass `navItems` to `AdminLayout`:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { Calendar } from 'lucide-react'
|
|
170
|
+
|
|
171
|
+
<AdminLayout
|
|
172
|
+
navItems={[
|
|
173
|
+
{ id: 'my-feature', label: 'My Feature', href: '/admin/my-feature', icon: <Calendar className="h-4 w-4" /> },
|
|
174
|
+
]}
|
|
175
|
+
>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
If `navItems` already exists, add your item to the existing array.
|
|
179
|
+
Active state is auto-detected from the URL — no extra config needed.
|
|
180
|
+
|
|
181
|
+
## Common Mistakes
|
|
182
|
+
|
|
183
|
+
| Mistake | Fix |
|
|
184
|
+
|---------|-----|
|
|
185
|
+
| `db.query(sql)` | Use `getSql()` tagged templates |
|
|
186
|
+
| `db.execute(sql)` | Use `getSql()` tagged templates |
|
|
187
|
+
| `result.rows[0]` | `result[0]` — no `.rows` wrapper |
|
|
188
|
+
| Missing `?.` on API responses | Always check `res.ok` before accessing data |
|
|
189
|
+
| Admin API auth check with `getServerSession` | Admin pages are already protected by `AdminGuard` layout |
|
|
190
|
+
| Forgot admin sidebar item | If feature has admin page, MUST add `navItems` to AdminLayout in layout.tsx |
|
|
191
|
+
|
|
192
|
+
## Checklist
|
|
193
|
+
|
|
194
|
+
- [ ] Tables created and verified with `list_objects`
|
|
195
|
+
- [ ] API routes return correct data (test with `execute_sql` for comparison)
|
|
196
|
+
- [ ] Frontend fetches from real API (not hardcoded data)
|
|
197
|
+
- [ ] Error states handled (loading, empty, error)
|
|
198
|
+
- [ ] `get_errors` returns zero errors
|
|
199
|
+
- [ ] Customer navigation updated (Header/Footer links)
|
|
200
|
+
- [ ] Admin sidebar updated if feature has admin page (`navItems` in layout.tsx)
|
|
201
|
+
- [ ] Mobile responsive (no fixed widths)
|