Skip to content
Draft
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
33 changes: 33 additions & 0 deletions templates/croct/starter/ecommerce-nanostores/README.md
Original file line number Diff line number Diff line change
@@ -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` |
45 changes: 45 additions & 0 deletions templates/croct/starter/ecommerce-nanostores/code/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="app">
<AnnouncementBar />
<header className="header">
<div className="header-content">
<a href="/" className="logo">
Store
</a>
<nav className="nav">
<a href="#">Products</a>
<a href="#">Categories</a>
<a href="#">About</a>
</nav>
<Cart />
</div>
</header>
<main>
<HeroBanner />
<ProductGrid />
</main>
<footer className="footer">
<p>
Powered by{' '}
<a href="https://croct.com" target="_blank" rel="noopener noreferrer">
Croct
</a>{' '}
&amp;{' '}
<a
href="https://croct-nano.fryuni.dev"
target="_blank"
rel="noopener noreferrer"
>
Nanostores
</a>
</p>
</footer>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useStore } from '@nanostores/react';
import { announcementBar } from '../stores/banner';

export function AnnouncementBar() {
const { content } = useStore(announcementBar);

const bar = (
<div
className="announcement-bar"
style={{
backgroundColor: content.style.backgroundColor,
color: content.style.textColor,
}}
>
{content.text}
</div>
);

if (content.link) {
return (
<a href={content.link} className="announcement-bar-link">
{bar}
</a>
);
}

return bar;
}
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button className="cart-toggle" onClick={() => setIsOpen(!isOpen)}>
Cart ({count})
</button>
{isOpen && (
<div className="cart-panel">
<div className="cart-header">
<h2>Shopping Cart</h2>
<button className="cart-close" onClick={() => setIsOpen(false)}>
&times;
</button>
</div>
{items.length === 0 ? (
<p className="cart-empty">Your cart is empty.</p>
) : (
<>
<ul className="cart-items">
{items.map(item => (
<li key={item.productId} className="cart-item">
<div className="cart-item-info">
<span className="cart-item-name">{item.name}</span>
<span className="cart-item-price">
${(item.unitPrice * item.quantity).toFixed(2)}
</span>
</div>
<div className="cart-item-actions">
<button
onClick={() =>
updateQuantity(
item.productId,
item.quantity - 1,
)
}
>
&minus;
</button>
<span>{item.quantity}</span>
<button
onClick={() =>
updateQuantity(
item.productId,
item.quantity + 1,
)
}
>
+
</button>
<button
className="cart-item-remove"
onClick={() => removeFromCart(item.productId)}
>
Remove
</button>
</div>
</li>
))}
</ul>
<div className="cart-total">
<strong>Total: ${total.toFixed(2)}</strong>
</div>
</>
)}
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useStore } from '@nanostores/react';
import { heroBanner } from '../stores/banner';

export function HeroBanner() {
const { content } = useStore(heroBanner);

return (
<section className="hero-banner">
<div className="hero-content">
<h1>{content.title}</h1>
{content.subtitle && <p className="hero-subtitle">{content.subtitle}</p>}
<a href={content.ctaLink} className="hero-cta">
{content.ctaLabel}
</a>
</div>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<section className="product-section">
{content.title && <h2 className="section-title">{content.title}</h2>}
{content.description && <p className="section-description">{content.description}</p>}
<div className="product-grid">
{content.cards.map((card, index) => (
<div key={index} className="product-card">
{card.image && (
<img src={card.image} alt={card.name} className="product-image" />
)}
<div className="product-info">
<h3 className="product-name">{card.name}</h3>
{card.description && (
<p className="product-description">{card.description}</p>
)}
<p className="product-price">${card.price.toFixed(2)}</p>
{card.cta && (
<button
className="product-cta"
onClick={() =>
addToCart({
productId: card.name.toLowerCase().replace(/\s+/g, '-'),
name: card.name,
unitPrice: card.price,
})
}
>
{card.cta.label}
</button>
)}
</div>
</div>
))}
</div>
</section>
);
}
Loading
Loading