@sellhapi/mcp 0.0.0-stage → 0.1.1
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 +142 -2
- package/dist/client.d.ts +33 -0
- package/dist/client.js +99 -0
- package/dist/hosted.d.ts +25 -0
- package/dist/hosted.js +234 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +54 -0
- package/dist/resources/build-rules.d.ts +5 -0
- package/dist/resources/build-rules.js +151 -0
- package/dist/resources/capability-patterns.d.ts +15 -0
- package/dist/resources/capability-patterns.js +339 -0
- package/dist/resources/design-direction-schema.d.ts +11 -0
- package/dist/resources/design-direction-schema.js +252 -0
- package/dist/resources/index.d.ts +9 -0
- package/dist/resources/index.js +55 -0
- package/dist/resources/runtime-hooks.d.ts +5 -0
- package/dist/resources/runtime-hooks.js +269 -0
- package/dist/resources/wireframe-schema.d.ts +8 -0
- package/dist/resources/wireframe-schema.js +220 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +27 -0
- package/dist/tools/commerce.d.ts +9 -0
- package/dist/tools/commerce.js +86 -0
- package/dist/tools/content.d.ts +8 -0
- package/dist/tools/content.js +49 -0
- package/dist/tools/intelligence.d.ts +5 -0
- package/dist/tools/intelligence.js +39 -0
- package/dist/tools/studio.d.ts +5 -0
- package/dist/tools/studio.js +342 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.js +21 -0
- package/package.json +36 -4
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BuildGuard rules — extracted from buildGuard.service.ts.
|
|
4
|
+
* Code that violates these rules will be rejected by push_code.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.BUILD_RULES = void 0;
|
|
8
|
+
exports.BUILD_RULES = `
|
|
9
|
+
# SellHapi BuildGuard Rules
|
|
10
|
+
|
|
11
|
+
Your code MUST pass all these rules or push_code will reject it with specific violation details.
|
|
12
|
+
|
|
13
|
+
## APPROVED IMPORTS (only these are allowed)
|
|
14
|
+
|
|
15
|
+
Exact matches:
|
|
16
|
+
@sellhapi/studio-runtime
|
|
17
|
+
react
|
|
18
|
+
react-dom
|
|
19
|
+
react-dom/client
|
|
20
|
+
react/jsx-runtime
|
|
21
|
+
react/jsx-dev-runtime
|
|
22
|
+
framer-motion
|
|
23
|
+
clsx
|
|
24
|
+
date-fns
|
|
25
|
+
zod
|
|
26
|
+
lucide-react
|
|
27
|
+
|
|
28
|
+
Subpath imports allowed:
|
|
29
|
+
date-fns/*
|
|
30
|
+
react-dom/*
|
|
31
|
+
react/*
|
|
32
|
+
|
|
33
|
+
Relative imports (./ and ../) are always allowed between project files.
|
|
34
|
+
|
|
35
|
+
EVERYTHING ELSE IS BLOCKED — no axios, no tailwindcss, no lodash, no node builtins.
|
|
36
|
+
|
|
37
|
+
## HARD VIOLATIONS (build fails)
|
|
38
|
+
|
|
39
|
+
1. DISALLOWED IMPORTS
|
|
40
|
+
Any import not in the approved list above.
|
|
41
|
+
Violation: "Import from X is not allowed."
|
|
42
|
+
|
|
43
|
+
2. DYNAMIC REQUIRE
|
|
44
|
+
require() with non-string argument or unapproved package.
|
|
45
|
+
Violation: "Dynamic require() is not allowed."
|
|
46
|
+
|
|
47
|
+
3. DYNAMIC IMPORT
|
|
48
|
+
import() expressions (as opposed to static import statements).
|
|
49
|
+
Violation: "Dynamic import() is not allowed in generated storefront code."
|
|
50
|
+
|
|
51
|
+
4. CODE INJECTION
|
|
52
|
+
eval(), new Function(), Function() — all blocked.
|
|
53
|
+
Violation: "eval()/Function() is not allowed — code injection risk."
|
|
54
|
+
|
|
55
|
+
5. RAW NETWORK ACCESS
|
|
56
|
+
fetch(), new XMLHttpRequest(), new WebSocket() — all blocked.
|
|
57
|
+
Violation: "Direct fetch() calls are not allowed. Use hooks from @sellhapi/studio-runtime instead."
|
|
58
|
+
All data must flow through runtime hooks.
|
|
59
|
+
|
|
60
|
+
6. DANGEROUS GLOBALS
|
|
61
|
+
Blocked property access:
|
|
62
|
+
- process.env
|
|
63
|
+
- document.cookie, document.write(), document.writeln(), document.domain, document.execCommand()
|
|
64
|
+
- window.fetch, window.eval, window.Function
|
|
65
|
+
- window.XMLHttpRequest, window.WebSocket
|
|
66
|
+
- window.localStorage, window.sessionStorage, window.indexedDB
|
|
67
|
+
- navigator.sendBeacon
|
|
68
|
+
- Standalone: localStorage, sessionStorage, indexedDB
|
|
69
|
+
Violation: "X is not allowed in storefront code."
|
|
70
|
+
|
|
71
|
+
7. SUBSCRIPT ACCESS ON GLOBALS
|
|
72
|
+
window["fetch"], globalThis["eval"], self["XMLHttpRequest"] — all blocked.
|
|
73
|
+
Violation: "Subscript access on 'window' is not allowed."
|
|
74
|
+
|
|
75
|
+
8. DANGEROUS JSX
|
|
76
|
+
dangerouslySetInnerHTML — blocked.
|
|
77
|
+
Violation: "dangerouslySetInnerHTML is not allowed — XSS vector."
|
|
78
|
+
|
|
79
|
+
9. MISSING CART DRAWER
|
|
80
|
+
If any file uses useCart or addItem, <CartDrawer /> MUST be rendered somewhere.
|
|
81
|
+
Violation: "useCart/addItem is used but <CartDrawer /> is not rendered anywhere."
|
|
82
|
+
|
|
83
|
+
## SOFT WARNINGS (build passes, but flagged)
|
|
84
|
+
|
|
85
|
+
1. product.images[N].url — WRONG. images is string[], each element IS the URL.
|
|
86
|
+
Fix: use images[N] directly or product.imageUrl for primary.
|
|
87
|
+
|
|
88
|
+
2. addItem() without type field — CartItem.type is required.
|
|
89
|
+
Fix: add type: 'physical' | 'digital' to every addItem() call.
|
|
90
|
+
|
|
91
|
+
3. campaign.raised / campaign.goalAmount — WRONG field names.
|
|
92
|
+
Fix: use campaign.raisedAmountKobo / campaign.goalAmountKobo (divide by 100 for display).
|
|
93
|
+
|
|
94
|
+
4. ticketType.soldCount — WRONG. Use ticketType.sold.
|
|
95
|
+
|
|
96
|
+
## REQUIRED: data-region ATTRIBUTES
|
|
97
|
+
|
|
98
|
+
Every section that renders live data MUST have a data-region attribute on its outermost container element.
|
|
99
|
+
This is required for Studio's region selector, Layers panel, and entity injection to work.
|
|
100
|
+
|
|
101
|
+
Required attributes:
|
|
102
|
+
data-region="<slug>" — unique kebab-case identifier, e.g. "hero", "products", "appointments"
|
|
103
|
+
data-label="<name>" — human-readable label shown in Studio UI, e.g. "Products Section"
|
|
104
|
+
data-scope="content" — always "content" for data-driven sections
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
<section data-region="products" data-label="Products" data-scope="content">
|
|
108
|
+
{/* useProducts() renders here */}
|
|
109
|
+
</section>
|
|
110
|
+
|
|
111
|
+
<section data-region="appointments" data-label="Consultations" data-scope="content">
|
|
112
|
+
{/* useServices() renders here */}
|
|
113
|
+
</section>
|
|
114
|
+
|
|
115
|
+
ALSO add data-region to static layout sections (hero, footer, nav) — these don't need data hooks but must still be selectable:
|
|
116
|
+
<section data-region="hero" data-label="Hero" data-scope="layout">
|
|
117
|
+
<header data-region="navigation" data-label="Navigation" data-scope="navigation">
|
|
118
|
+
<footer data-region="footer" data-label="Footer" data-scope="layout">
|
|
119
|
+
|
|
120
|
+
If you use ANY data hook (useProducts, useServices, useEvents, etc.) without ANY data-region attributes, the build will warn you.
|
|
121
|
+
|
|
122
|
+
## FILE STRUCTURE
|
|
123
|
+
|
|
124
|
+
Required: App.tsx (or App.jsx) as entry point.
|
|
125
|
+
Recommended structure:
|
|
126
|
+
App.tsx — main component with routing
|
|
127
|
+
components/ — reusable UI components
|
|
128
|
+
pages/ — route-level pages (ProductPage, EventPage, etc.)
|
|
129
|
+
styles/ — optional shared styles
|
|
130
|
+
|
|
131
|
+
## BUNDLE SIZE LIMITS
|
|
132
|
+
|
|
133
|
+
Hard limit: 4 MB (build fails above this)
|
|
134
|
+
Warning: 3.5 MB (runtime baseline is ~2.1 MB — aim to keep user code under 1.4 MB)
|
|
135
|
+
|
|
136
|
+
## STYLING
|
|
137
|
+
|
|
138
|
+
Use inline styles (React style objects) — this is the convention.
|
|
139
|
+
CSS modules work but are not typical.
|
|
140
|
+
Tailwind is NOT available (not in approved imports).
|
|
141
|
+
framer-motion is available for animations.
|
|
142
|
+
|
|
143
|
+
## KEY PATTERNS
|
|
144
|
+
|
|
145
|
+
1. All data comes from hooks. NEVER hardcode product names, prices, or content.
|
|
146
|
+
2. Show loading states: if (loading) return <div>Loading...</div>
|
|
147
|
+
3. Show empty states: if (data.length === 0) return <div>No items yet</div>
|
|
148
|
+
4. Format prices: (priceKobo / 100).toLocaleString() for Naira display
|
|
149
|
+
5. Use useHashRouter() for multi-page navigation
|
|
150
|
+
6. Commerce actions return { paymentLink } — redirect: window.location.href = result.paymentLink
|
|
151
|
+
`;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-capability UX patterns, anti-patterns, and data shape rules.
|
|
3
|
+
*
|
|
4
|
+
* Previously a hand-maintained static string — now fetched live from sell_api
|
|
5
|
+
* which serializes STUDIO_PROMPT_REGISTRY on every request. Zero drift.
|
|
6
|
+
*
|
|
7
|
+
* FALLBACK_PATTERNS is used when the API call fails (e.g. network error,
|
|
8
|
+
* server cold-starting). It reflects the last known good state and should
|
|
9
|
+
* be updated whenever the registry diverges significantly.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Fetch live capability patterns from sell_api.
|
|
13
|
+
* Falls back to FALLBACK_PATTERNS on any error — never throws.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildCapabilityPatterns(): Promise<string>;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Per-capability UX patterns, anti-patterns, and data shape rules.
|
|
4
|
+
*
|
|
5
|
+
* Previously a hand-maintained static string — now fetched live from sell_api
|
|
6
|
+
* which serializes STUDIO_PROMPT_REGISTRY on every request. Zero drift.
|
|
7
|
+
*
|
|
8
|
+
* FALLBACK_PATTERNS is used when the API call fails (e.g. network error,
|
|
9
|
+
* server cold-starting). It reflects the last known good state and should
|
|
10
|
+
* be updated whenever the registry diverges significantly.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.buildCapabilityPatterns = buildCapabilityPatterns;
|
|
14
|
+
const client_js_1 = require("../client.js");
|
|
15
|
+
/**
|
|
16
|
+
* Fetch live capability patterns from sell_api.
|
|
17
|
+
* Falls back to FALLBACK_PATTERNS on any error — never throws.
|
|
18
|
+
*/
|
|
19
|
+
async function buildCapabilityPatterns() {
|
|
20
|
+
try {
|
|
21
|
+
const data = await (0, client_js_1.get)("/api/mcp/capability-patterns");
|
|
22
|
+
if (data?.patterns)
|
|
23
|
+
return data.patterns;
|
|
24
|
+
return FALLBACK_PATTERNS;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return FALLBACK_PATTERNS;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Last-known-good static fallback. Used when the API is unreachable. */
|
|
31
|
+
const FALLBACK_PATTERNS = `
|
|
32
|
+
# SellHapi Capability Patterns
|
|
33
|
+
|
|
34
|
+
## PHYSICAL PRODUCTS
|
|
35
|
+
|
|
36
|
+
Route: #/products (list) + product quick-view modal on card click
|
|
37
|
+
Hooks: useProducts(options?), useProduct(id), useCart(), useCartCheckout()
|
|
38
|
+
|
|
39
|
+
useProducts default limit is 6. Always pass an explicit limit:
|
|
40
|
+
- Homepage preview: useProducts({ limit: 4 }) — show a teaser, not the full catalog
|
|
41
|
+
- Full products page: useProducts({ limit: 200 }) — load the full catalog
|
|
42
|
+
|
|
43
|
+
MANDATORY: Product list is on a dedicated #/products page (not homepage).
|
|
44
|
+
|
|
45
|
+
MANDATORY: Homepage product section is a TEASER — visually distinct from the full products page.
|
|
46
|
+
It is an editorial/marketing section: large dominant images, 2-col or 4-col cards, product name + price only.
|
|
47
|
+
End it with a "Shop All Products →" button/link that navigates to #/products.
|
|
48
|
+
Do NOT reuse the same component as the /products page grid — different layout, different visual weight.
|
|
49
|
+
Example structure:
|
|
50
|
+
<section> {/* homepage teaser */}
|
|
51
|
+
<h2>Our Collection</h2>
|
|
52
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 24 }}>
|
|
53
|
+
{products.slice(0, 4).map(p => <HomepageProductCard key={p.id} product={p} />)}
|
|
54
|
+
</div>
|
|
55
|
+
<button onClick={() => navigate('#/products')}>Shop All Products →</button>
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
MANDATORY: Product detail opens in a MODAL overlay on card click — never navigate to a separate page for product detail.
|
|
59
|
+
Modal shows: images (filtered for non-video), name, price, compareAtPriceKobo strikethrough, discountBadge,
|
|
60
|
+
variant selector (chip/pill buttons per option group), quantity stepper with stock cap,
|
|
61
|
+
"Add to Cart" button, policies section (return/shipping), reviews section.
|
|
62
|
+
After addItem(): MUST call openCart() to show the cart drawer.
|
|
63
|
+
|
|
64
|
+
PAGINATION on #/products page (load more pattern):
|
|
65
|
+
const [limit, setLimit] = React.useState(12);
|
|
66
|
+
const { data: products } = useProducts({ limit });
|
|
67
|
+
// Show "Load More" only when the returned count equals the current limit (more may exist)
|
|
68
|
+
{products?.length === limit && (
|
|
69
|
+
<button onClick={() => setLimit(l => l + 12)}>Load More</button>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
Price fields:
|
|
73
|
+
- priceKobo: main price (divide by 100 for display). BIGINT from API — always wrap in Number(): Number(product.priceKobo)
|
|
74
|
+
- originalPriceKobo: pre-discount / strikethrough price (number | undefined). Show as strikethrough when originalPriceKobo > priceKobo.
|
|
75
|
+
- discountBadge: pre-formatted sale label e.g. "20% Off" (string | null). Show as a badge overlay on the card and in the modal header. When present, originalPriceKobo is also set — use it as the strikethrough price.
|
|
76
|
+
- priceRange: { minKobo, maxKobo } — present when variants have different prices. Display as "₦X – ₦Y". Show instead of a single price when minKobo !== maxKobo.
|
|
77
|
+
|
|
78
|
+
Variant shape (CRITICAL — no top-level size/color fields):
|
|
79
|
+
Each variant has: { id, options: Array<{ field: string; value: string }>, priceKobo?, stock? }
|
|
80
|
+
options is a generic attribute list — e.g. [{field:"Color",value:"Red"},{field:"Size",value:"XL"}]
|
|
81
|
+
Render variant groups by grouping on options[*].field and showing chip/pill buttons per group.
|
|
82
|
+
|
|
83
|
+
Correct variant label helper:
|
|
84
|
+
function variantLabel(v) { return v.options?.map(o => o.value).join(' / ') || 'Standard'; }
|
|
85
|
+
|
|
86
|
+
Correct variant selector pattern:
|
|
87
|
+
// Group options by field
|
|
88
|
+
const fields = [...new Set(variants.flatMap(v => v.options.map(o => o.field)))];
|
|
89
|
+
// For each field, show a row of pill buttons
|
|
90
|
+
fields.map(field => (
|
|
91
|
+
<div key={field}>
|
|
92
|
+
<span>{field}</span>
|
|
93
|
+
{[...new Set(variants.flatMap(v => v.options.filter(o => o.field === field).map(o => o.value)))].map(val => (
|
|
94
|
+
<button key={val} onClick={() => setSelected(field, val)}
|
|
95
|
+
style={{ fontWeight: isSelected(field, val) ? 'bold' : 'normal' }}>{val}</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
Stock rules:
|
|
101
|
+
- stock: null = no stock tracking — treat as unlimited (never show "Out of Stock")
|
|
102
|
+
- stock: 0 = tracked and out of stock — show "Out of Stock", disable Add to Cart
|
|
103
|
+
- stock > 0 = tracked, cap quantity stepper at this value
|
|
104
|
+
- inStock: boolean — convenience field for grid card sold-out badge
|
|
105
|
+
Qty stepper: const maxStock = selected?.stock != null ? Number(selected.stock) : (product?.stock != null ? Number(product.stock) : Infinity);
|
|
106
|
+
Disable '+' when qty >= maxStock.
|
|
107
|
+
|
|
108
|
+
Image filtering + URL encoding:
|
|
109
|
+
Some image URLs contain spaces — always encode before use in <img src>.
|
|
110
|
+
function imageList(imgs) {
|
|
111
|
+
return (imgs ?? [])
|
|
112
|
+
.filter(u => !/\.(mp4|webm|mov)$/i.test(u))
|
|
113
|
+
.map(u => u.includes(' ') ? u.split('/').map(encodeURIComponent).join('/') : u);
|
|
114
|
+
}
|
|
115
|
+
Use imageList(product.images) everywhere — never pass the raw images array to <img src>.
|
|
116
|
+
|
|
117
|
+
Anti-patterns:
|
|
118
|
+
- NEVER hardcode product data — always use useProducts()
|
|
119
|
+
- NEVER access product.images[0].url — images is string[], use images[0] directly
|
|
120
|
+
- NEVER call addItem() without type: 'physical' or type: 'digital'
|
|
121
|
+
- NEVER use product.price or product.priceNaira — use product.priceKobo
|
|
122
|
+
- NEVER use v.name / v.size / v.color for variant labels — iterate v.options array
|
|
123
|
+
- NEVER use local state as substitute for openCart() — cart drawer won't appear without it
|
|
124
|
+
- NEVER show all products on the homepage — use a 4-item teaser + "Shop All Products →" CTA
|
|
125
|
+
- NEVER call useProducts() without an explicit limit — default is 6, silently truncates catalog
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## DIGITAL PRODUCTS
|
|
130
|
+
|
|
131
|
+
Route: #/digital/:id → pages/DigitalProductPage.tsx
|
|
132
|
+
Hooks: useDigitalProducts(), useDigitalProduct(id), useProductCheckout() or useCart()
|
|
133
|
+
|
|
134
|
+
List page: product cards with imageUrl, name, priceKobo/100, description snippet.
|
|
135
|
+
Detail page: full product info + "Buy Now" (useProductCheckout) or "Add to Cart" (useCart).
|
|
136
|
+
|
|
137
|
+
Anti-patterns:
|
|
138
|
+
- Same as physical products (images is string[], priceKobo not price, etc.)
|
|
139
|
+
- NEVER use useProductCheckout() for multi-product stores — use cart pattern instead
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## SERVICES
|
|
144
|
+
|
|
145
|
+
Route: #/services/:id → pages/ServicePage.tsx
|
|
146
|
+
Hooks: useServiceListings(), useServiceListing(id), useCheckoutService(), useInitiateEngagement()
|
|
147
|
+
|
|
148
|
+
List page: service cards with coverImage, title, category, priceKobo/100.
|
|
149
|
+
Detail page: full service info, add-ons, sub-services, customer form, checkout button.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## APPOINTMENTS
|
|
154
|
+
|
|
155
|
+
Hooks: useServices(), useAvailableDates({ serviceId, month }), useAvailability({ serviceId, from, to }), useBookAppointment()
|
|
156
|
+
|
|
157
|
+
Flow:
|
|
158
|
+
1. Show services list (useServices) — each card shows name, duration, price
|
|
159
|
+
2. Customer picks a service → show month calendar (useAvailableDates gives available YYYY-MM-DD dates)
|
|
160
|
+
3. Customer picks a date → show time slots: useAvailability({ serviceId, from: date, to: date })
|
|
161
|
+
NOTE: to must be the SAME date as from — the hook internally extends it to end-of-day
|
|
162
|
+
4. Customer picks a slot — render slot.startTime formatted for display
|
|
163
|
+
5. Show booking form (name, email, phone)
|
|
164
|
+
6. Submit: useBookAppointment().book({ serviceId, dateTime: slot.startTime, customerName, customerEmail, customerPhone })
|
|
165
|
+
Returns { paymentLink } — redirect with window.location.href = result.paymentLink
|
|
166
|
+
|
|
167
|
+
Price display:
|
|
168
|
+
service.priceKobo is a BIGINT from the API — Sequelize returns it as a STRING.
|
|
169
|
+
ALWAYS wrap in Number(): Number(service.priceKobo) before any arithmetic or comparison.
|
|
170
|
+
Display: ₦{(Number(service.priceKobo) / 100).toLocaleString()}
|
|
171
|
+
|
|
172
|
+
Slot filtering: DO NOT filter slots by status. useAvailability returns only bookable slots — pass them all through.
|
|
173
|
+
|
|
174
|
+
Anti-patterns:
|
|
175
|
+
- NEVER call initiate() for appointments — always use book(). useBookAppointment returns { book, initiate (alias), loading, error }
|
|
176
|
+
- NEVER pass slotId — pass dateTime: slot.startTime (ISO datetime string from useAvailability result)
|
|
177
|
+
- NEVER use slot.durationMinutes — it doesn't exist on AvailabilitySlot
|
|
178
|
+
- NEVER hardcode service names/prices — always use useServices()
|
|
179
|
+
- NEVER skip the date→slot two-step — must pick date first, then load slots for that date
|
|
180
|
+
- NEVER filter slots with slot.status === 'available' — drops all slots; let the hook decide
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## EVENTS
|
|
185
|
+
|
|
186
|
+
Route: #/events/:id → pages/EventPage.tsx
|
|
187
|
+
Hooks: useEvents(), useEvent(id), usePurchaseTickets(), useCreateRSVP()
|
|
188
|
+
|
|
189
|
+
List page: event cards with name, startsAt, endsAt, location, imageUrl.
|
|
190
|
+
Date display: use event.startsAt (ISO string) — NOT event.date (does not exist).
|
|
191
|
+
Format: new Date(event.startsAt).toLocaleDateString('en-NG', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' })
|
|
192
|
+
If endsAt differs from startsAt date, show a range: "Fri, 5 Sep – Sun, 7 Sep 2025"
|
|
193
|
+
Detail page: event info + ticket type selection + customer form + purchase button.
|
|
194
|
+
|
|
195
|
+
Anti-patterns:
|
|
196
|
+
- NEVER use event.date — the fields are startsAt and endsAt (ISO strings)
|
|
197
|
+
- NEVER use ticketType.soldCount — use ticketType.sold
|
|
198
|
+
- NEVER hardcode event data — always use useEvents()
|
|
199
|
+
- Ticket availability: ticketType.available (number | null)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## DONATIONS
|
|
204
|
+
|
|
205
|
+
Route: #/campaigns/:id → pages/CampaignPage.tsx
|
|
206
|
+
Hooks: useDonationCampaigns(), useDonationCampaign(id), useDonate()
|
|
207
|
+
|
|
208
|
+
List page: campaign cards with name, imageUrl, progress bar (raisedAmountKobo / goalAmountKobo).
|
|
209
|
+
Detail page: campaign info + amount input + customer form + donate button.
|
|
210
|
+
|
|
211
|
+
Anti-patterns:
|
|
212
|
+
- NEVER use campaign.raised or campaign.raisedAmount — use campaign.raisedAmountKobo
|
|
213
|
+
- NEVER use campaign.goalAmount — use campaign.goalAmountKobo (can be null)
|
|
214
|
+
- Progress bar: (raisedAmountKobo / goalAmountKobo) * 100 — guard for null goalAmountKobo
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## MEMBERSHIPS
|
|
219
|
+
|
|
220
|
+
Route: #/membership/:tierId → pages/MembershipPage.tsx
|
|
221
|
+
Hooks: useMemberships(), useSubscribeMembership()
|
|
222
|
+
|
|
223
|
+
List page: tier cards with name, priceKobo/100, billingPeriod label, benefits list.
|
|
224
|
+
billingPeriod labels: "monthly" → "/month", "yearly" → "/year", "one_time" → no label.
|
|
225
|
+
benefits: (tier.benefits ?? []).map(...) — can be null.
|
|
226
|
+
Detail page: tier info + benefits checklist + customer form + subscribe button.
|
|
227
|
+
|
|
228
|
+
Anti-patterns:
|
|
229
|
+
- NEVER use tier.price or tier.amount — use tier.priceKobo
|
|
230
|
+
- NEVER use tier.features or tier.perks — use tier.benefits (string[] | null)
|
|
231
|
+
- NEVER display "annual" — the SDK uses "yearly"
|
|
232
|
+
- NEVER assume tier.imageUrl exists — it is string | null, conditionally render
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## CONTENT & CMS
|
|
237
|
+
|
|
238
|
+
Hooks: useContentImages(), useContentVideos(), useContentTestimonials(), useContentFaqs(),
|
|
239
|
+
useContentStats(), useContentFeatures(), useContentTeamMembers(), useContentForm(formId), useSubmitForm(formId)
|
|
240
|
+
|
|
241
|
+
NOTE: The team hook is useContentTeamMembers() — NOT useContentTeam() (does not exist).
|
|
242
|
+
|
|
243
|
+
All content hooks return FetchState<ContentItem[]>.
|
|
244
|
+
Forms: render fields dynamically from form.fields array — never hardcode form structure.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## LISTINGS
|
|
249
|
+
|
|
250
|
+
Route: #/listing/:slugOrId
|
|
251
|
+
Hooks: useListings(), useListingDetail(slug), useListingFilters()
|
|
252
|
+
|
|
253
|
+
Action hooks depend on listing type:
|
|
254
|
+
- Payment listings: useListingPayment(id).checkout(customer)
|
|
255
|
+
- Enquiry listings: useListingEnquiry(id).submit({ name, email, message })
|
|
256
|
+
- Application listings: useListingApplication(id).submit({ name, email, coverNote })
|
|
257
|
+
- Reserve listings: useListingReserve(id).reserve()
|
|
258
|
+
- Download listings: useListingDownload(id).download()
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## CUSTOMER ACCOUNTS
|
|
263
|
+
|
|
264
|
+
Hooks: useCustomerAuth()
|
|
265
|
+
|
|
266
|
+
Auth flow: signUp(name, email, password) / signIn(email, password) / signOut()
|
|
267
|
+
Check: isLoggedIn (NOT isAuthenticated)
|
|
268
|
+
Protected routes: if (!isLoggedIn) show sign-in form.
|
|
269
|
+
Account dashboard: useMyOrders(), useMyAppointments(), useMyPurchases(), useMyMembership(), useMyProfile()
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## CART DRAWER THEMING
|
|
274
|
+
|
|
275
|
+
CartDrawer reads CSS custom properties from document.documentElement to match the site design.
|
|
276
|
+
You MUST set these in a useEffect in App.tsx (or a top-level layout component) so the drawer
|
|
277
|
+
inherits the correct colors and font instead of falling back to defaults.
|
|
278
|
+
|
|
279
|
+
Required pattern in App.tsx:
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
const root = document.documentElement;
|
|
282
|
+
root.style.setProperty('--sh-drawer-bg', '#ffffff'); // drawer background
|
|
283
|
+
root.style.setProperty('--sh-drawer-overlay', 'rgba(0,0,0,0.5)');
|
|
284
|
+
root.style.setProperty('--sh-drawer-text', '#111111'); // primary text
|
|
285
|
+
root.style.setProperty('--sh-drawer-muted', '#666666'); // secondary/muted text
|
|
286
|
+
root.style.setProperty('--sh-drawer-border', '#e5e5e5'); // dividers
|
|
287
|
+
root.style.setProperty('--sh-drawer-input-border', '#d1d5db'); // input field borders
|
|
288
|
+
root.style.setProperty('--sh-drawer-accent', '#000000'); // button/CTA background
|
|
289
|
+
root.style.setProperty('--sh-drawer-accent-text', '#ffffff'); // button/CTA text
|
|
290
|
+
root.style.setProperty('--sh-drawer-font', 'inherit'); // font-family
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
Replace the values above with colors that match the site's palette (e.g. brand primary for accent).
|
|
294
|
+
|
|
295
|
+
Anti-patterns:
|
|
296
|
+
- NEVER skip this useEffect — CartDrawer will render with mismatched default colors
|
|
297
|
+
- NEVER apply these properties to a scoped container — they MUST go on document.documentElement
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## UNIVERSAL RULES
|
|
302
|
+
|
|
303
|
+
1. Price display: always (priceKobo / 100).toLocaleString() — prefix with ₦
|
|
304
|
+
2. Empty states: always handle empty arrays gracefully — never show blank sections
|
|
305
|
+
3. Loading states: always show a loading indicator while data is being fetched
|
|
306
|
+
4. Images: always handle null imageUrl — show placeholder or hide the image element
|
|
307
|
+
5. Hash routing: use useHashRouter() for navigation between pages
|
|
308
|
+
6. Commerce results: all initiate/checkout functions return { paymentLink } — redirect with window.location.href
|
|
309
|
+
7. Cart pattern: addItem() → openCart() → CartDrawer shows → customer proceeds to checkout
|
|
310
|
+
|
|
311
|
+
## LAYOUT ALIGNMENT — MANDATORY
|
|
312
|
+
|
|
313
|
+
Every section on every page MUST share the same horizontal container. Nav, hero, product grids,
|
|
314
|
+
content sections, and footer must all align to the same left and right edges.
|
|
315
|
+
|
|
316
|
+
MANDATORY pattern — one shared container constant:
|
|
317
|
+
const CONTAINER: React.CSSProperties = {
|
|
318
|
+
maxWidth: 1200,
|
|
319
|
+
margin: '0 auto',
|
|
320
|
+
padding: '0 24px',
|
|
321
|
+
};
|
|
322
|
+
// Apply to every section wrapper and the nav inner wrapper:
|
|
323
|
+
<nav><div style={CONTAINER}>...</div></nav>
|
|
324
|
+
<section><div style={CONTAINER}>...</div></section>
|
|
325
|
+
<footer><div style={CONTAINER}>...</div></footer>
|
|
326
|
+
|
|
327
|
+
Rules:
|
|
328
|
+
- Nav inner content MUST use the same CONTAINER — nav edges must align with content edges
|
|
329
|
+
- Full-bleed backgrounds (hero color, section color) live on the outer element; content is always inside CONTAINER
|
|
330
|
+
- NEVER give different sections different maxWidth or padding values
|
|
331
|
+
- NEVER use margin: '0 48px' on one section and padding: '0 24px' on another
|
|
332
|
+
- NEVER let a section push wider or narrower than the nav content width
|
|
333
|
+
- Grid gaps, card padding, and inner spacing are fine to vary — only the outer container must be consistent
|
|
334
|
+
|
|
335
|
+
Anti-patterns:
|
|
336
|
+
- Nav at full width while sections have a max-width container → WRONG
|
|
337
|
+
- Hero section edge-to-edge but product section indented → WRONG
|
|
338
|
+
- Different sections using different padding (e.g. 16px vs 40px left/right) → WRONG
|
|
339
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DesignDirection schema resource.
|
|
3
|
+
*
|
|
4
|
+
* Exposes the exact DesignDirection TypeScript interface + field documentation
|
|
5
|
+
* so AI clients can generate a valid direction JSON themselves (zero SellHapi
|
|
6
|
+
* Claude cost) and submit it via the apply_design_direction tool.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the interface in generate2.controller.ts. Keep in sync when that
|
|
9
|
+
* interface changes.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DESIGN_DIRECTION_SCHEMA = "\n# SellHapi Design Direction Schema\n\nUse this schema to generate a `DesignDirection` JSON object and submit it via\n`apply_design_direction`. This path costs SellHapi nothing \u2014 your own LLM does\nthe work. Only fall back to `set_design_direction` if you cannot generate\nstructured JSON reliably.\n\n---\n\n## Top-level object\n\n```typescript\n{\n businessType: string; // REQUIRED. e.g. \"fashion\", \"bakery\", \"photography\"\n capabilityIds: string[]; // REQUIRED. Active capability IDs. See /api/mcp/capabilities.\n capabilitySlots?: CapabilitySlot[]; // Optional. Richer slot-per-purpose model (see below).\n pages: Page[]; // REQUIRED. Non-empty. One entry per storefront page.\n visualDirection: VisualDirection; // REQUIRED. All visual tokens.\n businessContext: BusinessContext; // REQUIRED. Business identity.\n storefrontConfig?: StorefrontConfig; // Optional. Only for storefront-mode projects.\n displayModes?: DisplayModes; // Optional. Product display preferences.\n}\n```\n\n---\n\n## Page\n\nEach page the storefront will have. Minimum 1, typically 3\u20136.\n\n```typescript\n{\n slug: string; // URL-safe identifier. e.g. \"home\", \"products\", \"about\", \"contact\"\n title: string; // Display name. e.g. \"Home\", \"Shop\", \"About Us\"\n sections: string[]; // Section labels in order. e.g. [\"hero\", \"product-grid\", \"testimonials\", \"newsletter\"]\n}\n```\n\n**Section label conventions:**\n- Use lowercase kebab-case\n- Should describe content purpose: \"hero\", \"featured-products\", \"services-list\", \"booking-cta\",\n \"testimonials\", \"faq\", \"team\", \"newsletter\", \"about-story\", \"contact-form\", \"events-grid\"\n- Capability-specific sections should reference the capability:\n \"appointment-booking\", \"donation-campaigns\", \"membership-tiers\", \"events-list\"\n\n**Common page structures:**\n\nPhysical products store:\n```\nhome: [\"hero\", \"featured-products\", \"collections\", \"testimonials\", \"newsletter\"]\nproducts: [\"filter-bar\", \"product-grid\", \"pagination\"]\nabout: [\"brand-story\", \"values\", \"team\"]\ncontact: [\"contact-form\", \"location-map\"]\n```\n\nService business:\n```\nhome: [\"hero\", \"services-overview\", \"how-it-works\", \"testimonials\", \"booking-cta\"]\nservices: [\"services-list\", \"pricing\"]\nabout: [\"our-story\", \"team\", \"credentials\"]\ncontact: [\"contact-form\", \"faq\"]\n```\n\n---\n\n## VisualDirection\n\nAll fields except the optional ones are required.\n\n```typescript\n{\n // Descriptive aesthetic \u2014 free text. e.g. \"Minimal luxury\", \"Vibrant street market\"\n style: string;\n\n primaryColor: string; // Main brand color \u2014 hex. e.g. \"#1a1a2e\"\n accentColor: string; // CTA / highlight color \u2014 hex. e.g. \"#e94560\"\n backgroundColor: string; // Page background \u2014 hex. Can be dark, cream, off-white.\n surfaceColor: string; // Card / panel background \u2014 hex.\n textColor: string; // Primary text color \u2014 hex.\n\n headingFont: string; // Google Fonts name for headings. e.g. \"Playfair Display\", \"Space Grotesk\"\n bodyFont: string; // Google Fonts name for body copy. e.g. \"Inter\", \"DM Sans\"\n fontStyle: string; // Description. e.g. \"editorial serif headings with clean sans body\"\n\n // Enum values \u2014 use exactly as written:\n cardStyle: \"flat\" | \"soft-shadow\" | \"bordered\" | \"glassmorphism\";\n buttonStyle: \"pill\" | \"rounded\" | \"sharp\" | \"outline-first\";\n visualEffect: \"flat\" | \"subtle-gradient\" | \"glassmorphism\" | \"textured\";\n\n mood: string; // e.g. \"sophisticated and aspirational\", \"warm and approachable\"\n\n // Optional \u2014 include when you have reference site analysis or strong design intent:\n layoutPattern?: string; // e.g. \"full-bleed hero, 3-col card grid, alternating split sections\"\n spacingRhythm?: string; // e.g. \"generous whitespace, section padding 80\u2013120px\"\n typographyScale?: string; // e.g. \"oversized display ~72px, 17px body, tight heading tracking\"\n componentNotes?: string; // e.g. \"rounded cards with drop shadow, pill category tags\"\n distinctiveElements?: string; // e.g. \"full-bleed photography, sticky glassmorphic nav\"\n}\n```\n\n**Color guidance:**\n- All color fields must be valid hex strings (#rrggbb or #rgb)\n- Ensure sufficient contrast: textColor vs backgroundColor \u2265 4.5:1 (WCAG AA)\n- primaryColor and accentColor should be visually distinct\n- surfaceColor is usually a slightly lighter/darker variant of backgroundColor\n\n**Font guidance:**\n- Use Google Fonts names exactly as they appear on fonts.google.com\n- Pairing examples: \"Playfair Display\" + \"Inter\", \"Space Grotesk\" + \"DM Sans\",\n \"Syne\" + \"Outfit\", \"Cormorant Garamond\" + \"Jost\"\n\n---\n\n## BusinessContext\n\n```typescript\n{\n name: string; // REQUIRED. Business name. e.g. \"Adire Lagos\"\n tagline: string; // One-line value prop. e.g. \"Authentic Nigerian fashion for the bold\"\n targetAudience: string; // e.g. \"Nigerian professionals aged 25\u201340 who value craftsmanship\"\n uniqueSellingPoints: string[]; // 2\u20134 bullets. e.g. [\"Handcrafted in Lagos\", \"Free delivery over \u20A615,000\"]\n}\n```\n\n---\n\n## CapabilitySlot (optional, more expressive than capabilityIds alone)\n\n```typescript\n{\n purpose: string; // Human-readable job. e.g. \"Let customers book sessions\"\n capability: string; // Selected capability ID. e.g. \"appointments\"\n alternatives: string[]; // Valid swaps for this slot. Always include the selected capability.\n}\n```\n\n---\n\n## StorefrontConfig (optional)\n\n```typescript\n{\n storeType: \"fashion\" | \"beauty\" | \"food\" | \"electronics\" | \"general\";\n vendorOverrides: string[]; // Free-text additions. e.g. [\"Feature collections on homepage\"]\n}\n```\n\n---\n\n## DisplayModes (optional \u2014 only for stores with physical_products)\n\n```typescript\n{\n productGrid: boolean; // Main product grid (useProducts)\n collections: boolean; // Product collections (useProductCollections)\n sections: boolean; // Category sections (useSections)\n bundles: boolean; // Bundles & deals (useStorefrontBundles)\n}\n```\n\n---\n\n## Full example \u2014 Lagos fashion store\n\n```json\n{\n \"businessType\": \"fashion\",\n \"capabilityIds\": [\"physical_products\", \"content\", \"customer_accounts\"],\n \"pages\": [\n {\n \"slug\": \"home\",\n \"title\": \"Home\",\n \"sections\": [\"hero\", \"featured-products\", \"collections\", \"brand-story\", \"testimonials\", \"newsletter\"]\n },\n {\n \"slug\": \"shop\",\n \"title\": \"Shop\",\n \"sections\": [\"filter-bar\", \"product-grid\"]\n },\n {\n \"slug\": \"about\",\n \"title\": \"About\",\n \"sections\": [\"brand-story\", \"values\", \"team\"]\n },\n {\n \"slug\": \"contact\",\n \"title\": \"Contact\",\n \"sections\": [\"contact-form\", \"faq\"]\n }\n ],\n \"visualDirection\": {\n \"style\": \"Minimal luxury\",\n \"primaryColor\": \"#1a1a2e\",\n \"accentColor\": \"#c9a84c\",\n \"backgroundColor\": \"#fafaf8\",\n \"surfaceColor\": \"#ffffff\",\n \"textColor\": \"#1a1a2e\",\n \"headingFont\": \"Cormorant Garamond\",\n \"bodyFont\": \"Inter\",\n \"fontStyle\": \"editorial serif headings with clean sans body\",\n \"cardStyle\": \"soft-shadow\",\n \"buttonStyle\": \"sharp\",\n \"visualEffect\": \"flat\",\n \"mood\": \"sophisticated and aspirational\",\n \"layoutPattern\": \"full-bleed hero, 2-col editorial split, 3-col product grid\",\n \"spacingRhythm\": \"generous whitespace, 100\u2013120px section padding\",\n \"typographyScale\": \"oversized display 72px, 16px body, tight tracking on headings\"\n },\n \"businessContext\": {\n \"name\": \"Adire Lagos\",\n \"tagline\": \"Authentic Nigerian fashion for the bold\",\n \"targetAudience\": \"Nigerian professionals aged 25\u201340 who value craftsmanship\",\n \"uniqueSellingPoints\": [\n \"Handcrafted adire fabric from Abeokuta\",\n \"Free delivery over \u20A615,000\",\n \"30-day returns\"\n ]\n },\n \"storefrontConfig\": {\n \"storeType\": \"fashion\",\n \"vendorOverrides\": []\n }\n}\n```\n\n---\n\n## Submission\n\nOnce you have generated the direction JSON and the user has reviewed it:\n\n```\napply_design_direction(projectId, direction)\n```\n\nThe server validates the shape, filters any unrecognised capabilityIds, and stores\nthe direction on the project. Status advances to \"directing\".\n";
|