Here is the start of an ecommerce site using umai.js
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Termoclima Pellet Shop - umai</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary: #e63946;
--primary-dark: #c1121f;
--secondary: #1d3557;
--accent: #457b9d;
--light: #f1faee;
--gray: #6c757d;
--gray-light: #e9ecef;
--success: #2a9d8f;
--warning: #e9c46a;
--shadow: 0 2px 8px rgba(0,0,0,0.1);
--shadow-lg: 0 4px 16px rgba(0,0,0,0.15);
--radius: 8px;
--radius-lg: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, sans-serif;
background: var(--light);
color: var(--secondary);
line-height: 1.6;
}
.header {
background: linear-gradient(135deg, var(--secondary), var(--accent));
color: white;
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-lg);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { display: flex; align-items: center; gap: 0.75rem; }
.logo-icon { font-size: 1.75rem; }
.logo h1 { font-size: 1.4rem; font-weight: 700; }
.cart-btn {
background: var(--primary);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: var(--radius);
cursor: pointer;
font-weight: 600;
font-size: 0.95rem;
transition: background 0.2s;
}
.cart-btn:hover { background: var(--primary-dark); }
.hero {
background: linear-gradient(135deg, var(--secondary), var(--accent));
color: white;
padding: 3rem 2rem;
text-align: center;
}
.hero h2 { font-size: 2rem; margin-bottom: 0.75rem; }
.hero p { opacity: 0.9; max-width: 500px; margin: 0 auto; }
.categories {
background: white;
padding: 1.25rem 2rem;
box-shadow: var(--shadow);
position: sticky;
top: 70px;
z-index: 50;
}
.categories-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
.category-btn {
background: var(--gray-light);
border: 2px solid transparent;
padding: 0.6rem 1.25rem;
border-radius: var(--radius-lg);
cursor: pointer;
font-weight: 500;
font-size: 0.9rem;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.4rem;
}
.category-btn:hover { background: var(--accent); color: white; }
.category-btn.active { background: var(--primary); color: white; }
.products-section { max-width: 1400px; margin: 0 auto; padding: 2rem; }
.products-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.products-header h3 { font-size: 1.4rem; }
.products-count { color: var(--gray); }
.loading { text-align: center; padding: 3rem; color: var(--gray); }
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.5rem;
}
.product-card {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover { transform: translateY(-4px); box-shadow: var(--shadow-lg); }
.product-image { width: 100%; height: 180px; object-fit: cover; background: var(--gray-light); }
.product-info { padding: 1.25rem; }
.product-category { font-size: 0.7rem; color: var(--accent); text-transform: uppercase; font-weight: 600; letter-spacing: 0.5px; }
.product-name { font-size: 1.1rem; font-weight: 700; margin: 0.4rem 0; }
.product-description { color: var(--gray); font-size: 0.85rem; margin-bottom: 0.4rem; }
.product-specs { color: var(--accent); font-size: 0.8rem; font-weight: 500; }
.product-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--gray-light);
}
.product-price { font-size: 1.3rem; font-weight: 700; color: var(--primary); }
.add-to-cart {
background: var(--success);
color: white;
border: none;
padding: 0.6rem 1rem;
border-radius: var(--radius);
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
transition: background 0.2s;
}
.add-to-cart:hover { background: #238b7e; }
.cart-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
z-index: 200;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.cart-overlay.open { opacity: 1; visibility: visible; }
.cart-sidebar {
position: fixed;
top: 0;
right: -380px;
width: 380px;
max-width: 100%;
height: 100%;
background: white;
z-index: 201;
transition: right 0.3s;
display: flex;
flex-direction: column;
}
.cart-sidebar.open { right: 0; }
.cart-header {
padding: 1.25rem;
background: var(--secondary);
color: white;
display: flex;
justify-content: space-between;
align-items: center;
}
.cart-header h3 { font-size: 1.15rem; }
.close-cart { background: none; border: none; color: white; font-size: 1.4rem; cursor: pointer; }
.cart-items { flex: 1; overflow-y: auto; padding: 1rem; }
.cart-item { display: flex; gap: 1rem; padding: 1rem; border-bottom: 1px solid var(--gray-light); }
.cart-item img { width: 55px; height: 55px; object-fit: cover; border-radius: var(--radius); }
.cart-item-info { flex: 1; }
.cart-item-name { font-weight: 600; font-size: 0.9rem; }
.cart-item-price { color: var(--primary); font-weight: 600; font-size: 0.85rem; }
.cart-item-qty { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.4rem; }
.qty-btn { background: var(--gray-light); border: none; width: 26px; height: 26px; border-radius: 4px; cursor: pointer; font-weight: 600; }
.qty-btn:hover { background: var(--accent); color: white; }
.remove-item { background: var(--primary); color: white; border: none; padding: 0.2rem 0.4rem; border-radius: 4px; cursor: pointer; font-size: 0.7rem; margin-left: 0.5rem; }
.cart-footer { padding: 1.25rem; border-top: 2px solid var(--gray-light); background: var(--light); }
.cart-total { display: flex; justify-content: space-between; font-size: 1.15rem; font-weight: 700; margin-bottom: 1rem; }
.checkout-btn { width: 100%; background: var(--primary); color: white; border: none; padding: 0.9rem; border-radius: var(--radius); font-size: 1rem; font-weight: 600; cursor: pointer; }
.checkout-btn:hover { background: var(--primary-dark); }
.empty-cart { text-align: center; padding: 2.5rem; color: var(--gray); }
.empty-cart-icon { font-size: 3rem; margin-bottom: 0.75rem; }
.footer { background: var(--secondary); color: white; padding: 2.5rem 2rem; margin-top: 3rem; }
.footer-content { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 2rem; }
.footer-section h4 { font-size: 1rem; margin-bottom: 0.75rem; color: var(--warning); }
.footer-section p, .footer-section a { color: rgba(255,255,255,0.8); text-decoration: none; display: block; margin-bottom: 0.4rem; font-size: 0.9rem; }
.footer-section a:hover { color: white; }
.footer-bottom { text-align: center; padding-top: 1.5rem; margin-top: 1.5rem; border-top: 1px solid rgba(255,255,255,0.2); color: rgba(255,255,255,0.6); font-size: 0.85rem; }
.toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--success);
color: white;
padding: 0.9rem 1.75rem;
border-radius: var(--radius);
box-shadow: var(--shadow-lg);
z-index: 300;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
.toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
@media (max-width: 768px) {
.header-content { flex-direction: column; gap: 0.75rem; }
.hero h2 { font-size: 1.5rem; }
.products-grid { grid-template-columns: 1fr; }
.cart-sidebar { width: 100%; right: -100%; }
.categories-content { overflow-x: auto; flex-wrap: nowrap; justify-content: flex-start; }
.category-btn { white-space: nowrap; }
}
</style>
</head>
<body>
<div id="app"></div>
<script>
!function(e,l){"object"==typeof exports&&"undefined"!=typeof module?l(exports):"function"==typeof define&&define.amd?define(["exports"],l):l(e.umai={})}(this,(function(e){let l=void 0,n=[],t={},f=Array.isArray,r=e=>"string"==typeof e,i=e=>"function"==typeof e,o=e=>null!==e&&!f(e)&&"object"==typeof e,u=e=>null==e?e:e.key,s=(e,n,t,f)=>{if("dom"===n);else if(n in e&&"list"!==n&&"form"!==n&&"style"!==n){if("o"===n[0]&&"n"===n[1]){let e,n=t;t=t=>(e=n(t))instanceof Promise?e.finally(n=>(f(),e=l)):(f(),e=l)}e[n]=t}else null==t||!1===t?e.removeAttribute(n):e.setAttribute(n,"style"===n&&o(t)?(r=t,Object.entries(r).map(([e,l])=>e.replace(/[A-Z]/g,e=>"-"+e.toLowerCase())+":"+l).join(";")):t);var r},p=e=>!0!==e&&!1!==e&&e?e:{type:3,tag:""},c=(e,n)=>{let f,r=e.tag.memo,o=n?n.l:l,u=e.l;return f=o&&r&&!((e,l)=>{for(let n in e)if(e[n]!==l[n])return!0;for(let n in l)if(e[n]!==l[n])return!0})(o,u)?n.t:e.tag({...u,children:e.children},o||t),"="===f.tag?f=n.t:i(f)&&(e.type=4,f.memo=r,f={...e,type:2,tag:f}),f.key=e.key,f},y=(e,n)=>{if(2===e.type||4===e.type)return e.i=((e,l)=>y(e.t=c(e),l))(e,n);let t,f=e.l,r=3===e.type?document.createTextNode(e.tag):document.createElement(e.tag);for(t in f)s(r,t,f[t],n);if(1===e.type)for(t=0;t<e.children.length;t++)r.appendChild(y(e.children[t]=p(e.children[t]),n));return e.rm=f?i(f.dom)&&f.dom(r):l,e.i=r},d=(e,n=[])=>{if(e.children!==l)for(let l=0;l<e.children.length;l++)d(e.children[l],n);if(e.t!==l)d(e.t,n);else if(i(e.rm)){let l=e.rm(e.i);l&&i(l.then)&&(n.o=!0),n.push(l)}return n},a=(e,l)=>{let n=n=>e.removeChild(l.i),t=d(l);t.o?Promise.all(t).finally(n):n()},m=(e,l,n,t,f)=>{if(null!=n&&n.tag===t.tag&&(t.rm=n.rm),n===t);else if(null!=n&&3===n.type&&3===t.type)n.tag!==t.tag&&(l.nodeValue=t.tag);else if(null==n||n.tag!==t.tag)l=e.insertBefore(y(t=p(t),f),l),null!=n&&a(e,n);else if(4===n.type&&n.tag===t.tag)t.type=4,t.t={...n.t,l:t.l,children:t.children},m(e,l,n.t,t.t,f);else if(2===n.type&&n.tag===t.tag)t.t=c(t,n),m(e,l,n.t,t.t,f);else{let e,r,i,o,c=n.l,d=t.l,g=n.children,h=t.children,b=0,j=0,k=g.length-1,v=h.length-1;for(let e in{...c,...d})("value"===e||"selected"===e||"checked"===e?l[e]:c[e])!==d[e]&&s(l,e,d[e],f);for(;j<=v&&b<=k&&null!=(i=u(g[b]))&&i===u(h[j]);)m(l,g[b].i,g[b],h[j]=p(h[j++],g[b++]),f);for(;j<=v&&b<=k&&null!=(i=u(g[k]))&&i===u(h[v]);)m(l,g[k].i,g[k],h[v]=p(h[v--],g[k--]),f);if(b>k)for(;j<=v;)l.insertBefore(y(h[j]=p(h[j++]),f),(r=g[b])&&r.i);else if(j>v)for(;b<=k;)a(l,g[b++]);else{let n={},t={};for(let e=b;e<=k;e++)null!=(i=u(g[e]))&&(n[i]=g[e]);for(;j<=v;)i=u(r=g[b]),o=u(h[j]=p(h[j])),t[i]||null!=o&&o===u(g[b+1])?(null==i&&a(l,r),b++):null==o?(null==i&&(m(l,r&&r.i,r,h[j],f),j++),b++):(i===o?(m(l,r.i,r,h[j],f),t[o]=!0,b++):null!=(e=n[o])?(m(l,l.insertBefore(e.i,r&&r.i),e,h[j],f),t[o]=!0):m(l,r&&r.i,null,h[j],f),j++);for(;b<=k;)null==u(r=g[b++])&&a(l,r);for(let e in n)null==t[e]&&a(l,n[e])}}return t.i=l},g=(e,l)=>{if(f(e))for(let n=0;n<e.length;n++)g(e[n],l);else r(e)||"number"==typeof e?l.push({type:3,tag:e}):l.push(e)};function h(e,...l){let n,t,u,s,p={},c=[],y=l[0],d=i(e)?2:1;if(null==y&&l.shift(),l.length>0&&o(y)&&!f(y.children)&&([{key:n,...p},...l]=l),r(e)){if(p.className&&(p.class=p.className),[e,...s]=e.split("."),s=s.join(" "),o(u=p.class))for(t in u)u[t]&&(s&&(s+=" "),s+=t);r(u)&&(s+=s?u?" "+u:"":u),s&&(p.class=s)}return g(l,c),"["===e?c:{type:d,tag:e,key:n,l:p,children:c}}h.retain=e=>h("="),e.m=h,e.memo=e=>(e.memo=1)&&e,e.mount=function(e,l){e.innerHTML="<a></a>";let t={type:1,i:e=e.lastChild},f=n=>e=m(e.parentNode,e,t,t=l(),r),r=e=>requestAnimationFrame(f);return n.push(r),r()&&r},e.redraw=e=>n.map(e=>e()),e.reset=e=>n=[]}));
</script>
<!-- Load umai from esm.sh CDN -->
<script type="module">
// umai exports: m, mount, redraw (NO state)
const { m, mount, redraw } = umai
// ============================================
// API
// ============================================
const API_URL = 'https://68fe03397c700772bb128814.mockapi.io/products';
// Categories with BARBECUE added
const defaultCategories = [
{ id: 'all', name: 'Tutti', icon: '🏠' },
{ id: 'PELLET', name: 'PELLET', icon: '🔥' },
{ id: 'LEGNA', name: 'LEGNA', icon: '🪵' },
{ id: 'STUFE', name: 'STUFE', icon: '🏠' },
{ id: 'CAMINI', name: 'CAMINI', icon: '🔥' },
{ id: 'BARBECUE', name: 'BARBECUE', icon: '🍖' }
];
// ============================================
// State (plain object + redraw)
// ============================================
const store = {
products: [],
categories: defaultCategories,
loading: true,
activeCategory: 'all',
cart: [],
cartOpen: false,
toastMessage: '',
toastVisible: false
};
// ============================================
// Actions
// ============================================
async function fetchProducts() {
store.loading = true;
redraw();
try {
const res = await fetch(API_URL);
const data = await res.json();
store.products = data;
// Dynamically build categories from API data
const uniqueCats = [...new Set(data.map(p => p.category))];
const catIcons = {
'PELLET': '🔥',
'LEGNA': '🪵',
'STUFE': '🏠',
'CAMINI': '🔥',
'BARBECUE': '🍖',
'ELETTRODOMESTICI': '🔌'
};
store.categories = [
{ id: 'all', name: 'Tutti', icon: '🏠' },
...uniqueCats.map(cat => ({
id: cat,
name: cat,
icon: catIcons[cat] || '📦'
}))
];
} catch (e) {
console.error('Fetch error:', e);
}
store.loading = false;
redraw();
}
function getFiltered() {
const cat = store.activeCategory;
return cat === 'all' ? store.products : store.products.filter(p => p.category === cat);
}
function setCategory(id) {
store.activeCategory = id;
redraw();
}
function addToCart(product) {
const existing = store.cart.find(i => i.id === product.id);
if (existing) {
existing.qty += 1;
} else {
store.cart.push({ ...product, qty: 1 });
}
showToast(product.name + ' aggiunto!');
redraw();
}
function removeFromCart(id) {
store.cart = store.cart.filter(i => i.id !== id);
redraw();
}
function updateQty(id, delta) {
const item = store.cart.find(i => i.id === id);
if (item) {
item.qty += delta;
if (item.qty <= 0) {
removeFromCart(id);
return;
}
}
redraw();
}
function openCart() {
store.cartOpen = true;
redraw();
}
function closeCart() {
store.cartOpen = false;
redraw();
}
function getTotal() {
return store.cart.reduce((s, i) => s + i.price * i.qty, 0);
}
function getCount() {
return store.cart.reduce((s, i) => s + i.qty, 0);
}
let toastTimer;
function showToast(msg) {
store.toastMessage = msg;
store.toastVisible = true;
redraw();
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
store.toastVisible = false;
redraw();
}, 2500);
}
function fmt(price, cur = '€') {
return cur + price.toFixed(2);
}
// ============================================
// Components
// ============================================
const Header = () =>
m('header.header',
m('div.header-content',
m('div.logo',
m('span.logo-icon', '🔥'),
m('h1', 'Termo Clima Shop')
),
m('button.cart-btn', { onclick: openCart },
'🛒 Carrello (' + getCount() + ')'
)
)
);
const Hero = () =>
m('section.hero',
m('h2', 'Benvenuti da Termoclima'),
m('p', 'Pellet, legna, stufe, camini e barbecue di qualità.')
);
const Categories = () =>
m('section.categories',
m('div.categories-content',
store.categories.map(c =>
m('button.category-btn', {
class: store.activeCategory === c.id ? 'active' : '',
onclick: () => setCategory(c.id)
}, m('span', c.icon), ' ', c.name)
)
)
);
const ProductCard = (p) =>
m('article.product-card',
m('img.product-image', {
src: p.image_url,
alt: p.name,
onerror: e => { e.target.src = 'https://placehold.co/400x300?text=' + encodeURIComponent(p.name); }
}),
m('div.product-info',
m('span.product-category', p.category),
m('h4.product-name', p.name),
m('p.product-description', p.description),
p.certificate ? m('p.product-specs', '✓ ' + p.certificate) : null,
m('div.product-footer',
m('span.product-price', fmt(p.price, p.currency)),
m('button.add-to-cart', { onclick: () => addToCart(p) }, '+ Aggiungi')
)
)
);
const ProductGrid = () => {
const filtered = getFiltered();
const cat = store.activeCategory;
return m('section.products-section',
m('div.products-header',
m('h3', cat === 'all' ? 'Tutti i Prodotti' : cat),
m('span.products-count', filtered.length + ' prodotti')
),
store.loading
? m('div.loading', '⏳ Caricamento...')
: m('div.products-grid', filtered.map(p => ProductCard(p)))
);
};
const CartItem = (item) =>
m('div.cart-item',
m('img', {
src: item.image_url,
alt: item.name,
onerror: e => { e.target.src = 'https://placehold.co/60x60?text=IMG'; }
}),
m('div.cart-item-info',
m('div.cart-item-name', item.name),
m('div.cart-item-price', fmt(item.price, item.currency)),
m('div.cart-item-qty',
m('button.qty-btn', { onclick: () => updateQty(item.id, -1) }, '-'),
m('span', ' ' + item.qty + ' '),
m('button.qty-btn', { onclick: () => updateQty(item.id, 1) }, '+'),
m('button.remove-item', { onclick: () => removeFromCart(item.id) }, '✕')
)
)
);
const Cart = () => [
m('div.cart-overlay', {
class: store.cartOpen ? 'open' : '',
onclick: closeCart
}),
m('aside.cart-sidebar', { class: store.cartOpen ? 'open' : '' },
m('div.cart-header',
m('h3', '🛒 Carrello'),
m('button.close-cart', { onclick: closeCart }, '✕')
),
m('div.cart-items',
store.cart.length === 0
? m('div.empty-cart',
m('div.empty-cart-icon', '🛒'),
m('p', 'Carrello vuoto')
)
: store.cart.map(i => CartItem(i))
),
store.cart.length > 0
? m('div.cart-footer',
m('div.cart-total',
m('span', 'Totale:'),
m('span', fmt(getTotal()))
),
m('button.checkout-btn', 'Checkout')
)
: null
)
];
const Footer = () =>
m('footer.footer',
m('div.footer-content',
m('div.footer-section',
m('h4', 'Termo Clima Shop'),
m('p', 'Riscaldamento e barbecue di qualità.')
),
m('div.footer-section',
m('h4', 'Contatti'),
m('p', '📍 Via Roma 123, Milano'),
m('p', '📞 +39 02 1234567')
),
m('div.footer-section',
m('h4', 'Orari'),
m('p', 'Lun-Ven: 9-18'),
m('p', 'Sab: 9-13')
)
),
m('div.footer-bottom',
m('p', '© 2025 Termo Clima - Built with umai 🍜')
)
);
const Toast = () =>
m('div.toast', { class: store.toastVisible ? 'show' : '' }, store.toastMessage);
// ============================================
// App
// ============================================
const App = () =>
m('div',
Header(),
Hero(),
Categories(),
ProductGrid(),
Cart(),
Footer(),
Toast()
);
// ============================================
// Mount and fetch
// ============================================
mount(document.getElementById('app'), App);
fetchProducts();
console.log('🍜 Termoclima Shop powered by umai');
</script>
</body>
</html>
Top comments (0)