diff options
Diffstat (limited to 'examples')
22 files changed, 522 insertions, 1 deletions
diff --git a/examples/fast-build/package.json b/examples/fast-build/package.json index 924671aa8..21fe139d5 100644 --- a/examples/fast-build/package.json +++ b/examples/fast-build/package.json @@ -11,7 +11,7 @@ }, "devDependencies": { "astro": "^0.23.0-next.6", - "preact": "~10.5.15", + "preact": "~10.6.5", "unocss": "^0.15.5", "vite-imagetools": "^4.0.1" } diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs new file mode 100644 index 000000000..7c986b97d --- /dev/null +++ b/examples/ssr/astro.config.mjs @@ -0,0 +1,12 @@ +// @ts-check + +export default /** @type {import('astro').AstroUserConfig} */ ({ + renderers: ['@astrojs/renderer-svelte'], + vite: { + server: { + proxy: { + '/api': 'http://localhost:8085' + } + } + } +}); diff --git a/examples/ssr/build.mjs b/examples/ssr/build.mjs new file mode 100644 index 000000000..5d2e4a3aa --- /dev/null +++ b/examples/ssr/build.mjs @@ -0,0 +1,12 @@ +import {execa} from 'execa'; + +const api = execa('npm', ['run', 'dev-api']); +api.stdout.pipe(process.stdout); +api.stderr.pipe(process.stderr); + +const build = execa('yarn', ['astro', 'build', '--experimental-ssr']); +build.stdout.pipe(process.stdout); +build.stderr.pipe(process.stderr); +await build; + +api.kill(); diff --git a/examples/ssr/package.json b/examples/ssr/package.json new file mode 100644 index 000000000..c783a5416 --- /dev/null +++ b/examples/ssr/package.json @@ -0,0 +1,21 @@ +{ + "name": "@example/ssr", + "version": "0.0.1", + "private": true, + "scripts": { + "dev-api": "node server/dev-api.mjs", + "dev": "npm run dev-api & astro dev --experimental-ssr", + "start": "astro dev", + "build": "echo 'Run yarn build-ssr instead'", + "build-ssr": "node build.mjs", + "server": "node server/server.mjs" + }, + "devDependencies": { + "astro": "^0.23.0-next.0", + "unocss": "^0.15.5", + "vite-imagetools": "^4.0.1" + }, + "dependencies": { + "@astropub/webapi": "^0.10.13" + } +} diff --git a/examples/ssr/public/images/products/cereal.jpg b/examples/ssr/public/images/products/cereal.jpg Binary files differnew file mode 100644 index 000000000..c1f4cce4a --- /dev/null +++ b/examples/ssr/public/images/products/cereal.jpg diff --git a/examples/ssr/public/images/products/muffins.jpg b/examples/ssr/public/images/products/muffins.jpg Binary files differnew file mode 100644 index 000000000..897733ee8 --- /dev/null +++ b/examples/ssr/public/images/products/muffins.jpg diff --git a/examples/ssr/public/images/products/oats.jpg b/examples/ssr/public/images/products/oats.jpg Binary files differnew file mode 100644 index 000000000..b8db72ae0 --- /dev/null +++ b/examples/ssr/public/images/products/oats.jpg diff --git a/examples/ssr/public/images/products/yogurt.jpg b/examples/ssr/public/images/products/yogurt.jpg Binary files differnew file mode 100644 index 000000000..9cd39666d --- /dev/null +++ b/examples/ssr/public/images/products/yogurt.jpg diff --git a/examples/ssr/server/api.mjs b/examples/ssr/server/api.mjs new file mode 100644 index 000000000..3928d0507 --- /dev/null +++ b/examples/ssr/server/api.mjs @@ -0,0 +1,49 @@ +import fs from 'fs'; +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])); + +const routes = [ + { + match: /\/api\/products\/([0-9])+/, + async handle(_req, res, [,idStr]) { + const id = Number(idStr); + if(productMap.has(id)) { + const product = productMap.get(id); + res.writeHead(200, { + 'Content-Type': 'application/json' + }); + res.end(JSON.stringify(product)); + } else { + res.writeHead(404, { + 'Content-Type': 'text/plain' + }); + res.end('Not found'); + } + } + }, + { + match: /\/api\/products/, + async handle(_req, res) { + res.writeHead(200, { + 'Content-Type': 'application/json', + }); + res.end(JSON.stringify(products)); + } + } + +] + +export async function apiHandler(req, res) { + for(const route of routes) { + const match = route.match.exec(req.url); + if(match) { + return route.handle(req, res, match); + } + } + res.writeHead(404, { + 'Content-Type': 'text/plain' + }); + res.end('Not found'); +} diff --git a/examples/ssr/server/db.json b/examples/ssr/server/db.json new file mode 100644 index 000000000..76f9e4da3 --- /dev/null +++ b/examples/ssr/server/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/server/dev-api.mjs b/examples/ssr/server/dev-api.mjs new file mode 100644 index 000000000..74e0ef83b --- /dev/null +++ b/examples/ssr/server/dev-api.mjs @@ -0,0 +1,17 @@ +import { createServer } from 'http'; +import { apiHandler } from './api.mjs'; + +const PORT = process.env.PORT || 8085; + +const server = createServer((req, res) => { + apiHandler(req, res).catch(err => { + console.error(err); + res.writeHead(500, { + 'Content-Type': 'text/plain' + }); + res.end(err.toString()); + }) +}); + +server.listen(PORT); +console.log(`API running at http://localhost:${PORT}`); diff --git a/examples/ssr/server/server.mjs b/examples/ssr/server/server.mjs new file mode 100644 index 000000000..6f0a0dea6 --- /dev/null +++ b/examples/ssr/server/server.mjs @@ -0,0 +1,55 @@ +import { createServer } from 'http'; +import fs from 'fs'; +import mime from 'mime'; +import { loadApp } from 'astro/app/node'; +import { polyfill } from '@astropub/webapi' +import { apiHandler } from './api.mjs'; + +polyfill(globalThis); + +const clientRoot = new URL('../dist/client/', import.meta.url); +const serverRoot = new URL('../dist/server/', import.meta.url); +const app = await loadApp(serverRoot); + +async function handle(req, res) { + const route = app.match(req); + + if(route) { + const html = await app.render(req, route); + + res.writeHead(200, { + 'Content-Type': 'text/html' + }); + res.end(html) + } else if(/^\/api\//.test(req.url)) { + return apiHandler(req, res); + } else { + let local = new URL('.' + req.url, clientRoot); + try { + const data = await fs.promises.readFile(local); + res.writeHead(200, { + 'Content-Type': mime.getType(req.url) + }); + res.end(data); + } catch { + res.writeHead(404); + res.end(); + } + } +} + +const server = createServer((req, res) => { + handle(req, res).catch(err => { + console.error(err); + res.writeHead(500, { + 'Content-Type': 'text/plain' + }); + res.end(err.toString()); + }) +}); + +server.listen(8085); +console.log('Serving at http://localhost:8085'); + +// Silence weird <time> warning +console.error = () => {}; diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts new file mode 100644 index 000000000..9fd7d0683 --- /dev/null +++ b/examples/ssr/src/api.ts @@ -0,0 +1,35 @@ +interface Product { + id: number; + name: string; + price: number; + image: string; +} + +//let origin: string; +const { mode } = import.meta.env; +const origin = mode === 'develeopment' ? + `http://localhost:3000` : + `http://localhost:8085`; + +async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> { + const response = await fetch(`${origin}${endpoint}`); + if(!response.ok) { + // TODO make this better... + return null; + } + return cb(response); +} + +export async function getProducts(): Promise<Product[]> { + return get<Product[]>('/api/products', async response => { + const products: Product[] = await response.json(); + return products; + }); +} + +export async function getProduct(id: number): Promise<Product> { + return get<Product>(`/api/products/${id}`, async response => { + const product: Product = await response.json(); + return product; + }); +} diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte new file mode 100644 index 000000000..b03b8180a --- /dev/null +++ b/examples/ssr/src/components/AddToCart.svelte @@ -0,0 +1,47 @@ +<script> + export let id = 0; + + function addToCart() { + window.dispatchEvent(new CustomEvent('add-to-cart', { + detail: 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 on: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..63dd1b5a5 --- /dev/null +++ b/examples/ssr/src/components/Cart.svelte @@ -0,0 +1,32 @@ +<script> + export let count = 0; + let items = new Set(); + + function onAddToCart(ev) { + const id = ev.detail; + items.add(id); + count++; + } +</script> +<style> + .cart { + display: flex; + align-items: center; + } + .cart :first-child { + margin-right: 5px; + } + + .cart-icon { + font-size: 36px; + } + + .count { + font-size: 24px; + } +</style> +<svelte:window on:add-to-cart={onAddToCart}/> +<div class="cart"> + <span class="material-icons cart-icon">shopping_cart</span> + <span class="count">{count}</span> +</div> diff --git a/examples/ssr/src/components/Container.astro b/examples/ssr/src/components/Container.astro new file mode 100644 index 000000000..f982522b8 --- /dev/null +++ b/examples/ssr/src/components/Container.astro @@ -0,0 +1,12 @@ +--- +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..2839c70d3 --- /dev/null +++ b/examples/ssr/src/components/Header.astro @@ -0,0 +1,31 @@ +--- +import TextDecorationSkip from './TextDecorationSkip.astro'; +import Cart from './Cart.svelte'; +--- +<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; + } +</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 /> + </div> +</header> diff --git a/examples/ssr/src/components/ProductListing.astro b/examples/ssr/src/components/ProductListing.astro new file mode 100644 index 000000000..c0af5a34c --- /dev/null +++ b/examples/ssr/src/components/ProductListing.astro @@ -0,0 +1,61 @@ +--- +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"></slot> +<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..b35179ea8 --- /dev/null +++ b/examples/ssr/src/components/TextDecorationSkip.astro @@ -0,0 +1,15 @@ +--- +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> </Fragment>)} + </Fragment> +))} diff --git a/examples/ssr/src/pages/index.astro b/examples/ssr/src/pages/index.astro new file mode 100644 index 000000000..ea2c6c2f6 --- /dev/null +++ b/examples/ssr/src/pages/index.astro @@ -0,0 +1,36 @@ +--- +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(); +--- +<html> +<head> + <title>Online Store</title> + <style> + h1 { + font-size: 36px; + } + + .product-listing-title { + text-align: center; + } + + .product-listing { + + } + </style> +</head> +<body> + <Header /> + + <Container tag="main"> + <ProductListing products={products} class="product-listing"> + <h2 class="product-listing-title" slot="title">Product Listing</h2> + </ProductListing> + </Container> +</body> +</html> diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro new file mode 100644 index 000000000..943f2ab84 --- /dev/null +++ b/examples/ssr/src/pages/products/[id].astro @@ -0,0 +1,55 @@ +--- +import Header from '../../components/Header.astro'; +import Container from '../../components/Container.astro'; +import AddToCart from '../../components/AddToCart.svelte'; +import { getProducts, getProduct } from '../../api'; +import '../../styles/common.css'; + +export async function getStaticPaths() { + const products = await getProducts(); + return products.map(product => { + return { + params: { id: product.id.toString() } + } + }); +} + +const id = Number(Astro.request.params.id); +const product = await getProduct(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 id={id} client:idle /> + <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..7879df33c --- /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; +} |