Skip to content
Draft
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
17 changes: 17 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/scripts/claude-code-setup.sh",
"timeout": 600
}
]
}
]
}
}
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ cypress.env.json
/studio/.sanity
.env*.local

# Claude Code
.claude/
# Claude Code (shared settings are tracked, everything else stays local)
.claude/*
!.claude/settings.json

.beads
39 changes: 39 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# CLAUDE.md

Main site for Gi Effektivt / Ge Effektivt / Giv Effektivt — a multi-country (NO/SE/DK) donation platform. Next.js (Pages Router) frontend with Sanity as CMS, Auth0 for the logged-in profile pages, and the [effekt-backend](https://github.com/stiftelsen-effekt/effekt-backend) API for donation data.

## Commands

- `npm install` — install dependencies (Node version: see `.nvmrc`)
- `npm run dev` — dev server on http://localhost:3000
- `npm run typecheck` — TypeScript check (`tsc --noEmit`)
- `npm test` — Jest unit tests
- `npm run lint` — ESLint
- `npm run format` — Prettier (also runs on commit via husky/lint-staged)

Environment: copy `.env.example` to `.env.local`. The defaults point at a test Sanity dataset and a local backend; see comments in `.env.example` for the Swedish/Danish alternatives.

## Architecture notes

- **Custom routing**: only files with the `.page.tsx` extension are picked up by the Next.js router. `pages/[[...slug]].page.tsx` parses the slug and renders the correct page type. Page types are regular modules under `pages/` (without `.page.tsx`) that export static paths and props via the `withStaticProps()` helper.
- **Two layouts**: public pages use `components/main/layout.tsx`; everything under `pages/dashboard` uses the profile layout `components/min-side/layout.tsx` (wrapped in the Auth0 provider). Each page sets its layout via a `.Layout` property on the export.
- **Content**: fetched from Sanity with GROQ queries (`_queries.ts` and colocated query files). The Sanity studio lives in `studio/` as a separate npm project.
- **Reusable components** live in `components/`, mostly under `components/main/blocks/` for CMS-driven content blocks.

## Verifying UI changes (Claude Code cloud sessions)

Cloud sessions have `@playwright/test` and Chromium available (installed by `scripts/claude-code-setup.sh`). When a change affects anything user-visible:

1. Start the dev server (`npm run dev`) and wait until the affected page compiles.
2. Write a small throwaway Playwright script that opens the affected pages against `http://localhost:3000` and saves screenshots (desktop 1440×900 and mobile 390×844 viewports) to `screenshots/` at the repo root. Launch the browser with `launchSandboxPage()` from `scripts/claude-pw-sandbox.js` — it handles the sandbox's HTTPS proxy, its untrusted CA, QUIC, and hosts like `cdn.sanity.io` whose connections Chromium can't complete (without it, Sanity images render broken). Prefer viewport screenshots; if a full-page capture is genuinely needed, clip it to at most ~4000px tall — very tall images fail to upload and are unreadable in PRs anyway.
3. Commit the screenshots to the branch and embed them in the PR description using raw URLs: `https://raw.githubusercontent.com/stiftelsen-effekt/main-site/<branch>/screenshots/<file>.png`.
4. Keep screenshots small (PNG, only the affected pages) — reviewers may ask to drop the `screenshots/` commit before merge.

If Playwright or Chromium is missing, the environment's network allowlist probably lacks the Playwright CDN domains (`cdn.playwright.dev`, `playwright.azureedge.net`, `playwright.download.prss.microsoft.com`); say so in the PR instead of skipping verification silently.

## Conventions

- Prettier is the source of truth for formatting; don't hand-format.
- A fresh `npm install` can leave incidental `package-lock.json` churn (peer-dependency resolution entries). Don't commit lockfile changes unless you intentionally changed dependencies.
- Styles are CSS/SCSS modules colocated with components.
- Run `npm run typecheck` and `npm test` before considering a change done.
33 changes: 33 additions & 0 deletions scripts/claude-code-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
# Prepares the cloud sandbox for Claude Code on the web (claude.ai/code).
# Runs via the SessionStart hook in .claude/settings.json; no-ops locally.

if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
exit 0
fi

cd "$CLAUDE_PROJECT_DIR" || exit 0

if [ ! -f .env.local ]; then
cp .env.example .env.local
fi

# Skip the Cypress binary download: download.cypress.io is not on the sandbox
# network allowlist, and the failing/hanging download otherwise aborts npm
# install (leaving node_modules empty) or burns the whole hook timeout.
# Cypress e2e runs are not part of the cloud workflow anyway.
export CYPRESS_INSTALL_BINARY=0

if [ ! -d node_modules ]; then
npm install --no-audit --no-fund || true
fi

# Playwright for visual verification of UI changes (not a project dependency;
# browser binaries are cached in the cloud environment). Requires the Playwright
# CDN domains on the environment's network allowlist — see CLAUDE.md.
if [ ! -d node_modules/@playwright/test ]; then
npm install --no-save --no-audit --no-fund @playwright/test || true
fi
npx playwright install --with-deps chromium || npx playwright install chromium || true

exit 0
85 changes: 85 additions & 0 deletions scripts/claude-pw-sandbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Playwright bootstrap for Claude Code cloud sandbox sessions.
*
* The sandbox routes outbound HTTPS through a proxy that Chromium neither
* picks up from the environment nor trusts (custom CA), and it advertises
* HTTP/3 which doesn't tunnel through the proxy. On top of that, Chromium
* connections to some hosts (e.g. cdn.sanity.io) are reset even via the
* proxy, while curl succeeds — requests to those hosts are fulfilled
* out-of-band with curl so images render in screenshots.
*
* Harmless locally: without proxy env vars it launches a plain browser.
*
* Usage (from a throwaway verification script at the repo root):
* const { chromium } = require("@playwright/test");
* const { launchSandboxPage } = require("./scripts/claude-pw-sandbox");
* const { browser, page } = await launchSandboxPage(chromium, {
* viewport: { width: 1440, height: 900 },
* });
* await page.goto("http://localhost:3000");
* await page.screenshot({ path: "screenshots/home-desktop.png" });
* await browser.close();
*/
const { execFileSync } = require("child_process");

// Hosts that reset direct/proxied Chromium connections in the sandbox.
// Keep this to asset CDNs — the fallback only handles GET requests.
const CURL_FALLBACK_HOSTS = [/^cdn\.sanity\.io$/];

const TYPE_BY_EXT = {
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
webp: "image/webp",
gif: "image/gif",
avif: "image/avif",
ico: "image/x-icon",
json: "application/json",
};

function contentTypeFor(url) {
const ext = new URL(url).pathname.split(".").pop().toLowerCase();
return TYPE_BY_EXT[ext] || "application/octet-stream";
}

async function launchSandboxPage(chromium, { viewport } = {}) {
const proxy =
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
process.env.HTTP_PROXY ||
process.env.http_proxy;

const browser = await chromium.launch({
proxy: proxy ? { server: proxy } : undefined,
args: ["--disable-quic"],
});
const context = await browser.newContext({
ignoreHTTPSErrors: true,
viewport,
});

await context.route(
(url) => CURL_FALLBACK_HOSTS.some((re) => re.test(url.hostname)),
async (route) => {
if (route.request().method() !== "GET") {
await route.continue().catch(() => {});
return;
}
const url = route.request().url();
try {
const body = execFileSync("curl", ["-sSfL", "--max-time", "20", url], {
maxBuffer: 50 * 1024 * 1024,
});
await route.fulfill({ body, contentType: contentTypeFor(url) });
} catch {
await route.continue().catch(() => {});
}
},
);

const page = await context.newPage();
return { browser, context, page };
}

module.exports = { launchSandboxPage };
Loading