aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cartStore.ts31
-rw-r--r--src/components/AddToCartForm.tsx18
-rw-r--r--src/components/CartFlyout.module.css29
-rw-r--r--src/components/CartFlyout.tsx28
-rw-r--r--src/components/CartFlyoutToggle.tsx7
-rw-r--r--src/components/FigurineDescription.astro44
-rw-r--r--src/layouts/Layout.astro113
-rw-r--r--src/pages/index.astro50
-rw-r--r--src/utils.ts4
9 files changed, 324 insertions, 0 deletions
diff --git a/src/cartStore.ts b/src/cartStore.ts
new file mode 100644
index 000000000..a57a6ce87
--- /dev/null
+++ b/src/cartStore.ts
@@ -0,0 +1,31 @@
+import { atom, map } from 'nanostores';
+
+export const isCartOpen = atom(false);
+
+export type CartItem = {
+ id: string;
+ name: string;
+ imageSrc: string;
+ quantity: number;
+};
+
+export type CartItemDisplayInfo = Pick<CartItem, 'id' | 'name' | 'imageSrc'>;
+
+export const cartItems = map<Record<string, CartItem>>({});
+
+export function addCartItem({ id, name, imageSrc }: CartItemDisplayInfo) {
+ const existingEntry = cartItems.get()[id];
+ if (existingEntry) {
+ cartItems.setKey(id, {
+ ...existingEntry,
+ quantity: existingEntry.quantity + 1,
+ });
+ } else {
+ cartItems.setKey(id, {
+ id,
+ name,
+ imageSrc,
+ quantity: 1,
+ });
+ }
+}
diff --git a/src/components/AddToCartForm.tsx b/src/components/AddToCartForm.tsx
new file mode 100644
index 000000000..7498443f6
--- /dev/null
+++ b/src/components/AddToCartForm.tsx
@@ -0,0 +1,18 @@
+import { isCartOpen, addCartItem } from '../cartStore';
+import type { CartItemDisplayInfo } from '../cartStore';
+import type { ComponentChildren } from 'preact';
+
+type Props = {
+ item: CartItemDisplayInfo;
+ children: ComponentChildren;
+};
+
+export default function AddToCartForm({ item, children }: Props) {
+ function addToCart(e: SubmitEvent) {
+ e.preventDefault();
+ isCartOpen.set(true);
+ addCartItem(item);
+ }
+
+ return <form onSubmit={addToCart}>{children}</form>;
+}
diff --git a/src/components/CartFlyout.module.css b/src/components/CartFlyout.module.css
new file mode 100644
index 000000000..cee43dd4c
--- /dev/null
+++ b/src/components/CartFlyout.module.css
@@ -0,0 +1,29 @@
+.container {
+ position: fixed;
+ right: 0;
+ top: var(--nav-height);
+ height: 100vh;
+ background: var(--color-bg-2);
+ padding-inline: 2rem;
+ min-width: min(90vw, 300px);
+ border-left: 3px solid var(--color-bg-3);
+}
+
+.list {
+ list-style: none;
+ padding: 0;
+}
+
+.listItem {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.listItem * {
+ margin-block: 0.3rem;
+}
+
+.listItemImg {
+ width: 4rem;
+}
diff --git a/src/components/CartFlyout.tsx b/src/components/CartFlyout.tsx
new file mode 100644
index 000000000..98fd8cbfb
--- /dev/null
+++ b/src/components/CartFlyout.tsx
@@ -0,0 +1,28 @@
+import { useStore } from '@nanostores/preact';
+import { cartItems, isCartOpen } from '../cartStore';
+import styles from './CartFlyout.module.css';
+
+export default function CartFlyout() {
+ const $isCartOpen = useStore(isCartOpen);
+ const $cartItems = useStore(cartItems);
+
+ return (
+ <aside hidden={!$isCartOpen} className={styles.container}>
+ {Object.values($cartItems).length ? (
+ <ul className={styles.list} role="list">
+ {Object.values($cartItems).map((cartItem) => (
+ <li className={styles.listItem}>
+ <img className={styles.listItemImg} src={cartItem.imageSrc} alt={cartItem.name} />
+ <div>
+ <h3>{cartItem.name}</h3>
+ <p>Quantity: {cartItem.quantity}</p>
+ </div>
+ </li>
+ ))}
+ </ul>
+ ) : (
+ <p>Your cart is empty!</p>
+ )}
+ </aside>
+ );
+}
diff --git a/src/components/CartFlyoutToggle.tsx b/src/components/CartFlyoutToggle.tsx
new file mode 100644
index 000000000..14ce1c70d
--- /dev/null
+++ b/src/components/CartFlyoutToggle.tsx
@@ -0,0 +1,7 @@
+import { useStore } from '@nanostores/preact';
+import { isCartOpen } from '../cartStore';
+
+export default function CartFlyoutToggle() {
+ const $isCartOpen = useStore(isCartOpen);
+ return <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>;
+}
diff --git a/src/components/FigurineDescription.astro b/src/components/FigurineDescription.astro
new file mode 100644
index 000000000..1294b1510
--- /dev/null
+++ b/src/components/FigurineDescription.astro
@@ -0,0 +1,44 @@
+<h1>Astronaut Figurine</h1>
+<p class="limited-edition-badge">Limited Edition</p>
+<p>
+ The limited edition Astronaut Figurine is the perfect gift for any Astro contributor. This
+ fully-poseable action figurine comes equipped with:
+</p>
+<ul>
+ <li>A fabric space suit with adjustable straps</li>
+ <li>Boots lightly dusted by the lunar surface *</li>
+ <li>An adjustable space visor</li>
+</ul>
+<p>
+ <sub>* Dust not actually from the lunar surface</sub>
+</p>
+
+<style>
+ h1 {
+ margin: 0;
+ margin-block-start: 2rem;
+ }
+
+ .limited-edition-badge {
+ font-weight: 700;
+ text-transform: uppercase;
+ background-image: linear-gradient(0deg, var(--astro-blue), var(--astro-pink));
+ background-size: 100% 200%;
+ background-position-y: 100%;
+ border-radius: 0.4rem;
+ animation: pulse 4s ease-in-out infinite;
+ display: inline-block;
+ color: white;
+ padding: 0.2rem 0.4rem;
+ }
+
+ @keyframes pulse {
+ 0%,
+ 100% {
+ background-position-y: 0%;
+ }
+ 50% {
+ background-position-y: 80%;
+ }
+ }
+</style>
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
new file mode 100644
index 000000000..aa8c34773
--- /dev/null
+++ b/src/layouts/Layout.astro
@@ -0,0 +1,113 @@
+---
+import CartFlyout from '../components/CartFlyout';
+import CartFlyoutToggle from '../components/CartFlyoutToggle';
+import { withBase } from '../utils';
+
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <header>
+ <nav>
+ <a href={withBase('/')} class="nav-header">
+ <span style="color: var(--astro-blue)">Astro</span> storefront
+ </a>
+ <CartFlyoutToggle client:load />
+ </nav>
+ </header>
+ <slot />
+ <CartFlyout client:load />
+ </body>
+</html>
+
+<style is:global>
+ :root {
+ --font-family: system-ui, sans-serif;
+ --font-size-base: clamp(1rem, 0.34vw + 0.91rem, 1.19rem);
+ --font-size-lg: clamp(1.2rem, 0.7vw + 1.2rem, 1.5rem);
+ --font-size-xl: clamp(2rem, 1.75vw + 1.35rem, 2.75rem);
+
+ --color-text: hsl(12, 5%, 4%);
+ --color-bg: hsl(17, 20%, 97%);
+ --color-bg-2: hsl(17, 20%, 94%);
+ --color-bg-3: hsl(17, 20%, 88%);
+ --astro-blue: #4f39fa;
+ --astro-pink: #da62c4;
+
+ --content-max-width: 90ch;
+ --nav-height: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem);
+ }
+
+ h1 {
+ font-size: var(--font-size-xl);
+ }
+
+ button {
+ border: none;
+ color: var(--astro-blue);
+ border: 2px solid var(--astro-blue);
+ transition:
+ color 0.2s,
+ background-color 0.2s;
+ background-color: transparent;
+ padding: 0.4rem 0.8rem;
+ border-radius: 0.4rem;
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ cursor: pointer;
+ }
+
+ button:hover {
+ background-color: var(--astro-blue);
+ color: white;
+ }
+</style>
+
+<style>
+ html {
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ }
+
+ body {
+ margin: 0;
+ }
+
+ header {
+ background: var(--color-bg-2);
+ }
+
+ nav {
+ max-width: var(--content-max-width);
+ height: var(--nav-height);
+ margin: auto;
+ padding-inline: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .nav-header,
+ .nav-header:visited {
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ color: inherit;
+ text-decoration: none;
+ }
+</style>
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 000000000..c90609595
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,50 @@
+---
+import type { CartItemDisplayInfo } from '../cartStore';
+import Layout from '../layouts/Layout.astro';
+import AddToCartForm from '../components/AddToCartForm';
+import FigurineDescription from '../components/FigurineDescription.astro';
+import { withBase } from '../utils';
+
+const item: CartItemDisplayInfo = {
+ id: 'astronaut-figurine',
+ name: 'Astronaut Figurine',
+ imageSrc: withBase('/images/astronaut-figurine.png'),
+};
+---
+
+<Layout title={item.name}>
+ <main>
+ <div class="product-layout">
+ <div>
+ <FigurineDescription />
+ <AddToCartForm item={item} client:load>
+ <button type="submit">Add to cart</button>
+ </AddToCartForm>
+ </div>
+ <img src={item.imageSrc} alt={item.name} />
+ </div>
+ </main>
+</Layout>
+
+<style>
+ main {
+ margin: auto;
+ padding: 1em;
+ max-width: var(--content-max-width);
+ }
+
+ .product-layout {
+ display: grid;
+ gap: 2rem;
+ grid-template-columns: repeat(auto-fit, minmax(20rem, max-content));
+ }
+
+ .product-layout img {
+ width: 100%;
+ max-width: 26rem;
+ }
+
+ button[type='submit'] {
+ margin-block-start: 1rem;
+ }
+</style>
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 000000000..cbe38f5f3
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,4 @@
+const base = import.meta.env.BASE_URL.replace(/\/$/, '');
+
+/** Prefix a URL path with the site’s base path if set. */
+export const withBase = (path: string) => base + path;