commit cf792922e78aeb5757f9fbf86d89391ad4fe4bca Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon Dec 16 19:52:17 2024 +0000 Sync from a44cfb874a6f066214e851c98a410d89c6866992 diff --git a/.codesandbox/Dockerfile b/.codesandbox/Dockerfile new file mode 100644 index 0000000000..c3b5c81a12 --- /dev/null +++ b/.codesandbox/Dockerfile @@ -0,0 +1 @@ +FROM node:18-bullseye diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..16d54bb13c --- /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/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..22a15055d6 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode"], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..d642209762 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000000..78d88cb1aa --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,13 @@ +// @ts-check +import { defineConfig } from 'astro/config'; +import svelte from '@astrojs/svelte'; +import node from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + output: 'server', + adapter: node({ + mode: 'standalone', + }), + integrations: [svelte()], +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000000..ae12ac6c3b --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "@example/ssr", + "type": "module", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "server": "node dist/server/entry.mjs" + }, + "dependencies": { + "@astrojs/node": "^9.0.0", + "@astrojs/svelte": "^7.0.1", + "astro": "^5.0.8", + "svelte": "^5.1.16" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000000..f157bd1c5e --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/public/images/products/cereal.jpg b/public/images/products/cereal.jpg new file mode 100644 index 0000000000..35601a789c Binary files /dev/null and b/public/images/products/cereal.jpg differ diff --git a/public/images/products/muffins.jpg b/public/images/products/muffins.jpg new file mode 100644 index 0000000000..ced2d9a912 Binary files /dev/null and b/public/images/products/muffins.jpg differ diff --git a/public/images/products/oats.jpg b/public/images/products/oats.jpg new file mode 100644 index 0000000000..54ae1ebdbc Binary files /dev/null and b/public/images/products/oats.jpg differ diff --git a/public/images/products/yogurt.jpg b/public/images/products/yogurt.jpg new file mode 100644 index 0000000000..73c1b9a85b Binary files /dev/null and b/public/images/products/yogurt.jpg differ diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000000..74e09eb735 --- /dev/null +++ b/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( + incomingReq: Request, + endpoint: string, + cb: (response: Response) => Promise +): Promise { + 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 { + return get(incomingReq, '/api/products', async (response) => { + const products: Product[] = await response.json(); + return products; + }); +} + +export async function getProduct(incomingReq: Request, id: number): Promise { + return get(incomingReq, `/api/products/${id}`, async (response) => { + const product: Product = await response.json(); + return product; + }); +} + +export async function getUser(incomingReq: Request): Promise { + return get(incomingReq, `/api/user`, async (response) => { + const user: User = await response.json(); + return user; + }); +} + +export async function getCart(incomingReq: Request): Promise { + return get(incomingReq, `/api/cart`, async (response) => { + const cart: Cart = await response.json(); + return cart; + }); +} + +export async function addToUserCart(id: number | string, name: string): Promise { + 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/src/components/AddToCart.svelte b/src/components/AddToCart.svelte new file mode 100644 index 0000000000..bae888b6b3 --- /dev/null +++ b/src/components/AddToCart.svelte @@ -0,0 +1,53 @@ + + + diff --git a/src/components/Cart.svelte b/src/components/Cart.svelte new file mode 100644 index 0000000000..5d4b7d2510 --- /dev/null +++ b/src/components/Cart.svelte @@ -0,0 +1,34 @@ + + + + + shopping_cart + {count} + diff --git a/src/components/Container.astro b/src/components/Container.astro new file mode 100644 index 0000000000..f1741156cc --- /dev/null +++ b/src/components/Container.astro @@ -0,0 +1,13 @@ +--- +const { tag = 'div' } = Astro.props; +const Tag = tag; +--- + + + diff --git a/src/components/Header.astro b/src/components/Header.astro new file mode 100644 index 0000000000..d266733e90 --- /dev/null +++ b/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); +--- + + + +
+

+ +
diff --git a/src/components/ProductListing.astro b/src/components/ProductListing.astro new file mode 100644 index 0000000000..14e6e1d8ca --- /dev/null +++ b/src/components/ProductListing.astro @@ -0,0 +1,70 @@ +--- +import type { Product } from '../api'; + +interface Props { + products: Product[]; +} + +const { products } = Astro.props; +--- + + + + diff --git a/src/components/TextDecorationSkip.astro b/src/components/TextDecorationSkip.astro new file mode 100644 index 0000000000..7070277631 --- /dev/null +++ b/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; +--- + + +{ + words.map((word, i) => ( + + {word} + {i !== last && } + + )) +} diff --git a/src/models/db.json b/src/models/db.json new file mode 100644 index 0000000000..76f9e4da34 --- /dev/null +++ b/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/src/models/db.ts b/src/models/db.ts new file mode 100644 index 0000000000..0ec181f9ac --- /dev/null +++ b/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/src/models/session.ts b/src/models/session.ts new file mode 100644 index 0000000000..16dce00b4e --- /dev/null +++ b/src/models/session.ts @@ -0,0 +1,2 @@ +// Normally this would be in a database. +export const userCartItems = new Map(); diff --git a/src/pages/api/cart.ts b/src/pages/api/cart.ts new file mode 100644 index 0000000000..8d64ec7d84 --- /dev/null +++ b/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/src/pages/api/products.ts b/src/pages/api/products.ts new file mode 100644 index 0000000000..8bf02a03dd --- /dev/null +++ b/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/src/pages/api/products/[id].ts b/src/pages/api/products/[id].ts new file mode 100644 index 0000000000..f0f6fa89f3 --- /dev/null +++ b/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/src/pages/cart.astro b/src/pages/cart.astro new file mode 100644 index 0000000000..40e5cf1265 --- /dev/null +++ b/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); +--- + + + + Cart | Online Store + + + +
+ + +

Cart

+

Hi {user.name}! Here are your cart items:

+ + + + + + + + + { + cart.items.map((item) => ( + + + + + )) + } + +
ItemCount
{item.name}{item.count}
+
+ + diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000000..1ce70bc81e --- /dev/null +++ b/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); +--- + + + + Online Store + + + +
+ + + +

Product Listing

+
+
+ + diff --git a/src/pages/login.astro b/src/pages/login.astro new file mode 100644 index 0000000000..030838a64b --- /dev/null +++ b/src/pages/login.astro @@ -0,0 +1,58 @@ +--- +import Header from '../components/Header.astro'; +import Container from '../components/Container.astro'; +--- + + + + Online Store + + + + + +
+ + +

Login

+
+ + + + + + + +
+
+
+ + diff --git a/src/pages/login.form.async.ts b/src/pages/login.form.async.ts new file mode 100644 index 0000000000..94020d9c9b --- /dev/null +++ b/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/src/pages/login.form.ts b/src/pages/login.form.ts new file mode 100644 index 0000000000..f3cd50db46 --- /dev/null +++ b/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/src/pages/products/[id].astro b/src/pages/products/[id].astro new file mode 100644 index 0000000000..e90900e455 --- /dev/null +++ b/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); +--- + + + + {product.name} | Online Store + + + +
+ + +

{product.name}

+
+ +
+ +

Description here...

+
+
+
+ + diff --git a/src/styles/common.css b/src/styles/common.css new file mode 100644 index 0000000000..9d73ad1a4d --- /dev/null +++ b/src/styles/common.css @@ -0,0 +1,3 @@ +body { + font-family: 'GT America Standard', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..8bf91d3bb9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +}