diff options
Diffstat (limited to 'examples/ssr')
-rw-r--r-- | examples/ssr/astro.config.mjs | 8 | ||||
-rw-r--r-- | examples/ssr/package.json | 5 | ||||
-rw-r--r-- | examples/ssr/server/api.mjs | 52 | ||||
-rw-r--r-- | examples/ssr/server/server.mjs | 7 | ||||
-rw-r--r-- | examples/ssr/src/api.ts | 51 | ||||
-rw-r--r-- | examples/ssr/src/components/AddToCart.svelte | 9 | ||||
-rw-r--r-- | examples/ssr/src/components/Cart.svelte | 6 | ||||
-rw-r--r-- | examples/ssr/src/components/Header.astro | 20 | ||||
-rw-r--r-- | examples/ssr/src/models/user.ts | 8 | ||||
-rw-r--r-- | examples/ssr/src/pages/cart.astro | 47 | ||||
-rw-r--r-- | examples/ssr/src/pages/login.astro | 30 | ||||
-rw-r--r-- | examples/ssr/src/pages/login.form.js | 10 | ||||
-rw-r--r-- | examples/ssr/src/pages/products/[id].astro | 2 | ||||
-rw-r--r-- | examples/ssr/tsconfig.json | 8 |
14 files changed, 249 insertions, 14 deletions
diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs index d54ab5929..4b01264ec 100644 --- a/examples/ssr/astro.config.mjs +++ b/examples/ssr/astro.config.mjs @@ -4,8 +4,14 @@ export default defineConfig({ renderers: ['@astrojs/renderer-svelte'], vite: { server: { + cors: { + credentials: true + }, proxy: { - '/api': 'http://localhost:8085', + '/api': { + target: 'http://127.0.0.1:8085', + changeOrigin: true, + } }, }, }, diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 5180ae7f9..fe135b832 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "dev-api": "node server/dev-api.mjs", - "dev": "npm run dev-api & astro dev --experimental-ssr", + "dev-server": "astro dev --experimental-ssr", + "dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"", "start": "astro dev", "build": "echo 'Run pnpm run build-ssr instead'", "build-ssr": "node build.mjs", @@ -13,6 +14,8 @@ "devDependencies": { "@astrojs/renderer-svelte": "^0.5.2", "astro": "^0.24.3", + "concurrently": "^7.0.0", + "lightcookie": "^1.0.25", "unocss": "^0.15.6", "vite-imagetools": "^4.0.3" } diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs index 3d2656815..9bb0be72a 100644 --- a/examples/ssr/server/api.mjs +++ b/examples/ssr/server/api.mjs @@ -1,9 +1,14 @@ import fs from 'fs'; +import lightcookie from 'lightcookie'; + const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url)); const db = JSON.parse(dbJSON); const products = db.products; const productMap = new Map(products.map((product) => [product.id, product])); +// Normally this would be in a database. +const userCartItems = new Map(); + const routes = [ { match: /\/api\/products\/([0-9])+/, @@ -32,6 +37,53 @@ const routes = [ res.end(JSON.stringify(products)); }, }, + { + match: /\/api\/cart/, + async handle(req, res) { + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + let cookie = req.headers.cookie; + let userId = cookie ? lightcookie.parse(cookie)['user-id'] : '1'; // default for testing + if(!userId || !userCartItems.has(userId)) { + res.end(JSON.stringify({ items: [] })); + return; + } + let items = userCartItems.get(userId); + let array = Array.from(items.values()); + res.end(JSON.stringify({ items: array })); + } + }, + { + match: /\/api\/add-to-cart/, + async handle(req, res) { + let body = ''; + req.on('data', chunk => body += chunk); + return new Promise(resolve => { + req.on('end', () => { + let cookie = req.headers.cookie; + let userId = lightcookie.parse(cookie)['user-id']; + let msg = JSON.parse(body); + + if(!userCartItems.has(userId)) { + userCartItems.set(userId, new Map()); + } + + let cart = userCartItems.get(userId); + if(cart.has(msg.id)) { + cart.get(msg.id).count++; + } else { + cart.set(msg.id, { id: msg.id, name: msg.name, count: 1 }); + } + + res.writeHead(200, { + 'Content-Type': 'application/json', + }); + res.end(JSON.stringify({ ok: true })); + }); + }); + } + } ]; export async function apiHandler(req, res) { diff --git a/examples/ssr/server/server.mjs b/examples/ssr/server/server.mjs index e760ac2f8..c6f35685e 100644 --- a/examples/ssr/server/server.mjs +++ b/examples/ssr/server/server.mjs @@ -15,9 +15,10 @@ async function handle(req, res) { const route = app.match(req); if (route) { - const html = await app.render(req, route); - - res.writeHead(200, { + /** @type {Response} */ + const response = await app.render(req, route); + const html = await response.text(); + res.writeHead(response.status, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': Buffer.byteLength(html, 'utf-8'), }); diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts index 59619ade6..b71990f3f 100644 --- a/examples/ssr/src/api.ts +++ b/examples/ssr/src/api.ts @@ -5,12 +5,25 @@ interface Product { image: string; } -//let origin: string; -const { mode } = import.meta.env; -const origin = mode === 'develepment' ? `http://localhost:3000` : `http://localhost:8085`; +interface User { + id: number; +} + +interface Cart { + items: Array<{ + id: number; + name: string; + count: number; + }>; +} + +const { MODE } = import.meta.env; +const origin = MODE === 'development' ? `http://127.0.0.1:3000` : `http://127.0.0.1:8085`; async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> { - const response = await fetch(`${origin}${endpoint}`); + const response = await fetch(`${origin}${endpoint}`, { + credentials: 'same-origin' + }); if (!response.ok) { // TODO make this better... return null; @@ -31,3 +44,33 @@ export async function getProduct(id: number): Promise<Product> { return product; }); } + +export async function getUser(): Promise<User> { + return get<User>(`/api/user`, async response => { + const user: User = await response.json(); + return user; + }); +} + +export async function getCart(): Promise<Cart> { + return get<Cart>(`/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(`${origin}/api/add-to-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 index b03b8180a..0f7a97a93 100644 --- a/examples/ssr/src/components/AddToCart.svelte +++ b/examples/ssr/src/components/AddToCart.svelte @@ -1,11 +1,18 @@ <script> + import { addToUserCart } from '../api'; export let id = 0; + export let name = ''; - function addToCart() { + function notifyCartItem(id) { window.dispatchEvent(new CustomEvent('add-to-cart', { detail: id })); } + + async function addToCart() { + await addToUserCart(id, name); + notifyCartItem(id); + } </script> <style> button { diff --git a/examples/ssr/src/components/Cart.svelte b/examples/ssr/src/components/Cart.svelte index 63dd1b5a5..74db0bc79 100644 --- a/examples/ssr/src/components/Cart.svelte +++ b/examples/ssr/src/components/Cart.svelte @@ -12,6 +12,8 @@ .cart { display: flex; align-items: center; + text-decoration: none; + color: inherit; } .cart :first-child { margin-right: 5px; @@ -26,7 +28,7 @@ } </style> <svelte:window on:add-to-cart={onAddToCart}/> -<div class="cart"> +<a href="/cart" class="cart"> <span class="material-icons cart-icon">shopping_cart</span> <span class="count">{count}</span> -</div> +</a> diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro index 2839c70d3..c4d925a5f 100644 --- a/examples/ssr/src/components/Header.astro +++ b/examples/ssr/src/components/Header.astro @@ -1,6 +1,10 @@ --- import TextDecorationSkip from './TextDecorationSkip.astro'; import Cart from './Cart.svelte'; +import { getCart } from '../api'; + +const cart = await getCart(); +const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0); --- <style> @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap'); @@ -21,11 +25,25 @@ import Cart from './Cart.svelte'; 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"> - <Cart client:idle /> + <a href="/login"> + <span class="material-icons"> + login + </span> + </a> + <Cart client:idle count={cartCount} /> </div> </header> diff --git a/examples/ssr/src/models/user.ts b/examples/ssr/src/models/user.ts new file mode 100644 index 000000000..ecd839d46 --- /dev/null +++ b/examples/ssr/src/models/user.ts @@ -0,0 +1,8 @@ +import lightcookie from 'lightcookie'; + + +export function isLoggedIn(request: Request): boolean { + const cookie = request.headers.get('cookie'); + const parsed = lightcookie.parse(cookie); + return 'user-id' in parsed; +} diff --git a/examples/ssr/src/pages/cart.astro b/examples/ssr/src/pages/cart.astro new file mode 100644 index 000000000..e4a00183e --- /dev/null +++ b/examples/ssr/src/pages/cart.astro @@ -0,0 +1,47 @@ +--- +import Header from '../components/Header.astro'; +import Container from '../components/Container.astro'; +import { getCart } from '../api'; +import { isLoggedIn } from '../models/user'; + +if(!isLoggedIn(Astro.request)) { + return Astro.redirect('/'); +} + +// They must be logged in. + +const user = { name: 'test'}; // getUser? +const cart = await getCart(); +--- +<html> +<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/login.astro b/examples/ssr/src/pages/login.astro new file mode 100644 index 000000000..b12a82a5e --- /dev/null +++ b/examples/ssr/src/pages/login.astro @@ -0,0 +1,30 @@ +--- +import Header from '../components/Header.astro'; +import Container from '../components/Container.astro'; +--- +<html> +<head> + <title>Online Store</title> + <style> + h1 { + font-size: 36px; + } + </style> +</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> + </Container> +</body> +</html> diff --git a/examples/ssr/src/pages/login.form.js b/examples/ssr/src/pages/login.form.js new file mode 100644 index 000000000..9875ae160 --- /dev/null +++ b/examples/ssr/src/pages/login.form.js @@ -0,0 +1,10 @@ + +export function post(params, request) { + return new Response(null, { + status: 301, + headers: { + 'Location': '/', + 'Set-Cookie': 'user-id=1; Path=/; Max-Age=2592000' + } + }); +} diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro index 943f2ab84..9c400c2f1 100644 --- a/examples/ssr/src/pages/products/[id].astro +++ b/examples/ssr/src/pages/products/[id].astro @@ -45,7 +45,7 @@ const product = await getProduct(id); <figure> <img src={product.image} /> <figcaption> - <AddToCart id={id} client:idle /> + <AddToCart client:idle id={id} name={product.name} /> <p>Description here...</p> </figcaption> </figure> diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json new file mode 100644 index 000000000..e0065a323 --- /dev/null +++ b/examples/ssr/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "lib": ["ES2015", "DOM"], + "module": "ES2022", + "moduleResolution": "node", + "types": ["astro/env"] + } +} |