diff --git a/docs/_includes/docs/hero.html b/docs/_includes/docs/hero.html index 5a417b1f..9331c382 100644 --- a/docs/_includes/docs/hero.html +++ b/docs/_includes/docs/hero.html @@ -24,7 +24,9 @@ {% endif %} -
- {% include website/carbon.html %} -
+ {% unless include.hide_carbon == true %} +
+ {% include website/carbon.html %} +
+ {% endunless %} diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index c5cd131f..1dab741f 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -12,8 +12,11 @@ {% endfor %} {% endif %} - {% include global/support.html %} - {% include global/native.html %} + {% unless page.hide_footer %} + {% include global/support.html %} + {% include global/native.html %} + {% endunless %} + {% include global/about.html %} diff --git a/docs/assets/css/main.css b/docs/assets/css/main.css index f3e28544..ae1bb9ca 100644 --- a/docs/assets/css/main.css +++ b/docs/assets/css/main.css @@ -1,15 +1,3 @@ -:root { - --zlog-h: 221deg; - --zlog-s: 40%; - --zlog-l: 30%; - --zlog: hsl(var(--zlog-h), var(--zlog-s), var(--zlog-l)); -} - -.zlog { - color: var(--zlog); - display: none; -} - body { align-content: flex-start; /* display: grid; */ diff --git a/docs/assets/javascript/shop.js b/docs/assets/javascript/shop.js new file mode 100644 index 00000000..91aa6a85 --- /dev/null +++ b/docs/assets/javascript/shop.js @@ -0,0 +1,1053 @@ +document.addEventListener("DOMContentLoaded", () => { + // Utils + const isObject = (obj) => { + return obj !== null && typeof obj === "object" && !Array.isArray(obj); + }; + + const isEmpty = (obj) => { + return Object.keys(obj).length === 0; + }; + + const humanizeGraphQLResponse = (input) => { + if (!input) return null; + const output = {}; + + Object.keys(input).forEach((key) => { + if (input[key] && input[key].edges) { + output[key] = input[key].edges.map((edge) => + humanizeGraphQLResponse(edge.node), + ); + } else if (isObject(input[key])) { + output[key] = humanizeGraphQLResponse(input[key]); + } else { + output[key] = input[key]; + } + }); + + return output; + }; + + const formatPrice = (price) => { + const { amount, currencyCode } = price; + return `${CURRENCIES[currencyCode]}${Math.trunc(amount)}`; + }; + + const getId = (id) => { + const parts = id.split("/"); + return parts[parts.length - 1]; + }; + + const getProductFromVariant = (variantId) => { + let product = {}; + let variant = {}; + + state.products.forEach((p) => { + const foundVariant = p.variants.find((v) => { + return v.id === variantId; + }); + + if (foundVariant) { + product = p; + variant = foundVariant; + } + }); + + return { + product, + variant, + }; + }; + + const STORAGE_CART_ID = "bulma-shop-cart-id"; + + const CURRENCIES = { + AED: "د.إ", + AFN: "؋", + ALL: "L", + AMD: "֏", + ANG: "ƒ", + AOA: "Kz", + ARS: "$", + AUD: "$", + AWG: "ƒ", + AZN: "₼", + BAM: "KM", + BBD: "$", + BDT: "৳", + BGN: "лв", + BHD: ".د.ب", + BIF: "FBu", + BMD: "$", + BND: "$", + BOB: "$b", + BOV: "BOV", + BRL: "R$", + BSD: "$", + BTC: "₿", + BTN: "Nu.", + BWP: "P", + BYN: "Br", + BYR: "Br", + BZD: "BZ$", + CAD: "$", + CDF: "FC", + CHE: "CHE", + CHF: "CHF", + CHW: "CHW", + CLF: "CLF", + CLP: "$", + CNH: "¥", + CNY: "¥", + COP: "$", + COU: "COU", + CRC: "₡", + CUC: "$", + CUP: "₱", + CVE: "$", + CZK: "Kč", + DJF: "Fdj", + DKK: "kr", + DOP: "RD$", + DZD: "دج", + EEK: "kr", + EGP: "£", + ERN: "Nfk", + ETB: "Br", + ETH: "Ξ", + EUR: "€", + FJD: "$", + FKP: "£", + GBP: "£", + GEL: "₾", + GGP: "£", + GHC: "₵", + GHS: "GH₵", + GIP: "£", + GMD: "D", + GNF: "FG", + GTQ: "Q", + GYD: "$", + HKD: "$", + HNL: "L", + HRK: "kn", + HTG: "G", + HUF: "Ft", + IDR: "Rp", + ILS: "₪", + IMP: "£", + INR: "₹", + IQD: "ع.د", + IRR: "﷼", + ISK: "kr", + JEP: "£", + JMD: "J$", + JOD: "JD", + JPY: "¥", + KES: "KSh", + KGS: "лв", + KHR: "៛", + KMF: "CF", + KPW: "₩", + KRW: "₩", + KWD: "KD", + KYD: "$", + KZT: "₸", + LAK: "₭", + LBP: "£", + LKR: "₨", + LRD: "$", + LSL: "M", + LTC: "Ł", + LTL: "Lt", + LVL: "Ls", + LYD: "LD", + MAD: "MAD", + MDL: "lei", + MGA: "Ar", + MKD: "ден", + MMK: "K", + MNT: "₮", + MOP: "MOP$", + MRO: "UM", + MRU: "UM", + MUR: "₨", + MVR: "Rf", + MWK: "MK", + MXN: "$", + MXV: "MXV", + MYR: "RM", + MZN: "MT", + NAD: "$", + NGN: "₦", + NIO: "C$", + NOK: "kr", + NPR: "₨", + NZD: "$", + OMR: "﷼", + PAB: "B/.", + PEN: "S/.", + PGK: "K", + PHP: "₱", + PKR: "₨", + PLN: "zł", + PYG: "Gs", + QAR: "﷼", + RMB: "¥", + RON: "lei", + RSD: "Дин.", + RUB: "₽", + RWF: "R₣", + SAR: "﷼", + SBD: "$", + SCR: "₨", + SDG: "ج.س.", + SEK: "kr", + SGD: "S$", + SHP: "£", + SLL: "Le", + SOS: "S", + SRD: "$", + SSP: "£", + STD: "Db", + STN: "Db", + SVC: "$", + SYP: "£", + SZL: "E", + THB: "฿", + TJS: "SM", + TMT: "T", + TND: "د.ت", + TOP: "T$", + TRL: "₤", + TRY: "₺", + TTD: "TT$", + TVD: "$", + TWD: "NT$", + TZS: "TSh", + UAH: "₴", + UGX: "USh", + USD: "$", + UYI: "UYI", + UYU: "$U", + UYW: "UYW", + UZS: "лв", + VEF: "Bs", + VES: "Bs.S", + VND: "₫", + VUV: "VT", + WST: "WS$", + XAF: "FCFA", + XBT: "Ƀ", + XCD: "$", + XOF: "CFA", + XPF: "₣", + XSU: "Sucre", + XUA: "XUA", + YER: "﷼", + ZAR: "R", + ZMW: "ZK", + ZWD: "Z$", + ZWL: "$", + }; + + const CART_QL = ` + id + createdAt + updatedAt + checkoutUrl + buyerIdentity { + countryCode + } + cost { + totalAmount { + amount + currencyCode + } + } + lines(first: 20) { + edges { + node { + id + quantity + cost { + subtotalAmount { + amount + currencyCode + } + totalAmount { + amount + currencyCode + } + } + merchandise { + ... on ProductVariant { + id + } + } + } + } + } + `; + + const COST_QL = ` + cost { + totalAmount { + amount + currencyCode + } + # The estimated amount, before taxes and discounts, for the customer to pay at checkout. + subtotalAmount { + amount + currencyCode + } + # The estimated tax amount for the customer to pay at checkout. + totalTaxAmount { + amount + currencyCode + } + # The estimated duty amount for the customer to pay at checkout. + totalDutyAmount { + amount + currencyCode + } + } + `; + + // State + const state = { + cart: {}, + products: [], + isLoading: false, + hasFetchedProducts: false, + countryCode: null, + }; + + // UI + const $cart = document.getElementById("cart"); + const $cartClose = document.querySelectorAll(".shop-cart-close"); + const $openCart = document.getElementById("open-cart"); + const $emptyCart = document.getElementById("empty-cart"); + const $fullCart = document.getElementById("full-cart"); + const $cartItems = document.getElementById("cart-items"); + const $products = document.getElementById("products"); + const $modal = document.getElementById("shop-modal"); + const $modalClose = document.querySelectorAll(".shop-modal-close"); + + $cartClose.forEach((el) => { + el.addEventListener("click", (event) => { + event.preventDefault(); + $cart.classList.remove("is-active"); + }); + }); + + $openCart.addEventListener("click", (event) => { + event.preventDefault(); + $cart.classList.add("is-active"); + }); + + $modalClose.forEach((el) => { + el.addEventListener("click", (event) => { + event.preventDefault(); + closeModal(); + }); + }); + + document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + $cart.classList.remove("is-active"); + closeModal(); + } + }); + + const closeModal = () => { + $modal.classList.remove("is-active"); + }; + + const openModal = (product) => { + $title = $modal.querySelector(".modal-title"); + $body = $modal.querySelector(".modal-body"); + $buttons = $modal.querySelector(".modal .buttons"); + $close = $modal.querySelector(".modal .buttons .button.is-close"); + + $title.replaceChildren(); + buildHeading($title, product); + + $body.replaceChildren(); + $body.className = `modal-body block shop-product shop-product-${getId(product.id)}`; + + buildDescription($body, product, false); + buildOptions($body, product); + + $buttons.replaceChildren(); + buildAddButton($buttons, product); + + $modal.classList.add("is-active"); + + update(); + }; + + const buildHeading = (el, product) => { + const { priceRange, title } = product; + const { minVariantPrice: min } = priceRange; + + const $heading = El("shop-product-heading"); + + const $h3 = El("shop-product-title", "h3"); + $h3.innerText = title; + $heading.appendChild($h3); + + const $price = El("shop-product-price"); + $price.appendChild(Price(min)); + $heading.appendChild($price); + + el.appendChild($heading); + }; + + const buildSizeGuide = (desc) => { + if (!desc) { + return; + } + + const parts = desc.split('

'); + const first = parts[0]; + + if (parts.length === 1) { + return first; + } + + const items = parts[1].split("Size guide

"); + + if (parts.length === 1) { + return items[0]; + } + + return ` + ${first} +
+ Size Guide + ${items[1]} +
+ `; + }; + + const buildDescription = (el, product) => { + const { descriptionHtml } = product; + + const $description = El("shop-product-description"); + const $tagline = El("shop-product-tagline"); + const $rest = El("shop-product-rest content"); + + const { first, rest } = truncateDescription(descriptionHtml); + + $tagline.innerHTML = first; + $rest.innerHTML = buildSizeGuide(rest); + + $description.appendChild($tagline); + $description.appendChild($rest); + el.appendChild($description); + }; + + const buildOptions = (el, product) => { + const { variants } = product; + + const $options = El("buttons has-addons are-small variants"); + $options.className += variants.length > 1 ? " multiple" : " single"; + + if (variants.length > 1) { + variants.forEach((variant) => { + const { id, title } = variant; + + const $option = El("button", "button"); + $option.dataset.id = id; + $option.innerText = title; + + $option.addEventListener("click", (event) => { + event.preventDefault(); + product.selectedVariant = id; + update(); + }); + + $options.appendChild($option); + }); + + el.appendChild($options); + } + }; + + const buildAddButton = (el, product) => { + const $buy = El("button is-primary is-medium", "button"); + $buy.innerText = "Add to cart"; + + $buy.addEventListener("click", async (event) => { + event.preventDefault(); + await addToCart(product.selectedVariant); + closeModal(); + }); + + el.appendChild($buy); + }; + + // Update Cycle + const updateProducts = () => { + if (state.hasFetchedProducts) { + $products.classList.add("has-loaded"); + } + + if ($products.childElementCount > 4) { + return; + } + + state.products.forEach((product) => { + const { id, availableForSale, featuredImage } = product; + + if (!availableForSale) { + return; + } + + const el = El(`shop-product shop-product-${getId(product.id)}`); + el.dataset.id = id; + + const $figure = El("shop-product-image image is-square", "figure"); + const $img = document.createElement("img"); + $img.src = featuredImage.url; + $figure.appendChild($img); + el.appendChild($figure); + + $figure.addEventListener("click", async (event) => { + event.preventDefault(); + openModal(product); + }); + + buildHeading(el, product); + buildDescription(el, product); + buildOptions(el, product); + + const $buttons = El("shop-product-buttons buttons"); + + buildAddButton($buttons, product); + + const $more = El("button is-text", "button"); + $more.innerText = "Learn more"; + $buttons.appendChild($more); + + $more.addEventListener("click", async (event) => { + event.preventDefault(); + openModal(product); + }); + + el.appendChild($buttons); + + $products.appendChild(el); + }); + }; + + const updateCart = () => { + if (isEmpty(state.cart)) { + return; + } + + const { checkoutUrl, cost, lines } = state.cart; + + if (lines.length > 0) { + $openCart.classList.add("is-primary"); + $cartItems.replaceChildren(); + + $emptyCart.style.display = "none"; + $fullCart.style.display = "block"; + + lines.forEach((line) => { + const variantId = line.merchandise.id; + const { product, variant } = getProductFromVariant(variantId); + + const $item = El("media shop-item"); + $item.dataset.id = line.id; + + const $left = El("media-left"); + const $image = El("shop-item-image image is-64x64"); + const $img = El("", "img"); + + if (product.featuredImage) { + $img.src = product.featuredImage.url; + } + + $image.appendChild($img); + $left.appendChild($image); + + const $right = El("media-content"); + + const $cost = El("shop-item-price"); + $cost.innerText = formatPrice(line.cost.totalAmount); + $right.appendChild($cost); + + const $title = El("shop-item-title"); + $title.innerText = `${product.title}`; + $right.appendChild($title); + + if (variant.title !== "Default Title") { + const $tag = El( + "shop-item-variant button is-primary is-small is-outlined", + "span", + ); + $tag.innerText = `${variant.title}`; + $right.appendChild($tag); + } + + const $quantity = El("shop-item-quantity button is-static", "span"); + $quantity.innerText = `${line.quantity}`; + $right.appendChild($quantity); + + const $buttons = El("shop-item-actions"); + + const $remove = El("button shop-item-remove is-small", "button"); + const $icon = Icon("fa-solid fa-trash-can"); + $remove.appendChild($icon); + $remove.addEventListener("click", async (event) => { + event.preventDefault(); + + if ( + window.confirm( + `Are you sure you want to remove this item from your cart?`, + ) + ) { + await removeFromCart(line.id); + } + }); + $buttons.appendChild($remove); + + const $addons = El("shop-item-buttons buttons are-small has-addons"); + + const $plus = El("button", "button"); + const $plusIcon = Icon("fa-solid fa-plus"); + $plus.appendChild($plusIcon); + $plus.addEventListener("click", async (event) => { + event.preventDefault(); + await addToCart(variant.id); + }); + + const $minus = El("button", "button"); + const $minusIcon = Icon("fa-solid fa-minus"); + $minus.appendChild($minusIcon); + $minus.addEventListener("click", async (event) => { + event.preventDefault(); + + if (line.quantity === 1) { + if ( + window.confirm( + `Are you sure you want to remove this item from your cart?`, + ) + ) { + await removeFromCart(line.id); + } + } else { + await decreaseFromCart(line.id, line.quantity - 1); + } + }); + + $addons.appendChild($minus); + $addons.appendChild($quantity); + $addons.appendChild($plus); + + $buttons.appendChild($addons); + $right.appendChild($buttons); + + $item.appendChild($left); + $item.appendChild($right); + + $cartItems.appendChild($item); + }); + + const $total = El("shop-total"); + const $totalLeft = El("shop-total-left"); + const $totalLabel = El("shop-total-label"); + $totalLabel.innerText = "Total"; + const $disclaimer = El("shop-total-disclaimer"); + $disclaimer.innerText = + "Tax included and shipping and discounts calculated at checkout"; + const $totalRight = El("shop-total-amount"); + $totalRight.innerText = formatPrice(cost.totalAmount); + $totalLeft.appendChild($totalLabel); + $totalLeft.appendChild($disclaimer); + $total.appendChild($totalLeft); + $total.appendChild($totalRight); + $cartItems.appendChild($total); + + const $checkout = El("button is-primary is-fullwidth", "a"); + $checkout.innerText = "Checkout"; + $checkout.href = checkoutUrl; + $cartItems.appendChild($checkout); + } else { + $openCart.classList.remove("is-primary"); + $emptyCart.style.display = "block"; + $fullCart.style.display = "none"; + } + }; + + const updateButtons = () => { + const $buttons = document.querySelectorAll( + "#open-cart, #shop button.button, #shop-modal button.button", + ); + + $buttons.forEach((button) => { + if (state.isLoading) { + button.setAttribute("disabled", ""); + } else { + button.removeAttribute("disabled"); + } + }); + }; + + const updateVariants = () => { + state.products.forEach((product) => { + const $blocs = document.querySelectorAll( + `.shop-product-${getId(product.id)}`, + ); + + $blocs.forEach(($bloc) => { + const $variants = $bloc.querySelectorAll(`.variants .button`); + + $variants.forEach(($el) => { + if ($el.dataset.id === product.selectedVariant) { + $el.classList.add("is-primary"); + } else { + $el.classList.remove("is-primary"); + } + }); + }); + }); + }; + + const update = () => { + updateProducts(); + updateCart(); + updateButtons(); + updateVariants(); + }; + + // HTML Elements + const El = (className = "", tag = "div") => { + const el = document.createElement(tag); + el.className = className; + return el; + }; + + const Icon = (icon) => { + const el = document.createElement("span"); + el.className = "icon"; + const i = document.createElement("i"); + i.className = icon; + el.appendChild(i); + return el; + }; + + const Price = (price) => { + const { amount, currencyCode } = price; + const el = El("shop-price", "span"); + el.innerText = `${CURRENCIES[currencyCode]}${Math.trunc(amount)}`; + return el; + }; + + const truncateDescription = (desc) => { + const parts = desc.split("
\n
\n"); + + return { + first: parts[0], + rest: parts.slice(1).join(" "), + }; + }; + + // API calls + const client = window.ShopifyStorefrontAPIClient.createStorefrontApiClient({ + storeDomain: "8df2f8-d5.myshopify.com", + apiVersion: "2024-04", + publicAccessToken: "e3764a4be9897a2d0531c4b5c2699c9f", + }); + + async function retrieveProducts() { + let context = ""; + + if (state.countryCode) { + context = `@inContext(country: ${state.countryCode})`; + } + + const query = ` + query allProducts ${context} { + products(first: 10) { + edges { + node { + id + availableForSale + description + descriptionHtml + featuredImage { + height + url + width + } + handle + images(first: 10) { + edges { + node { + height + url + width + } + } + } + priceRange { + maxVariantPrice { + amount + currencyCode + } + minVariantPrice { + amount + currencyCode + } + } + title + variants(first: 10) { + edges { + node { + availableForSale + id + price { + amount + currencyCode + } + title + } + } + } + } + } + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.products = clean.products.map((product) => { + return { + ...product, + selectedVariant: product.variants[0].id, + }; + }); + state.hasFetchedProducts = true; + } catch (error) { + console.error("Error fetching products:", error); + } + + state.isLoading = false; + update(); + } + + async function createCart() { + const query = ` + mutation cartCreate { + cartCreate ( + input: {} + ) { + cart { + ${CART_QL} + ${COST_QL} + } + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.cart = clean.cartCreate.cart; + + localStorage.setItem(STORAGE_CART_ID, clean.cartCreate.cart.id); + } catch (error) { + console.error("Error fetching products:", error); + } + + state.isLoading = false; + update(); + } + + async function retrieveCart(cartId) { + const query = ` + { + cart ( + id: "${cartId}" + ) { + ${CART_QL} + ${COST_QL} + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + createCart(); + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.cart = clean.cart; + + if (clean.cart.buyerIdentity.countryCode) { + state.countryCode = clean.cart.buyerIdentity.countryCode; + } + } catch (error) { + console.error("Error fetching products:", error); + createCart(); + } + + state.isLoading = false; + update(); + } + + async function addToCart(productId) { + const query = ` + mutation cartAdd { + cartLinesAdd ( + cartId: "${state.cart.id}" + lines: { + merchandiseId: "${productId}" + quantity: 1 + } + ) { + cart { + ${CART_QL} + ${COST_QL} + } + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.cart = clean.cartLinesAdd.cart; + + $cart.classList.add("is-active"); + } catch (error) { + console.error("Error fetching products:", error); + } + + state.isLoading = false; + update(); + } + + async function removeFromCart(lineId) { + const query = ` + mutation cartAdd { + cartLinesRemove ( + cartId: "${state.cart.id}" + lineIds: ["${lineId}"] + ) { + cart { + ${CART_QL} + ${COST_QL} + } + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.cart = clean.cartLinesRemove.cart; + } catch (error) { + console.error("Error fetching products:", error); + } + + state.isLoading = false; + update(); + } + + async function decreaseFromCart(lineId, quantity) { + const query = ` + mutation cartAdd { + cartLinesUpdate ( + cartId: "${state.cart.id}" + lines: { + id: "${lineId}" + quantity: ${quantity} + } + ) { + cart { + ${CART_QL} + ${COST_QL} + } + } + } + `; + + state.isLoading = true; + update(); + + try { + const { data, errors } = await client.request(query); + + if (errors) { + return console.error(errors); + } + + const clean = humanizeGraphQLResponse(data); + state.cart = clean.cartLinesUpdate.cart; + } catch (error) { + console.error("Error fetching products:", error); + } + + state.isLoading = false; + update(); + } + + // Init + const init = async () => { + const storedCart = localStorage.getItem(STORAGE_CART_ID); + + if (storedCart) { + await retrieveCart(storedCart); + } else { + await createCart(); + } + + await retrieveProducts(); + }; + + init(); +}); diff --git a/docs/shop.html b/docs/shop.html new file mode 100644 index 00000000..fedce3d0 --- /dev/null +++ b/docs/shop.html @@ -0,0 +1,396 @@ +--- +title: "The Bulma Shop" +layout: default +theme: primary +route: shop +hide_footer: true +breadcrumb: + - home + - shop +--- + + + +{% include global/header.html %} + +{% capture shop_placeholder %} +
+
+ +
+ +
+

The Bulma T-Shirt

+
+ $15 +
+
+
+
+ Show your CSS skills and add a little extra motivation with the + official Bulma sticker. A perfect reminder that design can be easy + with your favorite framework. +
+
+
+ +
+
+{% endcapture %} + +
+
+
+

+ The Bulma Shop +

+ +
+ +

+ Get yourself some Bulma swag. +

+ + +
+
+ +
+
+
+ +
+

Cart

+ +
+
+

Your cart is empty!

+ +
+
+ +
+
+
+
+
+ +
+ {{ shop_placeholder }} + {{ shop_placeholder }} + {{ shop_placeholder }} + {{ shop_placeholder }} +
+
+
+ + + + +