Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/ai-agent-task.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: AI agent task
about: Scope work for Claude/Copilot/GitHub coding agents
labels: ai-agent, needs-plan
---

## Goal

Describe the customer/business outcome.

## Context

- Storefront area:
- Shopify/Supabase/Chatbase/Vercel impact:
- Design references:
- Plain `.patch`/`.diff` URL, if reviewing GitHub changes:

## Acceptance criteria

- [ ] Implementation matches the requested UX.
- [ ] `npm run check` passes.
- [ ] Vercel preview link is attached for UI changes.
- [ ] No secrets are committed.
- [ ] PR summary explains Shopify/Supabase/Chatbase impact if relevant.
20 changes: 20 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Summary

-

## Integration impact

- Shopify:
- Supabase:
- Chatbase:
- Vercel:

## Review links

- Plain diff/patch URL:

## Testing

- [ ] `npm run check`
- [ ] Vercel preview reviewed, if UI changed
- [ ] No secrets committed
22 changes: 22 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Storefront checks

on:
pull_request:
push:
branches: [main, master, work]

jobs:
static-check:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Run static checks
run: npm run check
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
.env
.turbo/
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Claude/GitHub agent instructions for FORMA

- Keep the storefront deployable as a static Vercel site unless a task explicitly adds a backend.
- Do not commit production secrets. Use `assets/config.js` only with blank/demo values in git.
- Prefer adding integration hooks in `assets/integrations.js` and UI behavior in `assets/store.js`.
- Run `npm run check` before every PR.
- For visual changes, attach a Vercel preview link or screenshot evidence in the PR.
- Keep Shopify as the commerce source of truth, Supabase as the CRM/event store, and Chatbase as the support assistant.
- When reviewing GitHub changes, prefer plain-text `.patch` or `.diff` URLs and see `docs/GITHUB_PLAIN_DIFFS.md`.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# FORMA storefront

See `docs/GO_LIVE_CHECKLIST.md` to deploy and connect integrations.

## Turborepo

See `docs/TURBOREPO.md` for installing and running Turbo with this repo.

## Monorepo apps and packages

This repo now includes two deployable apps (`apps/storefront`, `apps/docs`) and three shared packages (`packages/ui`, `packages/config`, `packages/storefront-data`) to match the Turborepo starter shape.
23 changes: 23 additions & 0 deletions apps/docs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FORMA Docs App</title>
<style>
body{margin:0;font-family:Inter,system-ui,sans-serif;background:#f7efe4;color:#16130f}.shell{width:min(860px,calc(100% - 2rem));margin:0 auto;padding:4rem 0}li{margin:.6rem 0}a{color:inherit;font-weight:800}
</style>
</head>
<body>
<main class="shell">
<p>Deployable app 2 · apps/docs</p>
<h1>FORMA Docs</h1>
<p>Documentación deployable para instalación, go-live, Shopify, Supabase, Chatbase, Vercel y Turborepo.</p>
<ul data-app-records>
<li><a href="../../docs/GO_LIVE_CHECKLIST.md">Go-live checklist</a></li>
<li><a href="../../docs/INTEGRATIONS.md">Integrations</a></li>
<li><a href="../../docs/TURBOREPO.md">Turborepo</a></li>
</ul>
</main>
</body>
</html>
15 changes: 15 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@forma/app-docs",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node ../../scripts/smoke-app.js .",
"check": "node ../../scripts/smoke-app.js .",
"dev": "python3 -m http.server 4175 --directory apps/docs",
"start": "python3 -m http.server ${PORT:-4175} --directory apps/docs"
},
"dependencies": {
"@forma/config": "*",
"@forma/ui": "*"
}
}
24 changes: 24 additions & 0 deletions apps/storefront/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>FORMA Storefront App</title>
<style>
body{margin:0;font-family:Inter,system-ui,sans-serif;background:#fffaf2;color:#16130f}.shell{width:min(960px,calc(100% - 2rem));margin:0 auto;padding:4rem 0}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem}.card{padding:1rem;border:1px solid #eadfce;border-radius:1rem;background:#fff}a{color:inherit;font-weight:800}
</style>
</head>
<body>
<main class="shell">
<p>Deployable app 1 · apps/storefront</p>
<h1>FORMA Storefront</h1>
<p>App estática deployable dentro del monorepo Turborepo. La tienda principal completa sigue disponible en la raíz del repo.</p>
<div class="grid" data-app-records>
<article class="card"><strong>Trench Lino Arena</strong><br><span>Prendas · $148</span></article>
<article class="card"><strong>Bolso Arc Cognac</strong><br><span>Bolsos · $96</span></article>
<article class="card"><strong>Tote Weekend</strong><br><span>Bolsos · $112</span></article>
</div>
<p><a href="../../index.html">Abrir storefront completo</a></p>
</main>
</body>
</html>
16 changes: 16 additions & 0 deletions apps/storefront/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@forma/app-storefront",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "node ../../scripts/smoke-app.js .",
"check": "node ../../scripts/smoke-app.js .",
"dev": "python3 -m http.server 4174 --directory apps/storefront",
"start": "python3 -m http.server ${PORT:-4174} --directory apps/storefront"
},
"dependencies": {
"@forma/config": "*",
"@forma/storefront-data": "*",
"@forma/ui": "*"
}
}
26 changes: 26 additions & 0 deletions assets/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// FORMA runtime configuration.
// Keep real credentials in Vercel Environment Variables or generate this file during deploy.
// Values left blank keep the storefront in safe local-demo mode.
window.FORMA_CONFIG = {
shopify: {
domain: '',
storefrontToken: '',
apiVersion: '2026-01',
enableRemoteProducts: false,
},
supabase: {
url: '',
anonKey: '',
eventsTable: 'store_events',
newsletterTable: 'newsletter_signups',
},
chatbase: {
botId: '',
enabled: false,
},
githubAgent: {
provider: 'Claude',
repo: 'FEISHTML',
workflow: 'Plan → branch → PR → Vercel preview → review → merge',
},
};
189 changes: 189 additions & 0 deletions assets/integrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
(function attachFormaIntegrations() {
const emptyConfig = {
shopify: {},
supabase: {},
chatbase: {},
githubAgent: {},
};

function getConfig() {
return { ...emptyConfig, ...(window.FORMA_CONFIG || {}) };
}

function hasSupabase() {
const { supabase } = getConfig();
return Boolean(supabase?.url && supabase?.anonKey);
}

async function supabaseInsert(tableName, payload) {
const { supabase } = getConfig();
if (!hasSupabase() || !tableName) return { skipped: true };

const response = await fetch(`${supabase.url.replace(/\/$/, '')}/rest/v1/${tableName}`, {
method: 'POST',
headers: {
apikey: supabase.anonKey,
Authorization: `Bearer ${supabase.anonKey}`,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
},
body: JSON.stringify(payload),
});

if (!response.ok) {
throw new Error(`Supabase insert failed: ${response.status}`);
}

return { ok: true };
}

async function trackEvent(eventName, payload = {}) {
const { supabase } = getConfig();
try {
return await supabaseInsert(supabase.eventsTable, {
event_name: eventName,
payload,
page_path: window.location.pathname,
user_agent: window.navigator.userAgent,
created_at: new Date().toISOString(),
});
} catch (error) {
console.warn('[FORMA] Event tracking skipped:', error.message);
return { skipped: true, error };
}
}

async function saveNewsletter(email) {
const { supabase } = getConfig();
try {
return await supabaseInsert(supabase.newsletterTable, {
email,
source: 'forma-storefront',
created_at: new Date().toISOString(),
});
} catch (error) {
console.warn('[FORMA] Newsletter sync skipped:', error.message);
return { skipped: true, error };
}
}

function loadChatbase() {
const { chatbase } = getConfig();
if (!chatbase?.enabled || !chatbase?.botId || document.querySelector('[data-chatbase-loader]')) {
return false;
}

window.embeddedChatbotConfig = { chatbotId: chatbase.botId, domain: 'www.chatbase.co' };
const script = document.createElement('script');
script.src = 'https://www.chatbase.co/embed.min.js';
script.id = chatbase.botId;
script.setAttribute('chatbotId', chatbase.botId);
script.setAttribute('domain', 'www.chatbase.co');
script.dataset.chatbaseLoader = 'true';
script.defer = true;
document.body.appendChild(script);
return true;
}

function hasShopify() {
const { shopify } = getConfig();
return Boolean(shopify?.domain && shopify?.storefrontToken && shopify?.enableRemoteProducts);
}

async function fetchShopifyProducts() {
const { shopify } = getConfig();
if (!hasShopify()) return [];

const endpoint = `https://${shopify.domain}/api/${shopify.apiVersion || '2026-01'}/graphql.json`;
const query = `
query FormaProducts($first: Int!) {
products(first: $first, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
title
productType
createdAt
description
tags
featuredImage { url altText }
variants(first: 20) {
edges {
node {
id
title
price { amount currencyCode }
compareAtPrice { amount currencyCode }
selectedOptions { name value }
}
}
}
}
}
}
}
`;

const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': shopify.storefrontToken,
},
body: JSON.stringify({ query, variables: { first: 12 } }),
});

if (!response.ok) {
throw new Error(`Shopify request failed: ${response.status}`);
}

const data = await response.json();
if (data.errors) {
throw new Error(data.errors.map((error) => error.message).join(', '));
}

return data.data.products.edges.map(({ node }, index) => {
const firstVariant = node.variants.edges[0]?.node;
const price = Number(firstVariant?.price?.amount || 0);
const oldPrice = Number(firstVariant?.compareAtPrice?.amount || 0) || null;
const colorValues = new Set();
const sizeValues = new Set();

node.variants.edges.forEach(({ node: variant }) => {
variant.selectedOptions.forEach((option) => {
if (/color/i.test(option.name)) colorValues.add(option.value);
if (/size|talla/i.test(option.name)) sizeValues.add(option.value);
});
});

return {
id: node.id,
shopifyVariantId: firstVariant?.id,
name: node.title,
category: node.productType || 'Shopify',
price,
oldPrice,
rating: 4.8,
badge: node.tags[0] || 'Shopify',
createdAt: 100 + index,
description: node.description || 'Producto sincronizado desde Shopify.',
colors: [...colorValues].length ? [...colorValues] : ['Default'],
sizes: [...sizeValues].length ? [...sizeValues] : ['Única'],
bg: '#eadfd0',
gradient: 'linear-gradient(145deg, #fff8ec, #c89b77)',
shape: '1.2rem',
image: node.featuredImage?.url,
};
});
}

window.FormaIntegrations = {
fetchShopifyProducts,
getConfig,
hasShopify,
hasSupabase,
loadChatbase,
saveNewsletter,
trackEvent,
};
})();
Loading