diff --git a/templates/croct/starter/ecommerce-nanostores/README.md b/templates/croct/starter/ecommerce-nanostores/README.md new file mode 100644 index 00000000..7f3d5a21 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/README.md @@ -0,0 +1,33 @@ +# Introduction + +This template sets up an ecommerce site using [React](https://react.dev/?utm_source=croct), +[Vite](https://vite.dev/?utm_source=croct), and [Croct Nanostores](https://croct-nano.fryuni.dev). + +It demonstrates how to use the [croct-nanostores](https://croct-nano.fryuni.dev) library to add +personalized banners, product recommendations, and cart tracking to a storefront using +framework-agnostic [Nanostores](https://github.com/nanostores/nanostores) atoms. + +## What's included + +- **Announcement bar** — A personalized top-of-page banner managed via a Croct slot. +- **Hero banner** — A full-width hero section with title, subtitle, and call-to-action. +- **Product recommendations** — A personalized product grid powered by Croct content rules. +- **Cart tracking** — Cart state tracked via `trackCart`, triggering automatic content refresh. +- **Auto-refresh** — Content automatically updates when user behavior changes (e.g., cart modifications). + +## Usage + +To create a new project using this template, run: + +```croct-cmd +croct use croct://starter/ecommerce-nanostores +``` + +## Options + +The following options are available for this template: + +| Option | Description | Required | Default | +|-------------------|--------------------------------------------------|----------|-----------------| +| `name` | The name of the project. | No | `my-croct-shop` | +| `disableLauncher` | Whether to disable the project launcher. | No | `false` | diff --git a/templates/croct/starter/ecommerce-nanostores/code/App.tsx b/templates/croct/starter/ecommerce-nanostores/code/App.tsx new file mode 100644 index 00000000..ec431331 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/App.tsx @@ -0,0 +1,45 @@ +import { AnnouncementBar } from './components/AnnouncementBar'; +import { HeroBanner } from './components/HeroBanner'; +import { ProductGrid } from './components/ProductGrid'; +import { Cart } from './components/Cart'; + +export function App() { + return ( +
+ +
+
+ + Store + + + +
+
+
+ + +
+ +
+ ); +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/components/AnnouncementBar.tsx b/templates/croct/starter/ecommerce-nanostores/code/components/AnnouncementBar.tsx new file mode 100644 index 00000000..a41dce43 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/components/AnnouncementBar.tsx @@ -0,0 +1,28 @@ +import { useStore } from '@nanostores/react'; +import { announcementBar } from '../stores/banner'; + +export function AnnouncementBar() { + const { content } = useStore(announcementBar); + + const bar = ( +
+ {content.text} +
+ ); + + if (content.link) { + return ( + + {bar} + + ); + } + + return bar; +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/components/Cart.tsx b/templates/croct/starter/ecommerce-nanostores/code/components/Cart.tsx new file mode 100644 index 00000000..8086ec74 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/components/Cart.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react'; +import { useStore } from '@nanostores/react'; +import { $cartItems, $cartTotal, $cartCount, removeFromCart, updateQuantity } from '../stores/cart'; + +export function Cart() { + const [isOpen, setIsOpen] = useState(false); + const items = useStore($cartItems); + const total = useStore($cartTotal); + const count = useStore($cartCount); + + return ( + <> + + {isOpen && ( +
+
+

Shopping Cart

+ +
+ {items.length === 0 ? ( +

Your cart is empty.

+ ) : ( + <> + +
+ Total: ${total.toFixed(2)} +
+ + )} +
+ )} + + ); +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/components/HeroBanner.tsx b/templates/croct/starter/ecommerce-nanostores/code/components/HeroBanner.tsx new file mode 100644 index 00000000..ab535015 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/components/HeroBanner.tsx @@ -0,0 +1,18 @@ +import { useStore } from '@nanostores/react'; +import { heroBanner } from '../stores/banner'; + +export function HeroBanner() { + const { content } = useStore(heroBanner); + + return ( +
+
+

{content.title}

+ {content.subtitle &&

{content.subtitle}

} + + {content.ctaLabel} + +
+
+ ); +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/components/ProductGrid.tsx b/templates/croct/starter/ecommerce-nanostores/code/components/ProductGrid.tsx new file mode 100644 index 00000000..b6ed8682 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/components/ProductGrid.tsx @@ -0,0 +1,44 @@ +import { useStore } from '@nanostores/react'; +import { recommendedProducts } from '../stores/products'; +import { addToCart } from '../stores/cart'; + +export function ProductGrid() { + const { content } = useStore(recommendedProducts); + + return ( +
+ {content.title &&

{content.title}

} + {content.description &&

{content.description}

} +
+ {content.cards.map((card, index) => ( +
+ {card.image && ( + {card.name} + )} +
+

{card.name}

+ {card.description && ( +

{card.description}

+ )} +

${card.price.toFixed(2)}

+ {card.cta && ( + + )} +
+
+ ))} +
+
+ ); +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/index.css b/templates/croct/starter/ecommerce-nanostores/code/index.css new file mode 100644 index 00000000..4e42907f --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/index.css @@ -0,0 +1,347 @@ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + sans-serif; + color: #1a1a2e; + background: #fafafa; + line-height: 1.6; +} + +a { + color: inherit; + text-decoration: none; +} + +/* Announcement Bar */ +.announcement-bar { + text-align: center; + padding: 0.5rem 1rem; + font-size: 0.875rem; + font-weight: 500; +} + +.announcement-bar-link { + display: block; +} + +.announcement-bar-link:hover .announcement-bar { + opacity: 0.9; +} + +/* Header */ +.header { + border-bottom: 1px solid #e5e7eb; + background: #fff; + position: sticky; + top: 0; + z-index: 10; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 1rem 2rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.logo { + font-size: 1.5rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.nav { + display: flex; + gap: 1.5rem; +} + +.nav a:hover { + color: #6366f1; +} + +/* Hero Banner */ +.hero-banner { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: #fff; + padding: 5rem 2rem; + text-align: center; +} + +.hero-content { + max-width: 640px; + margin: 0 auto; +} + +.hero-content h1 { + font-size: 3rem; + font-weight: 800; + line-height: 1.1; + margin-bottom: 1rem; +} + +.hero-subtitle { + font-size: 1.25rem; + opacity: 0.9; + margin-bottom: 2rem; +} + +.hero-cta { + display: inline-block; + background: #fff; + color: #667eea; + padding: 0.75rem 2rem; + border-radius: 0.5rem; + font-weight: 600; + font-size: 1rem; + transition: transform 0.15s, box-shadow 0.15s; +} + +.hero-cta:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Product Section */ +.product-section { + max-width: 1200px; + margin: 0 auto; + padding: 4rem 2rem; +} + +.section-title { + font-size: 2rem; + font-weight: 700; + text-align: center; + margin-bottom: 0.5rem; +} + +.section-description { + text-align: center; + color: #6b7280; + margin-bottom: 2.5rem; +} + +.product-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; +} + +.product-card { + background: #fff; + border-radius: 0.75rem; + overflow: hidden; + border: 1px solid #e5e7eb; + transition: box-shadow 0.2s; +} + +.product-card:hover { + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.product-image { + width: 100%; + height: 240px; + object-fit: cover; + background: #f3f4f6; +} + +.product-info { + padding: 1.25rem; +} + +.product-name { + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.product-description { + color: #6b7280; + font-size: 0.875rem; + margin-bottom: 0.75rem; +} + +.product-price { + font-size: 1.25rem; + font-weight: 700; + color: #111827; + margin-bottom: 1rem; +} + +.product-cta { + width: 100%; + padding: 0.625rem; + background: #111827; + color: #fff; + border: none; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s; +} + +.product-cta:hover { + background: #374151; +} + +/* Cart */ +.cart-toggle { + background: none; + border: 1px solid #e5e7eb; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: background 0.15s; +} + +.cart-toggle:hover { + background: #f3f4f6; +} + +.cart-panel { + position: fixed; + top: 0; + right: 0; + width: 380px; + max-width: 100vw; + height: 100vh; + background: #fff; + border-left: 1px solid #e5e7eb; + box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1); + z-index: 100; + display: flex; + flex-direction: column; + padding: 1.5rem; +} + +.cart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.cart-header h2 { + font-size: 1.25rem; + font-weight: 700; +} + +.cart-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem; + color: #6b7280; +} + +.cart-empty { + color: #6b7280; + text-align: center; + padding: 2rem 0; +} + +.cart-items { + list-style: none; + flex: 1; + overflow-y: auto; +} + +.cart-item { + padding: 1rem 0; + border-bottom: 1px solid #f3f4f6; +} + +.cart-item-info { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +.cart-item-name { + font-weight: 500; +} + +.cart-item-price { + font-weight: 600; +} + +.cart-item-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.cart-item-actions button { + background: #f3f4f6; + border: none; + width: 28px; + height: 28px; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: center; +} + +.cart-item-actions button:hover { + background: #e5e7eb; +} + +.cart-item-remove { + width: auto !important; + padding: 0 0.5rem !important; + margin-left: auto; + color: #ef4444; + background: transparent !important; + font-size: 0.75rem !important; +} + +.cart-item-remove:hover { + background: #fef2f2 !important; +} + +.cart-total { + padding-top: 1rem; + border-top: 2px solid #111827; + text-align: right; + font-size: 1.125rem; +} + +/* Footer */ +.footer { + text-align: center; + padding: 2rem; + border-top: 1px solid #e5e7eb; + color: #6b7280; + font-size: 0.875rem; +} + +.footer a { + color: #6366f1; + font-weight: 500; +} + +.footer a:hover { + text-decoration: underline; +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/main.tsx b/templates/croct/starter/ecommerce-nanostores/code/main.tsx new file mode 100644 index 00000000..9b39cc33 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/templates/croct/starter/ecommerce-nanostores/code/stores/banner.ts b/templates/croct/starter/ecommerce-nanostores/code/stores/banner.ts new file mode 100644 index 00000000..43ca2715 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/stores/banner.ts @@ -0,0 +1,18 @@ +import { croctContent } from 'croct-nanostores'; +import './croct'; + +export const announcementBar = croctContent('%announcementBarSlotId%@%announcementBarSlotVersion%', { + text: 'Free shipping on orders over $50!', + link: 'https://example.com/products', + style: { + backgroundColor: '#000000', + textColor: '#ffffff', + }, +}); + +export const heroBanner = croctContent('%heroBannerSlotId%@%heroBannerSlotVersion%', { + title: 'Welcome to our store', + subtitle: 'Discover our curated collection of premium products.', + ctaLabel: 'Shop now', + ctaLink: 'https://example.com/products', +}); diff --git a/templates/croct/starter/ecommerce-nanostores/code/stores/cart.ts b/templates/croct/starter/ecommerce-nanostores/code/stores/cart.ts new file mode 100644 index 00000000..979bd163 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/stores/cart.ts @@ -0,0 +1,82 @@ +import { atom, computed } from 'nanostores'; +import { trackCart } from 'croct-nanostores'; +import './croct'; + +type CartItem = { + productId: string; + name: string; + unitPrice: number; + quantity: number; +}; + +type CroctCart = { + items: Array<{ + productId: string; + name: string; + unitPrice: number; + quantity: number; + }>; +}; + +export const $cartItems = atom([]); + +export const $cartTotal = computed($cartItems, items => + items.reduce((sum, item) => sum + item.unitPrice * item.quantity, 0), +); + +export const $cartCount = computed($cartItems, items => + items.reduce((sum, item) => sum + item.quantity, 0), +); + +const $croctCart = atom(null); + +trackCart($croctCart); + +function syncCroctCart(): void { + const items = $cartItems.get(); + + $croctCart.set( + items.length > 0 + ? { + items: items.map(item => ({ + productId: item.productId, + name: item.name, + unitPrice: item.unitPrice, + quantity: item.quantity, + })), + } + : null, + ); +} + +export function addToCart(item: Omit): void { + const items = $cartItems.get(); + const existing = items.find(i => i.productId === item.productId); + + if (existing) { + $cartItems.set( + items.map(i => + i.productId === item.productId ? { ...i, quantity: i.quantity + 1 } : i, + ), + ); + } else { + $cartItems.set([...items, { ...item, quantity: 1 }]); + } + + syncCroctCart(); +} + +export function removeFromCart(productId: string): void { + $cartItems.set($cartItems.get().filter(i => i.productId !== productId)); + syncCroctCart(); +} + +export function updateQuantity(productId: string, quantity: number): void { + if (quantity <= 0) { + removeFromCart(productId); + return; + } + + $cartItems.set($cartItems.get().map(i => (i.productId === productId ? { ...i, quantity } : i))); + syncCroctCart(); +} diff --git a/templates/croct/starter/ecommerce-nanostores/code/stores/croct.ts b/templates/croct/starter/ecommerce-nanostores/code/stores/croct.ts new file mode 100644 index 00000000..1b7f91e0 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/stores/croct.ts @@ -0,0 +1,6 @@ +import { croct } from 'croct-nanostores'; + +croct.plug({ + appId: '%appId%', + plugins: ['auto-refresh-atom'], +}); diff --git a/templates/croct/starter/ecommerce-nanostores/code/stores/products.ts b/templates/croct/starter/ecommerce-nanostores/code/stores/products.ts new file mode 100644 index 00000000..a7c8a767 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/code/stores/products.ts @@ -0,0 +1,36 @@ +import { croctContent } from 'croct-nanostores'; +import './croct'; + +export const recommendedProducts = croctContent('%productsSlotId%@%productsSlotVersion%', { + title: 'Recommended for you', + description: 'Handpicked products based on your preferences.', + cards: [ + { + name: 'Classic T-Shirt', + description: 'Premium cotton comfort for everyday wear.', + price: 29.99, + cta: { + label: 'Add to cart', + link: 'https://example.com/products/classic-t-shirt', + }, + }, + { + name: 'Running Shoes', + description: 'Lightweight performance for every run.', + price: 89.99, + cta: { + label: 'Add to cart', + link: 'https://example.com/products/running-shoes', + }, + }, + { + name: 'Leather Backpack', + description: 'Durable everyday carry with timeless style.', + price: 129.99, + cta: { + label: 'Add to cart', + link: 'https://example.com/products/leather-backpack', + }, + }, + ], +}); diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-content.en.json b/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-content.en.json new file mode 100644 index 00000000..562651fe --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-content.en.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content.json", + "type": "structure", + "attributes": { + "text": { + "type": "text", + "value": { + "type": "static", + "value": "Free shipping on orders over $50!" + } + }, + "link": { + "type": "text", + "value": { + "type": "static", + "value": "https://example.com/products" + } + }, + "style": { + "type": "structure", + "attributes": { + "backgroundColor": { + "type": "text", + "value": { + "type": "static", + "value": "#000000" + } + }, + "textColor": { + "type": "text", + "value": { + "type": "static", + "value": "#ffffff" + } + } + } + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-schema.json b/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-schema.json new file mode 100644 index 00000000..b9ece105 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/announcement-bar-schema.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content-schema.json", + "type": "structure", + "attributes": { + "text": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Message", + "private": false, + "optional": false, + "position": 0, + "description": "The message to display in the announcement bar." + }, + "link": { + "type": { + "type": "text", + "format": "url" + }, + "label": "Link", + "private": false, + "optional": true, + "position": 1, + "description": "The link to navigate when clicking the announcement bar." + }, + "style": { + "type": { + "type": "structure", + "attributes": { + "backgroundColor": { + "type": { + "type": "text", + "format": "color" + }, + "label": "Background Color", + "private": false, + "optional": false, + "position": 0, + "description": "The background color of the announcement bar." + }, + "textColor": { + "type": { + "type": "text", + "format": "color" + }, + "label": "Text Color", + "private": false, + "optional": false, + "position": 1, + "description": "The text color of the announcement bar." + } + } + }, + "label": "Style", + "position": 2, + "description": "The style of the announcement bar." + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-content.en.json b/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-content.en.json new file mode 100644 index 00000000..5ca34ed7 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-content.en.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content.json", + "type": "structure", + "attributes": { + "title": { + "type": "text", + "value": { + "type": "static", + "value": "Welcome to our store" + } + }, + "subtitle": { + "type": "text", + "value": { + "type": "static", + "value": "Discover our curated collection of premium products." + } + }, + "ctaLabel": { + "type": "text", + "value": { + "type": "static", + "value": "Shop now" + } + }, + "ctaLink": { + "type": "text", + "value": { + "type": "static", + "value": "https://example.com/products" + } + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-schema.json b/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-schema.json new file mode 100644 index 00000000..98a17065 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/hero-banner-schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content-schema.json", + "type": "structure", + "attributes": { + "title": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Title", + "private": false, + "optional": false, + "position": 0, + "description": "The main heading of the hero banner." + }, + "subtitle": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Subtitle", + "private": false, + "optional": true, + "position": 1, + "description": "The subtitle displayed below the title." + }, + "ctaLabel": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "CTA Label", + "private": false, + "optional": false, + "position": 2, + "description": "The label of the call-to-action button." + }, + "ctaLink": { + "type": { + "type": "text", + "format": "url" + }, + "label": "CTA Link", + "private": false, + "optional": false, + "position": 3, + "description": "The target URL of the call-to-action button." + }, + "image": { + "type": { + "id": "@croct/file", + "type": "reference", + "properties": { + "maximumSize": 307200 + } + }, + "label": "Background Image", + "private": false, + "optional": true, + "position": 4, + "description": "An optional background image for the hero banner." + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-content.en.json b/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-content.en.json new file mode 100644 index 00000000..34c44ada --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-content.en.json @@ -0,0 +1,181 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content.json", + "type": "structure", + "attributes": { + "title": { + "type": "text", + "value": { + "type": "static", + "value": "Recommended for you" + } + }, + "description": { + "type": "text", + "value": { + "type": "static", + "value": "Handpicked products based on your preferences." + } + }, + "cards": { + "type": "list", + "items": [ + { + "type": "structure", + "attributes": { + "name": { + "type": "text", + "value": { + "type": "static", + "value": "Classic T-Shirt" + } + }, + "description": { + "type": "text", + "value": { + "type": "static", + "value": "Premium cotton comfort for everyday wear." + } + }, + "image": { + "type": "text", + "value": { + "type": "static", + "value": "https://cdn.croct.io/workspace/customer-assets/bac910ff-a18e-4ca5-bdd0-155139967ea4/4ed0d2a8-07e4-4808-a5f5-a163aa114e48" + } + }, + "price": { + "type": "number", + "value": { + "type": "static", + "value": 29.99 + } + }, + "cta": { + "type": "structure", + "attributes": { + "label": { + "type": "text", + "value": { + "type": "static", + "value": "Add to cart" + } + }, + "link": { + "type": "text", + "value": { + "type": "static", + "value": "https://example.com/products/classic-t-shirt" + } + } + } + } + } + }, + { + "type": "structure", + "attributes": { + "name": { + "type": "text", + "value": { + "type": "static", + "value": "Running Shoes" + } + }, + "description": { + "type": "text", + "value": { + "type": "static", + "value": "Lightweight performance for every run." + } + }, + "image": { + "type": "text", + "value": { + "type": "static", + "value": "https://cdn.croct.io/workspace/customer-assets/bac910ff-a18e-4ca5-bdd0-155139967ea4/4ed0d2a8-07e4-4808-a5f5-a163aa114e48" + } + }, + "price": { + "type": "number", + "value": { + "type": "static", + "value": 89.99 + } + }, + "cta": { + "type": "structure", + "attributes": { + "label": { + "type": "text", + "value": { + "type": "static", + "value": "Add to cart" + } + }, + "link": { + "type": "text", + "value": { + "type": "static", + "value": "https://example.com/products/running-shoes" + } + } + } + } + } + }, + { + "type": "structure", + "attributes": { + "name": { + "type": "text", + "value": { + "type": "static", + "value": "Leather Backpack" + } + }, + "description": { + "type": "text", + "value": { + "type": "static", + "value": "Durable everyday carry with timeless style." + } + }, + "image": { + "type": "text", + "value": { + "type": "static", + "value": "https://cdn.croct.io/workspace/customer-assets/bac910ff-a18e-4ca5-bdd0-155139967ea4/4ed0d2a8-07e4-4808-a5f5-a163aa114e48" + } + }, + "price": { + "type": "number", + "value": { + "type": "static", + "value": 129.99 + } + }, + "cta": { + "type": "structure", + "attributes": { + "label": { + "type": "text", + "value": { + "type": "static", + "value": "Add to cart" + } + }, + "link": { + "type": "text", + "value": { + "type": "static", + "value": "https://example.com/products/leather-backpack" + } + } + } + } + } + } + ] + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-schema.json b/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-schema.json new file mode 100644 index 00000000..41459bff --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/configuration/product-cards-schema.json @@ -0,0 +1,121 @@ +{ + "$schema": "https://schema.croct.com/json/v1/template-content-schema.json", + "type": "structure", + "attributes": { + "title": { + "type": { + "type": "text" + }, + "label": "Title", + "private": false, + "optional": true, + "position": 0, + "description": "The section heading displayed above the product grid." + }, + "description": { + "type": { + "type": "text" + }, + "label": "Description", + "private": false, + "optional": true, + "position": 1, + "description": "A short description displayed below the section title." + }, + "cards": { + "type": { + "type": "list", + "items": { + "type": "structure", + "attributes": { + "name": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Name", + "private": false, + "optional": false, + "position": 0, + "description": "The name of the product." + }, + "description": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Description", + "private": false, + "optional": true, + "position": 1, + "description": "A short description of the product." + }, + "image": { + "type": { + "id": "@croct/file", + "type": "reference", + "properties": {} + }, + "label": "Image", + "private": false, + "optional": true, + "position": 2, + "description": "The product image." + }, + "price": { + "type": { + "type": "number", + "minimum": 0 + }, + "label": "Price", + "private": false, + "optional": false, + "position": 3, + "description": "The display price of the product." + }, + "cta": { + "type": { + "type": "structure", + "attributes": { + "label": { + "type": { + "type": "text", + "minimumLength": 1 + }, + "label": "Label", + "private": false, + "optional": false, + "position": 0, + "description": "The label of the button." + }, + "link": { + "type": { + "type": "text", + "format": "url" + }, + "label": "Link", + "private": false, + "optional": false, + "position": 1, + "description": "The target URL for the button." + } + } + }, + "label": "Call to action", + "private": false, + "optional": true, + "position": 4, + "description": "The call-to-action button for the product." + } + } + }, + "itemLabel": "Product card" + }, + "label": "Cards", + "private": false, + "optional": false, + "position": 2, + "description": "The list of product cards to display." + } + } +} diff --git a/templates/croct/starter/ecommerce-nanostores/cover.png b/templates/croct/starter/ecommerce-nanostores/cover.png new file mode 100644 index 00000000..7b64fd2c Binary files /dev/null and b/templates/croct/starter/ecommerce-nanostores/cover.png differ diff --git a/templates/croct/starter/ecommerce-nanostores/template.json5 b/templates/croct/starter/ecommerce-nanostores/template.json5 new file mode 100644 index 00000000..317db4b9 --- /dev/null +++ b/templates/croct/starter/ecommerce-nanostores/template.json5 @@ -0,0 +1,316 @@ +{ + "$schema": "https://schema.croct.com/json/v1/catalog-template.json", + "title": "Ecommerce + Nanostores starter", + "description": "An ecommerce site with personalized banners and product recommendations using Croct Nanostores.", + "metadata": { + "id": "boilerplate/starter/ecommerce-nanostores", + "ecosystem": { + "name": "Croct", + "avatarUrl": "https://github.com/croct-tech.png", + "websiteUrl": "https://croct.com" + }, + "documentationUrl": "https://github.com/croct-tech/templates/blob/master/templates/croct/starter/ecommerce-nanostores/README.md", + "sourceUrl": "https://github.com/croct-tech/templates/blob/master/templates/croct/starter/ecommerce-nanostores/template.json5", + "coverImageUrl": "https://github.com/croct-tech/templates/blob/master/templates/croct/starter/ecommerce-nanostores/cover.png", + "installationUrl": "croct://starter/ecommerce-nanostores", + "categories": [ + "boilerplate/starter", + "framework/react", + "language/typescript", + "library/nanostores" + ], + "relatedTemplates": [ + "boilerplate/starter/nextjs", + "boilerplate/starter/shadcn-ui" + ] + }, + "options": { + "name": { + "type": "string", + "description": "The name of the project.", + "default": "my-croct-shop" + }, + "disableLauncher": { + "type": "boolean", + "description": "Whether to disable opening the project in the browser.", + "default": false + } + }, + "actions": [ + { + "name": "test", + "condition": "${project.platform === 'unknown'}", + "then": [ + { + "name": "print", + "semantics": "info", + "message": "No project found in the current directory" + }, + { + "name": "prompt", + "type": "confirmation", + "message": "Start a new project?", + "default": true, + "result": "createProject" + }, + { + "name": "test", + "condition": "${this.createProject}", + "then": [ + { + "name": "define", + "variables": { + "projectName": "${options.name}" + } + }, + { + "name": "import", + "template": "croct://utils/filename-generator", + "options": { + "reference": "projectName" + } + }, + { + "name": "execute-package", + "runner": "npm", + "command": "create-vite@latest", + "arguments": "${[this.projectName, '--template', 'react-ts']}", + "interactions": [ + { + "when": "Ok to proceed", + "then": [ + "y", + "[enter]" + ] + }, + { + "when": "Scaffolding project", + "final": true + }, + { + "when": "Done.", + "final": true + } + ] + }, + { + "name": "change-directory", + "path": "${this.projectName}" + } + ], + "else": { + "name": "fail", + "title": "Project not found", + "message": "Unable to locate a project in the current directory.", + "suggestions": [ + "Navigate to the project directory and try again." + ] + } + } + ], + "else": [ + { + "name": "integrate-croct" + } + ] + }, + { + "name": "add-dependency", + "dependencies": [ + "croct-nanostores", + "nanostores", + "@nanostores/react" + ] + }, + { + "name": "create-resource", + "resources": { + "components": { + "announcement-bar": { + "name": "Announcement bar", + "description": "A small text bar featured at the top of a page.", + "schema": "${import('./configuration/announcement-bar-schema.json')}" + }, + "hero-banner": { + "name": "Hero banner", + "description": "A full-width banner with a title, subtitle, and call-to-action.", + "schema": "${import('./configuration/hero-banner-schema.json')}" + }, + "product-cards": { + "name": "Product cards", + "description": "A section featuring personalized product recommendations.", + "schema": "${import('./configuration/product-cards-schema.json')}" + } + }, + "slots": { + "announcement-bar": { + "name": "Announcement bar", + "component": "announcement-bar", + "content": { + "en": "${import('./configuration/announcement-bar-content.en.json')}" + } + }, + "home-hero-banner": { + "name": "Home hero banner", + "component": "hero-banner", + "content": { + "en": "${import('./configuration/hero-banner-content.en.json')}" + } + }, + "recommended-products": { + "name": "Recommended products", + "component": "product-cards", + "content": { + "en": "${import('./configuration/product-cards-content.en.json')}" + } + } + } + }, + "result": { + "slots": { + "announcement-bar": { + "id": "announcementBarSlotId", + "version": "announcementBarSlotVersion" + }, + "home-hero-banner": { + "id": "heroBannerSlotId", + "version": "heroBannerSlotVersion" + }, + "recommended-products": { + "id": "productsSlotId", + "version": "productsSlotVersion" + } + } + } + }, + { + "name": "add-slot", + "slots": [ + "${this.announcementBarSlotId}@${this.announcementBarSlotVersion}", + "${this.heroBannerSlotId}@${this.heroBannerSlotVersion}", + "${this.productsSlotId}@${this.productsSlotVersion}" + ] + }, + { + "name": "download", + "source": "code/stores/croct.ts", + "destination": "${project.path.source}/stores" + }, + { + "name": "download", + "source": "code/stores/banner.ts", + "destination": "${project.path.source}/stores" + }, + { + "name": "download", + "source": "code/stores/products.ts", + "destination": "${project.path.source}/stores" + }, + { + "name": "download", + "source": "code/stores/cart.ts", + "destination": "${project.path.source}/stores" + }, + { + "name": "download", + "source": "code/components/AnnouncementBar.tsx", + "destination": "${project.path.source}/components" + }, + { + "name": "download", + "source": "code/components/HeroBanner.tsx", + "destination": "${project.path.source}/components" + }, + { + "name": "download", + "source": "code/components/ProductGrid.tsx", + "destination": "${project.path.source}/components" + }, + { + "name": "download", + "source": "code/components/Cart.tsx", + "destination": "${project.path.source}/components" + }, + { + "name": "download", + "source": "code/App.tsx", + "destination": "${project.path.source}", + "overwrite": true + }, + { + "name": "download", + "source": "code/main.tsx", + "destination": "${project.path.source}", + "overwrite": true + }, + { + "name": "download", + "source": "code/index.css", + "destination": "${project.path.source}", + "overwrite": true + }, + { + "name": "replace-file-content", + "files": [ + { + "path": "${project.path.source}/stores/croct.ts", + "replacements": [ + { + "pattern": "%appId%", + "value": "${project.workspace.applicationId}" + } + ] + }, + { + "path": "${project.path.source}/stores/banner.ts", + "replacements": [ + { + "pattern": "%announcementBarSlotId%", + "value": "${this.announcementBarSlotId}" + }, + { + "pattern": "%announcementBarSlotVersion%", + "value": "${this.announcementBarSlotVersion}" + }, + { + "pattern": "%heroBannerSlotId%", + "value": "${this.heroBannerSlotId}" + }, + { + "pattern": "%heroBannerSlotVersion%", + "value": "${this.heroBannerSlotVersion}" + } + ] + }, + { + "path": "${project.path.source}/stores/products.ts", + "replacements": [ + { + "pattern": "%productsSlotId%", + "value": "${this.productsSlotId}" + }, + { + "pattern": "%productsSlotVersion%", + "value": "${this.productsSlotVersion}" + } + ] + } + ] + }, + { + "name": "print", + "semantics": "success", + "message": "Ecommerce project with Nanostores successfully set up." + }, + { + "name": "test", + "condition": "${!options.disableLauncher}", + "then": [ + { + "name": "import", + "template": "croct://utils/example-launcher" + } + ] + } + ] +}