Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,29 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
# Repository secrets for a test event. Forks do not have them, so there
# the job only builds and the route checks are skipped.
VITE_SEATLAYER_EVENT_KEY: ${{ secrets.DEMO_EVENT_KEY }}
VITE_SEATLAYER_PUBLIC_KEY: ${{ secrets.DEMO_PUBLIC_KEY }}
VITE_SEATLAYER_SEASON_KEY: ${{ secrets.DEMO_SEASON_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build

- name: Install the route check browser
if: env.VITE_SEATLAYER_EVENT_KEY != '' && env.VITE_SEATLAYER_PUBLIC_KEY != ''
run: |
npm install --no-save playwright@1.63.0
npx playwright install --with-deps chromium

- name: Load every route
if: env.VITE_SEATLAYER_EVENT_KEY != '' && env.VITE_SEATLAYER_PUBLIC_KEY != ''
run: |
npm run preview -- --port 4173 --strictPort &
for i in $(seq 1 30); do curl -sf http://localhost:4173 > /dev/null && break; sleep 2; done
node scripts/check-routes.mjs
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,26 @@ countdown.
- [Server SDKs](https://docs.seatlayer.io/server-sdk/)
- [seatlayer-sdk on GitHub](https://github.com/seatlayer/seatlayer-sdk)

## Hosting on Cloudflare

The build output in `dist` is a static site, so it runs on Cloudflare Pages as
it is. Use `npm run build` as the build command and `dist` as the output
directory. Set the `VITE_` variables as build variables, because Vite writes
them into the bundle at build time.

The routes are handled in the browser by React Router. Pages serves
`index.html` for any path that is not a file when the site has no top level
`404.html`, so deep links such as `/seat-picker` work without a `_redirects`
file.

Add `?embed=1` to any route to hide the navigation and the route heading, so
the example sits cleanly in an iframe.

CI builds every push. When the repository has `DEMO_EVENT_KEY` and
`DEMO_PUBLIC_KEY` secrets (and optionally `DEMO_SEASON_KEY`), it also serves the
build with `npm run preview` and loads every route with
`scripts/check-routes.mjs`, failing on an HTTP error or a console error.

## License

MIT. See [LICENSE](./LICENSE).
Expand Down
52 changes: 52 additions & 0 deletions scripts/check-routes.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Loads every route in a real browser and fails on an HTTP error, an uncaught
* exception or a console error. Each route is also loaded with ?embed=1, where
* the navigation must be hidden.
*
* BASE_URL=http://localhost:4173 node scripts/check-routes.mjs
*
* CI runs it against `npm run preview` when the demo keys are available.
*/
import { chromium } from "playwright";

const baseUrl = process.env.BASE_URL ?? "http://localhost:4173";

const routes = ["/", "/seat-picker", "/season", "/events", "/best-available", "/control-room"];

const browser = await chromium.launch();
const failures = [];

for (const route of routes) {
for (const path of [route, `${route}?embed=1`]) {
const page = await browser.newPage();
const problems = [];
page.on("pageerror", (error) => problems.push(`uncaught: ${error.message}`));
page.on("console", (message) => {
if (message.type() === "error") problems.push(`console: ${message.text()}`);
});

const response = await page.goto(baseUrl + path, { waitUntil: "load" });
if (!response || !response.ok()) {
problems.push(`HTTP ${response ? response.status() : "no response"}`);
}

// Give the seat map time to fetch its chart and render.
await page.waitForTimeout(4000);

if (path.endsWith("?embed=1") && (await page.locator(".nav").isVisible())) {
problems.push("navigation is visible in embed mode");
}

console.log(`${problems.length ? "FAIL" : "ok "} ${path}`);
for (const problem of problems) console.log(` ${problem}`);
if (problems.length) failures.push(path);
await page.close();
}
}

await browser.close();

if (failures.length) {
console.error(`\n${failures.length} route(s) failed.`);
process.exit(1);
}
35 changes: 21 additions & 14 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavLink, Outlet } from "react-router-dom";
import { NavLink, Outlet, useSearchParams } from "react-router-dom";

const routes = [
{ to: "/", label: "Single event", end: true },
Expand All @@ -10,20 +10,27 @@ const routes = [
];

export default function App() {
// Add ?embed=1 to any route to show only the example itself, without the
// navigation and the route heading, for use inside an iframe.
const [searchParams] = useSearchParams();
const embed = searchParams.get("embed") === "1";

return (
<div className="page">
<nav className="nav" aria-label="Examples">
{routes.map((route) => (
<NavLink
key={route.to}
to={route.to}
end={route.end}
className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")}
>
{route.label}
</NavLink>
))}
</nav>
<div className={embed ? "page embed" : "page"}>
{embed ? null : (
<nav className="nav" aria-label="Examples">
{routes.map((route) => (
<NavLink
key={route.to}
to={route.to}
end={route.end}
className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")}
>
{route.label}
</NavLink>
))}
</nav>
)}
<main>
<Outlet />
</main>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export interface EventOption {

export const events: EventOption[] = [
{ key: eventKey, name: "Opening night" },
{ key: import.meta.env.VITE_SEATLAYER_EVENT_KEY_2 ?? eventKey, name: "Saturday matinee" },
{ key: import.meta.env.VITE_SEATLAYER_EVENT_KEY_3 ?? eventKey, name: "Closing night" },
{ key: import.meta.env.VITE_SEATLAYER_EVENT_KEY_2 || eventKey, name: "Saturday matinee" },
{ key: import.meta.env.VITE_SEATLAYER_EVENT_KEY_3 || eventKey, name: "Closing night" },
];
10 changes: 10 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ body {
padding: 24px 16px 48px;
}

/* ?embed=1 drops the navigation and the route heading. See src/App.tsx. */
.page.embed {
max-width: none;
padding: 0;
}

.page.embed .intro {
display: none;
}

h1 {
font-size: 1.6rem;
margin: 0 0 4px;
Expand Down
Loading