Files
Youssef 9f907ad38e Marketing · make Worked-examples cards clickable, open in new tab
Each .ex-card div is now an <a target="_blank" rel="noopener"> wrapping the same content. The hover-play video, prompt line, and .ex-card styles all work unchanged (the class already had color: inherit + focus-visible). Added one CSS line — text-decoration: none on .ex-card — so the <a> doesn't underline the card.

Linked destinations:
- examples/tally/        — SaaS, modern-minimal (from /Users/youssef/test-hallmark-9)
- examples/wayfare/      — travel booking, atmospheric (from test-hallmark-12)
- _tests/09-slow-pour/   — small-batch coffee subscription (already in site)
- examples/bananastudio/ — creative studio with playful motion (from test-hallmark-6)
- _tests/06-anya-portfolio/ — software architect personal site (already in site)
- examples/najm/         — Moroccan fashion (from test-hallmark-7)
- _tests/11-soroe-ceramics/ — small ceramics studio (already in site)
- examples/hyperlane/    — developer infrastructure (from test-hallmark-15)

site/examples/ adds ~308 KB total across the 5 copied projects. The other 3 already live under site/_tests/ so no copying needed for them.
2026-05-18 16:23:29 +01:00

134 lines
4.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// NAJM · cart drawer + add-to-bag micro
const $ = (s, p = document) => p.querySelector(s);
const $$ = (s, p = document) => Array.from(p.querySelectorAll(s));
const drawer = $("#drawer");
const scrim = $("#scrim");
const cartBtn = $("#cartBtn");
const cartCount = $("#cartCount");
const cartTotal = $("#cartTotal");
const cartLines = $("#cartLines");
const cartFree = $("#cartFree");
const state = {
items: [
{ id: "atlas", name: "Atlas Wide-Leg Trouser", variant: "Bone · 32", price: 980, qty: 1, surface: "bone" },
{ id: "medina", name: "Médina Cropped Jacket", variant: "Terracotta · M", price: 1450, qty: 1, surface: "terracotta" },
],
};
const fmt = (n) => `${n.toLocaleString("fr-MA")} MAD`;
const FREE_OVER = 1500;
function render() {
cartLines.innerHTML = state.items.map(item => `
<div class="drawer__line" data-id="${item.id}">
<div class="drawer__thumb">
<div class="surface surface--${item.surface}"></div>
<div class="zellige${item.surface === "bone" ? " zellige--dark" : ""}"></div>
</div>
<div>
<div class="drawer__name">${item.name}</div>
<div class="drawer__sub">${item.variant}</div>
<div class="drawer__qty" role="group" aria-label="Quantity for ${item.name}">
<button data-act="dec" aria-label="Decrease"></button>
<span aria-live="polite">${item.qty}</span>
<button data-act="inc" aria-label="Increase">+</button>
</div>
</div>
<div class="drawer__price">${fmt(item.price * item.qty)}</div>
</div>
`).join("");
const subtotal = state.items.reduce((s, i) => s + i.price * i.qty, 0);
const count = state.items.reduce((s, i) => s + i.qty, 0);
cartCount.textContent = count;
cartTotal.textContent = fmt(subtotal);
const remain = FREE_OVER - subtotal;
cartFree.textContent = remain > 0
? `${fmt(remain)} away from free shipping`
: "Free shipping unlocked · ships in 24h";
}
function open() {
drawer.classList.add("is-open");
scrim.classList.add("is-open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
setTimeout(() => $(".drawer__close")?.focus(), 50);
}
function close() {
drawer.classList.remove("is-open");
scrim.classList.remove("is-open");
drawer.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
cartBtn.focus();
}
cartBtn.addEventListener("click", open);
$("#mobileCart")?.addEventListener("click", open);
scrim.addEventListener("click", close);
$(".drawer__close").addEventListener("click", close);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && drawer.classList.contains("is-open")) close();
});
cartLines.addEventListener("click", (e) => {
const btn = e.target.closest("button[data-act]");
if (!btn) return;
const line = btn.closest(".drawer__line");
const item = state.items.find(i => i.id === line.dataset.id);
if (!item) return;
if (btn.dataset.act === "inc") item.qty += 1;
if (btn.dataset.act === "dec") item.qty = Math.max(1, item.qty - 1);
render();
});
// Add-to-bag from product cards
$$("[data-add]").forEach(btn => {
btn.addEventListener("click", (e) => {
e.preventDefault();
const card = btn.closest("[data-product]");
const id = card.dataset.product;
const found = state.items.find(i => i.id === id);
if (found) {
found.qty += 1;
} else {
state.items.push({
id,
name: card.dataset.name,
variant: card.dataset.variant,
price: Number(card.dataset.price),
surface: card.dataset.surface,
qty: 1,
});
}
render();
cartCount.classList.remove("is-pulse");
void cartCount.offsetWidth; // restart animation
cartCount.classList.add("is-pulse");
btn.textContent = "Added ✓";
setTimeout(() => { btn.textContent = btn.dataset.label || "Add to bag"; }, 1400);
});
});
// Newsletter — inline confirmation, no toast
$("#news")?.addEventListener("submit", (e) => {
e.preventDefault();
const input = e.target.querySelector("input");
const note = $("#newsNote");
if (!input.value.includes("@")) {
note.textContent = "Add a valid email to subscribe.";
note.style.color = "var(--color-accent-deep)";
input.focus();
return;
}
note.textContent = `${input.value} is on the list — first drop ships Friday.`;
note.style.color = "var(--color-ink)";
input.value = "";
});
render();