summaryrefslogtreecommitdiff
path: root/examples/ssr/src
diff options
context:
space:
mode:
Diffstat (limited to 'examples/ssr/src')
-rw-r--r--examples/ssr/src/api.ts79
-rw-r--r--examples/ssr/src/components/AddToCart.svelte53
-rw-r--r--examples/ssr/src/components/Cart.svelte34
-rw-r--r--examples/ssr/src/components/Container.astro13
-rw-r--r--examples/ssr/src/components/Header.astro49
-rw-r--r--examples/ssr/src/components/ProductListing.astro70
-rw-r--r--examples/ssr/src/components/TextDecorationSkip.astro23
-rw-r--r--examples/ssr/src/models/db.json28
-rw-r--r--examples/ssr/src/models/db.ts6
-rw-r--r--examples/ssr/src/models/session.ts2
-rw-r--r--examples/ssr/src/pages/api/cart.ts38
-rw-r--r--examples/ssr/src/pages/api/products.ts5
-rw-r--r--examples/ssr/src/pages/api/products/[id].ts16
-rw-r--r--examples/ssr/src/pages/cart.astro51
-rw-r--r--examples/ssr/src/pages/index.astro33
-rw-r--r--examples/ssr/src/pages/login.astro58
-rw-r--r--examples/ssr/src/pages/login.form.async.ts14
-rw-r--r--examples/ssr/src/pages/login.form.ts16
-rw-r--r--examples/ssr/src/pages/products/[id].astro45
-rw-r--r--examples/ssr/src/styles/common.css3
20 files changed, 636 insertions, 0 deletions
diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts
new file mode 100644
index 000000000..74e09eb73
--- /dev/null
+++ b/examples/ssr/src/api.ts
@@ -0,0 +1,79 @@
+export interface Product {
+ id: number;
+ name: string;
+ price: number;
+ image: string;
+}
+
+interface User {
+ id: number;
+}
+
+interface Cart {
+ items: Array<{
+ id: number;
+ name: string;
+ count: number;
+ }>;
+}
+
+async function get<T>(
+ incomingReq: Request,
+ endpoint: string,
+ cb: (response: Response) => Promise<T>
+): Promise<T> {
+ const origin = new URL(incomingReq.url).origin;
+ const response = await fetch(`${origin}${endpoint}`, {
+ credentials: 'same-origin',
+ headers: incomingReq.headers,
+ });
+ if (!response.ok) {
+ // TODO make this better...
+ throw new Error('Fetch failed');
+ }
+ return cb(response);
+}
+
+export async function getProducts(incomingReq: Request): Promise<Product[]> {
+ return get<Product[]>(incomingReq, '/api/products', async (response) => {
+ const products: Product[] = await response.json();
+ return products;
+ });
+}
+
+export async function getProduct(incomingReq: Request, id: number): Promise<Product> {
+ return get<Product>(incomingReq, `/api/products/${id}`, async (response) => {
+ const product: Product = await response.json();
+ return product;
+ });
+}
+
+export async function getUser(incomingReq: Request): Promise<User> {
+ return get<User>(incomingReq, `/api/user`, async (response) => {
+ const user: User = await response.json();
+ return user;
+ });
+}
+
+export async function getCart(incomingReq: Request): Promise<Cart> {
+ return get<Cart>(incomingReq, `/api/cart`, async (response) => {
+ const cart: Cart = await response.json();
+ return cart;
+ });
+}
+
+export async function addToUserCart(id: number | string, name: string): Promise<void> {
+ await fetch(`${location.origin}/api/cart`, {
+ credentials: 'same-origin',
+ method: 'POST',
+ mode: 'no-cors',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cache: 'no-cache',
+ },
+ body: JSON.stringify({
+ id,
+ name,
+ }),
+ });
+}
diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte
new file mode 100644
index 000000000..bae888b6b
--- /dev/null
+++ b/examples/ssr/src/components/AddToCart.svelte
@@ -0,0 +1,53 @@
+<script>
+ import { addToUserCart } from '../api';
+ let { id, name } = $props()
+
+ function notifyCartItem(id) {
+ window.dispatchEvent(new CustomEvent('add-to-cart', {
+ detail: id
+ }));
+ }
+
+ async function addToCart() {
+ await addToUserCart(id, name);
+ notifyCartItem(id);
+ }
+</script>
+<style>
+ button {
+ display:block;
+ padding:0.5em 1em 0.5em 1em;
+ border-radius:100px;
+ border:none;
+ font-size: 1.4em;
+ position:relative;
+ background:#0652DD;
+ cursor:pointer;
+ height:2em;
+ width:10em;
+ overflow:hidden;
+ transition:transform 0.1s;
+ z-index:1;
+}
+button:hover {
+ transform:scale(1.1);
+}
+
+.pretext {
+ color:#fff;
+ background:#0652DD;
+ position:absolute;
+ top:0;
+ left:0;
+ height:100%;
+ width:100%;
+ display:flex;
+ justify-content:center;
+ align-items:center;
+ font-family: 'Quicksand', sans-serif;
+ text-transform: uppercase;
+}
+</style>
+<button click={addToCart}>
+ <span class="pretext">Add to cart</span>
+</button>
diff --git a/examples/ssr/src/components/Cart.svelte b/examples/ssr/src/components/Cart.svelte
new file mode 100644
index 000000000..5d4b7d251
--- /dev/null
+++ b/examples/ssr/src/components/Cart.svelte
@@ -0,0 +1,34 @@
+<script>
+ let { count } = $props()
+ let items = new Set();
+
+ function onAddToCart(ev) {
+ const id = ev.detail;
+ items.add(id);
+ count++;
+ }
+</script>
+<style>
+ .cart {
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: inherit;
+ }
+ .cart :first-child {
+ margin-right: 5px;
+ }
+
+ .cart-icon {
+ font-size: 36px;
+ }
+
+ .count {
+ font-size: 24px;
+ }
+</style>
+<svelte:window onadd-to-cart={onAddToCart}/>
+<a href="/cart" class="cart">
+ <span class="material-icons cart-icon">shopping_cart</span>
+ <span class="count">{count}</span>
+</a>
diff --git a/examples/ssr/src/components/Container.astro b/examples/ssr/src/components/Container.astro
new file mode 100644
index 000000000..f1741156c
--- /dev/null
+++ b/examples/ssr/src/components/Container.astro
@@ -0,0 +1,13 @@
+---
+const { tag = 'div' } = Astro.props;
+const Tag = tag;
+---
+
+<style>
+ .container {
+ width: 1248px; /** TODO: responsive */
+ margin-left: auto;
+ margin-right: auto;
+ }
+</style>
+<Tag class="container"><slot /></Tag>
diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro
new file mode 100644
index 000000000..d266733e9
--- /dev/null
+++ b/examples/ssr/src/components/Header.astro
@@ -0,0 +1,49 @@
+---
+import TextDecorationSkip from './TextDecorationSkip.astro';
+import Cart from './Cart.svelte';
+import { getCart } from '../api';
+
+const cart = await getCart(Astro.request);
+const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0);
+---
+
+<style>
+ @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
+
+ header {
+ margin: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h1 {
+ margin: 0;
+ font-family: 'Lobster', cursive;
+ color: black;
+ }
+
+ a,
+ a:visited {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ .right-pane {
+ display: flex;
+ }
+
+ .material-icons {
+ font-size: 36px;
+ margin-right: 1rem;
+ }
+</style>
+<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
+<header>
+ <h1><a href="/"><TextDecorationSkip text="Online Store" /></a></h1>
+ <div class="right-pane">
+ <a href="/login">
+ <span class="material-icons"> login</span>
+ </a>
+ <Cart client:idle count={cartCount} />
+ </div>
+</header>
diff --git a/examples/ssr/src/components/ProductListing.astro b/examples/ssr/src/components/ProductListing.astro
new file mode 100644
index 000000000..14e6e1d8c
--- /dev/null
+++ b/examples/ssr/src/components/ProductListing.astro
@@ -0,0 +1,70 @@
+---
+import type { Product } from '../api';
+
+interface Props {
+ products: Product[];
+}
+
+const { products } = Astro.props;
+---
+
+<style>
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ }
+
+ figure {
+ width: 200px;
+ padding: 7px;
+ border: 1px solid black;
+ display: flex;
+ flex-direction: column;
+ }
+
+ figure figcaption {
+ text-align: center;
+ line-height: 1.6;
+ }
+
+ figure img {
+ width: 100%;
+ height: 250px;
+ object-fit: cover;
+ }
+
+ .product a {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .name {
+ font-weight: 500;
+ }
+
+ .price {
+ font-size: 90%;
+ color: #787878;
+ }
+</style>
+<slot name="title" />
+<ul>
+ {
+ products.map((product) => (
+ <li class="product">
+ <a href={`/products/${product.id}`}>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <div class="name">{product.name}</div>
+ <div class="price">${product.price}</div>
+ </figcaption>
+ </figure>
+ </a>
+ </li>
+ ))
+ }
+</ul>
diff --git a/examples/ssr/src/components/TextDecorationSkip.astro b/examples/ssr/src/components/TextDecorationSkip.astro
new file mode 100644
index 000000000..707027763
--- /dev/null
+++ b/examples/ssr/src/components/TextDecorationSkip.astro
@@ -0,0 +1,23 @@
+---
+interface Props {
+ text: string;
+}
+
+const { text } = Astro.props;
+const words = text.split(' ');
+const last = words.length - 1;
+---
+
+<style>
+ span {
+ text-decoration: underline;
+ }
+</style>
+{
+ words.map((word, i) => (
+ <Fragment>
+ <span>{word}</span>
+ {i !== last && <Fragment>&#32;</Fragment>}
+ </Fragment>
+ ))
+}
diff --git a/examples/ssr/src/models/db.json b/examples/ssr/src/models/db.json
new file mode 100644
index 000000000..76f9e4da3
--- /dev/null
+++ b/examples/ssr/src/models/db.json
@@ -0,0 +1,28 @@
+{
+ "products": [
+ {
+ "id": 1,
+ "name": "Cereal",
+ "price": 3.99,
+ "image": "/images/products/cereal.jpg"
+ },
+ {
+ "id": 2,
+ "name": "Yogurt",
+ "price": 3.97,
+ "image": "/images/products/yogurt.jpg"
+ },
+ {
+ "id": 3,
+ "name": "Rolled Oats",
+ "price": 2.89,
+ "image": "/images/products/oats.jpg"
+ },
+ {
+ "id": 4,
+ "name": "Muffins",
+ "price": 4.39,
+ "image": "/images/products/muffins.jpg"
+ }
+ ]
+}
diff --git a/examples/ssr/src/models/db.ts b/examples/ssr/src/models/db.ts
new file mode 100644
index 000000000..0ec181f9a
--- /dev/null
+++ b/examples/ssr/src/models/db.ts
@@ -0,0 +1,6 @@
+import db from './db.json';
+
+const products = db.products;
+const productMap = new Map(products.map((product) => [product.id, product]));
+
+export { products, productMap };
diff --git a/examples/ssr/src/models/session.ts b/examples/ssr/src/models/session.ts
new file mode 100644
index 000000000..16dce00b4
--- /dev/null
+++ b/examples/ssr/src/models/session.ts
@@ -0,0 +1,2 @@
+// Normally this would be in a database.
+export const userCartItems = new Map();
diff --git a/examples/ssr/src/pages/api/cart.ts b/examples/ssr/src/pages/api/cart.ts
new file mode 100644
index 000000000..8d64ec7d8
--- /dev/null
+++ b/examples/ssr/src/pages/api/cart.ts
@@ -0,0 +1,38 @@
+import type { APIContext } from 'astro';
+import { userCartItems } from '../../models/session';
+
+export function GET({ cookies }: APIContext) {
+ let userId = cookies.get('user-id')?.value;
+
+ if (!userId || !userCartItems.has(userId)) {
+ return Response.json({ items: [] });
+ }
+ let items = userCartItems.get(userId);
+ let array = Array.from(items.values());
+
+ return Response.json({ items: array });
+}
+
+interface AddToCartItem {
+ id: number;
+ name: string;
+}
+
+export async function POST({ cookies, request }: APIContext) {
+ const item: AddToCartItem = await request.json();
+
+ let userId = cookies.get('user-id')?.value;
+
+ if (!userCartItems.has(userId)) {
+ userCartItems.set(userId, new Map());
+ }
+
+ let cart = userCartItems.get(userId);
+ if (cart.has(item.id)) {
+ cart.get(item.id).count++;
+ } else {
+ cart.set(item.id, { id: item.id, name: item.name, count: 1 });
+ }
+
+ return Response.json({ ok: true });
+}
diff --git a/examples/ssr/src/pages/api/products.ts b/examples/ssr/src/pages/api/products.ts
new file mode 100644
index 000000000..8bf02a03d
--- /dev/null
+++ b/examples/ssr/src/pages/api/products.ts
@@ -0,0 +1,5 @@
+import { products } from '../../models/db';
+
+export function GET() {
+ return new Response(JSON.stringify(products));
+}
diff --git a/examples/ssr/src/pages/api/products/[id].ts b/examples/ssr/src/pages/api/products/[id].ts
new file mode 100644
index 000000000..f0f6fa89f
--- /dev/null
+++ b/examples/ssr/src/pages/api/products/[id].ts
@@ -0,0 +1,16 @@
+import { productMap } from '../../../models/db';
+import type { APIContext } from 'astro';
+
+export function GET({ params }: APIContext) {
+ const id = Number(params.id);
+ if (productMap.has(id)) {
+ const product = productMap.get(id);
+
+ return new Response(JSON.stringify(product));
+ } else {
+ return new Response(null, {
+ status: 400,
+ statusText: 'Not found',
+ });
+ }
+}
diff --git a/examples/ssr/src/pages/cart.astro b/examples/ssr/src/pages/cart.astro
new file mode 100644
index 000000000..40e5cf126
--- /dev/null
+++ b/examples/ssr/src/pages/cart.astro
@@ -0,0 +1,51 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+import { getCart } from '../api';
+
+if (!Astro.cookies.get('user-id')) {
+ return Astro.redirect('/');
+}
+
+// They must be logged in.
+
+const user = { name: 'test' }; // getUser?
+const cart = await getCart(Astro.request);
+---
+
+<html lang="en">
+ <head>
+ <title>Cart | Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <h1>Cart</h1>
+ <p>Hi {user.name}! Here are your cart items:</p>
+ <table>
+ <thead>
+ <tr>
+ <th>Item</th>
+ <th>Count</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ cart.items.map((item) => (
+ <tr>
+ <td>{item.name}</td>
+ <td>{item.count}</td>
+ </tr>
+ ))
+ }
+ </tbody>
+ </table>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/index.astro b/examples/ssr/src/pages/index.astro
new file mode 100644
index 000000000..1ce70bc81
--- /dev/null
+++ b/examples/ssr/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+import ProductListing from '../components/ProductListing.astro';
+import { getProducts } from '../api';
+import '../styles/common.css';
+
+const products = await getProducts(Astro.request);
+---
+
+<html lang="en">
+ <head>
+ <title>Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+
+ .product-listing-title {
+ text-align: center;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <ProductListing products={products}>
+ <h2 class="product-listing-title" slot="title">Product Listing</h2>
+ </ProductListing>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/login.astro b/examples/ssr/src/pages/login.astro
new file mode 100644
index 000000000..030838a64
--- /dev/null
+++ b/examples/ssr/src/pages/login.astro
@@ -0,0 +1,58 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+---
+
+<html lang="en">
+ <head>
+ <title>Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+ </style>
+
+ <script type="module" is:inline>
+ document.addEventListener('DOMContentLoaded', () => {
+ const form = document.querySelector('form');
+ if (!form) throw new Error('Form not found');
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData);
+
+ fetch('/login.form.async', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ })
+ .then((res) => res.json())
+ .then(() => {
+ const result = document.querySelector('#result');
+ if (result) {
+ result.innerHTML =
+ 'Progressive login was successful! you will be redirected to the store in 3 seconds';
+ setTimeout(() => (location.href = '/'), 3000);
+ }
+ });
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <h1>Login</h1>
+ <form action="/login.form" method="POST">
+ <label for="name">Name</label>
+ <input type="text" name="name" />
+
+ <label for="password">Password</label>
+ <input type="password" name="password" />
+
+ <input type="submit" value="Submit" />
+ </form>
+ <div id="result"></div>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/login.form.async.ts b/examples/ssr/src/pages/login.form.async.ts
new file mode 100644
index 000000000..94020d9c9
--- /dev/null
+++ b/examples/ssr/src/pages/login.form.async.ts
@@ -0,0 +1,14 @@
+import type { APIContext, APIRoute } from 'astro';
+
+export const POST: APIRoute = ({ cookies }: APIContext) => {
+ // add a new cookie
+ cookies.set('user-id', '1', {
+ path: '/',
+ maxAge: 2592000,
+ });
+
+ return Response.json({
+ ok: true,
+ user: 1,
+ });
+};
diff --git a/examples/ssr/src/pages/login.form.ts b/examples/ssr/src/pages/login.form.ts
new file mode 100644
index 000000000..f3cd50db4
--- /dev/null
+++ b/examples/ssr/src/pages/login.form.ts
@@ -0,0 +1,16 @@
+import type { APIContext } from 'astro';
+
+export function POST({ cookies }: APIContext) {
+ // add a new cookie
+ cookies.set('user-id', '1', {
+ path: '/',
+ maxAge: 2592000,
+ });
+
+ return new Response(null, {
+ status: 301,
+ headers: {
+ Location: '/',
+ },
+ });
+}
diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro
new file mode 100644
index 000000000..e90900e45
--- /dev/null
+++ b/examples/ssr/src/pages/products/[id].astro
@@ -0,0 +1,45 @@
+---
+import Header from '../../components/Header.astro';
+import Container from '../../components/Container.astro';
+import AddToCart from '../../components/AddToCart.svelte';
+import { getProduct } from '../../api';
+import '../../styles/common.css';
+
+const id = Number(Astro.params.id);
+const product = await getProduct(Astro.request, id);
+---
+
+<html lang="en">
+ <head>
+ <title>{product.name} | Online Store</title>
+ <style>
+ h2 {
+ text-align: center;
+ font-size: 3.5rem;
+ }
+
+ figure {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ img {
+ width: 400px;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="article">
+ <h2>{product.name}</h2>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <AddToCart client:idle id={id} name={product.name} />
+ <p>Description here...</p>
+ </figcaption>
+ </figure>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/styles/common.css b/examples/ssr/src/styles/common.css
new file mode 100644
index 000000000..9d73ad1a4
--- /dev/null
+++ b/examples/ssr/src/styles/common.css
@@ -0,0 +1,3 @@
+body {
+ font-family: 'GT America Standard', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}