aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codesandbox/Dockerfile1
-rw-r--r--.gitignore24
-rw-r--r--README.md11
-rw-r--r--astro.config.mjs9
-rw-r--r--package.json19
-rw-r--r--public/favicon.svg9
-rw-r--r--public/images/astronaut-figurine.pngbin0 -> 498339 bytes
-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
-rw-r--r--tsconfig.json10
17 files changed, 407 insertions, 0 deletions
diff --git a/.codesandbox/Dockerfile b/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..163c9129a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+# Astro Example: Nanostores
+
+```sh
+npm create astro@latest -- --template with-nanostores
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-nanostores)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-nanostores)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-nanostores/devcontainer.json)
+
+This example showcases using [`nanostores`](https://github.com/nanostores/nanostores) to provide shared state between components of any framework. [**Read our documentation on sharing state**](https://docs.astro.build/en/core-concepts/sharing-state/) for a complete breakdown of this project, along with guides to use React, Vue, Svelte, or Solid!
diff --git a/astro.config.mjs b/astro.config.mjs
new file mode 100644
index 000000000..9f7dbd219
--- /dev/null
+++ b/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable many frameworks to support all different kinds of components.
+ integrations: [preact()],
+});
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..cb7cab954
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@example/with-nanostores",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/preact": "^4.1.0",
+ "@nanostores/preact": "^0.5.2",
+ "astro": "^5.9.0",
+ "nanostores": "^0.11.4",
+ "preact": "^10.26.5"
+ }
+}
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/public/images/astronaut-figurine.png b/public/images/astronaut-figurine.png
new file mode 100644
index 000000000..aac9b445e
--- /dev/null
+++ b/public/images/astronaut-figurine.png
Binary files differ
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;
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..c8983c2ef
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Preact specific settings
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ }
+}