diff --git a/README.md b/README.md index e3f9cbf..49ae306 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Raidy is a raid scheduling project for Discord statics: the web console is where everyone plans, and Discord carries the announcements. One Go process runs the Discord bot, web API, scheduler, and production web assets, backed by PostgreSQL. -**Status:** `v0.0.18` is an early development snapshot. +**Status:** `v0.0.19` is an early development snapshot. ```text backend/ Go service, Discord interactions, web API, PostgreSQL migrations @@ -60,8 +60,8 @@ The Go process applies pinned, embedded Goose migrations before opening the appl `web/product/` is the public product page, and the demo is the real app running against in-browser sample data. Neither needs the Go backend. From `web/`: ```sh -npm run dev:product # product page with live reload -npm run dev:demo # demo at /demo/ with live reload +npm run dev:product # product page and demo together, with live reload +npm run dev:demo # the demo alone, at /demo/ npm run screenshots # capture the app screenshots the page shows npm run build:pages # build both into dist-pages/ npm run preview:pages # serve dist-pages/ as GitHub Pages does diff --git a/web/e2e/product.spec.ts b/web/e2e/product.spec.ts index b9e7d15..6dc0c91 100644 --- a/web/e2e/product.spec.ts +++ b/web/e2e/product.spec.ts @@ -12,6 +12,19 @@ test("the product page leads to the live demo and the code", async ({ page }) => ); }); +// Regression: the demo link was relative to the current page, and the +// product server answered every path with the product page, so each click +// added another /demo/ to the URL instead of opening the demo. +test("the demo opens at /demo/ and leads back to the product page", async ({ page }) => { + await page.goto("/"); + await page.getByRole("link", { name: "Try the demo" }).click(); + await expect(page.getByRole("table", { name: /availability/i })).toBeVisible({ timeout: 30_000 }); + expect(new URL(page.url()).pathname).toBe("/demo/"); + await page.getByRole("link", { name: "About Raidy" }).click(); + await expect(page.getByRole("heading", { level: 1 })).toHaveText("See which nights your whole static can raid."); + expect(new URL(page.url()).pathname).toBe("/"); +}); + test("tapping a night in the hero updates the Discord post", async ({ page }) => { await page.clock.setFixedTime(monday); await page.goto("/"); diff --git a/web/package-lock.json b/web/package-lock.json index e3079fc..fea1fed 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "raidy-web", - "version": "0.0.18", + "version": "0.0.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "raidy-web", - "version": "0.0.18", + "version": "0.0.19", "dependencies": { "@fontsource-variable/geist": "^5.3.0", "@fontsource-variable/geist-mono": "^5.3.0", diff --git a/web/package.json b/web/package.json index 7c29a90..9677871 100644 --- a/web/package.json +++ b/web/package.json @@ -1,11 +1,11 @@ { "name": "raidy-web", "private": true, - "version": "0.0.18", + "version": "0.0.19", "type": "module", "scripts": { "dev": "vite", - "dev:product": "vite --config vite.product.config.ts", + "dev:product": "node scripts/dev-site.mjs", "dev:demo": "vite --mode demo --base /demo/", "build": "tsc -b && vite build", "check": "tsc -b --pretty false", @@ -14,7 +14,7 @@ "test:e2e": "playwright test", "build:demo": "tsc -b && vite build --mode demo --base /demo/ --outDir dist-demo", "screenshots": "playwright test --config playwright.screenshots.config.ts", - "build:pages": "rm -rf dist-pages && tsc -b && vite build --config vite.product.config.ts && vite build --mode demo --base /demo/ --outDir dist-pages/demo", + "build:pages": "rm -rf dist-pages && tsc -b && vite build --config vite.product.config.ts && vite build --mode demo --base ./ --outDir dist-pages/demo", "preview:pages": "vite preview --config vite.product.config.ts" }, "dependencies": { diff --git a/web/playwright.config.ts b/web/playwright.config.ts index 9d2d093..a6aec63 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -38,7 +38,8 @@ export default defineConfig({ reuseExistingServer: !process.env.CI }, { - command: `npx vite --config vite.product.config.ts --host 127.0.0.1 --port ${productPort} --strictPort`, + // The same command as `npm run dev:product`: the page plus the demo at /demo/. + command: `node scripts/dev-site.mjs --host 127.0.0.1 --port ${productPort} --strictPort`, url: `http://127.0.0.1:${productPort}/`, reuseExistingServer: !process.env.CI } diff --git a/web/scripts/dev-site.mjs b/web/scripts/dev-site.mjs new file mode 100644 index 0000000..753df66 --- /dev/null +++ b/web/scripts/dev-site.mjs @@ -0,0 +1,16 @@ +// Runs the product page and the demo together, so links to /demo/ work: +// the product dev server forwards /demo/ to the demo dev server. +import { spawn } from "node:child_process"; + +const demo = ["--mode", "demo", "--base", "/demo/", "--host", "127.0.0.1", "--port", "5174", "--strictPort"]; +const product = ["--config", "vite.product.config.ts", ...process.argv.slice(2)]; + +// Run vite directly rather than through npx, so stopping this script stops both servers. +const servers = [demo, product].map((args) => + spawn(process.execPath, ["node_modules/vite/bin/vite.js", ...args], { stdio: "inherit" }) +); + +const stop = () => servers.forEach((server) => server.kill()); +process.on("SIGINT", stop); +process.on("SIGTERM", stop); +servers.forEach((server) => server.on("exit", (code) => code && (stop(), process.exit(code)))); diff --git a/web/src/App.tsx b/web/src/App.tsx index 332d92d..c73e6a8 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,7 +10,7 @@ import { Overview } from "@/features/overview/overview"; import { SchedulePanel } from "@/features/schedule/schedule-panel"; import { TeamPanel } from "@/features/team/team-panel"; import { message } from "@/lib/format"; -import { isDemo, teamURL } from "@/lib/paths"; +import { isDemo, productURL, teamURL } from "@/lib/paths"; import { applyTheme, type Theme, type ThemePreference } from "@/lib/theme"; import { cn } from "@/lib/utils"; @@ -97,7 +97,7 @@ export default function App() { async function logout() { await api("/api/auth/logout", { method: "POST" }); - location.assign("/"); + location.assign(isDemo ? productURL : "/"); } function chooseTheme(next: ThemePreference) { @@ -232,7 +232,7 @@ function DemoBanner() { Demo ยท Sample data. Changes reset when you reload. - + About Raidy diff --git a/web/src/lib/paths.ts b/web/src/lib/paths.ts index 1f16715..5ed42ce 100644 --- a/web/src/lib/paths.ts +++ b/web/src/lib/paths.ts @@ -1,6 +1,9 @@ /** True in the demo build, which runs the app against in-browser sample data. */ export const isDemo = import.meta.env.MODE === "demo"; +/** The product page, which sits one level above the demo wherever the site is hosted. */ +export const productURL = "../"; + /** The app URL for a team: /app?team=N normally, /demo/?team=N in the demo. */ export function teamURL(teamID: number) { return `${import.meta.env.BASE_URL}${isDemo ? "" : "app"}?team=${teamID}`; diff --git a/web/src/product/product-page.tsx b/web/src/product/product-page.tsx index 808818e..74ffd2b 100644 --- a/web/src/product/product-page.tsx +++ b/web/src/product/product-page.tsx @@ -67,7 +67,7 @@ export function ProductPage() { return (