Skip to content

Struis112/struisict.com

Repository files navigation

struisict.com

Marketing site, helpcentrum and tools for Struis ICT. Built with Next.js 16 + React 19 + TypeScript, statically exported, deployed on Cloudflare Pages.

Stack

  • Next.js 16.2 (App Router, Turbopack)
  • React 19.2
  • TypeScript 6.0 (strict)
  • ESLint 10 (flat config, eslint-config-next)
  • No CSS framework — hand-rolled styles in app/globals.css (ported from struis-ict.css)
  • qrcode-generator for the WiFi-QR tool

Contributing

See CONTRIBUTING.md for the full guide. TL;DR: branch off main, conventional-commit your changes, the pre-commit hook formats

  • lints automatically, CI runs unit + integration + e2e + coverage on the PR, Codecov comments the diff, and Cloudflare Pages posts a live preview URL.

Develop

npm install
npm run dev      # http://localhost:3000

Build (static export)

npm run build    # writes to out/
npm run serve    # serves out/ via npx serve, for local preview of the production build

The build emits a fully static site to out/. next.config.ts sets output: "export", trailingSlash: true, images.unoptimized: true.

Quality scripts

npm test                  # vitest, full run — unit + integration combined (~18 s, 396 tests)
npm run test:unit         # only unit tests — pure functions + component renders (310 tests)
npm run test:integration  # only integration tests — components + user interactions (86 tests)
npm run test:watch        # vitest in watch mode
npm run test:coverage     # vitest + V8 coverage report (writes coverage/, enforces thresholds)
npm run e2e               # Playwright end-to-end tests (boots dev server, records video)
npm run e2e:ui            # Playwright in interactive UI mode
npm run e2e:headed        # Run E2E with visible browser windows
npm run e2e:report        # Open last Playwright HTML report
npm run lint              # next lint (Next-specific rules)
npm run lint:oxlint       # oxlint (fast Rust linter, separate ruleset)
npm run lint:oxlint:fix   # oxlint + auto-fix
npm run format            # oxfmt — format the whole repo
npm run format:check      # oxfmt --check (fails if anything is unformatted; CI uses this)

Coverage thresholds (enforced by vitest) live in vitest.config.ts: 78 % lines / 76 % statements / 78 % functions / 75 % branches. CI fails if coverage drops below those. components/SiteScripts.tsx is excluded from unit coverage — its global-DOM useEffect is exercised end-to-end by Playwright instead.

Test suite layers

The vitest suite is split into two named layers by file naming convention:

  • Unit tests*.test.{ts,tsx} (excluding *.integration.test.tsx). Pure functions, single-component renders without user interaction, page smoke tests. Fast, isolated, deterministic.
    • app/tools/**/*.test.ts — pure helpers (scoring, filter, regex, etc.)
    • components/{Footer,Nav,Icon,BackToTop,TitleSwap,WhatsAppButton}.test.tsx
    • app/**/page.test.tsx — route smoke tests
  • Integration tests*.integration.test.tsx. Render a component, drive it via userEvent (clicks, typing, slider drags), assert state-driven output. Multiple units cooperating.
    • app/tools/{wachtwoord,wachtwoordzin}/Generator.integration.test.tsx
    • app/tools/{avg-check,back-up-check}/{AvgCheck,BackupCheck}.integration.test.tsx
    • app/tools/{faq,foto-verkleinen,wifi-qr,link-check}/*.integration.test.tsx
    • components/SiteScripts.integration.test.tsx
  • End-to-end testse2e/*.spec.ts, run by Playwright (next section).

End-to-end tests

Playwright runs in chromium and records video for every test (kept for both passes and failures so you can rewatch a flow). Specs live in e2e/:

Spec Covers
smoke.spec.ts Every route returns 2xx and shows its expected heading
theme-and-lang.spec.ts Theme/lang toggle wiring, hamburger, back-to-top
tarieven-calculator.spec.ts Full price-calculator flow incl. €40 haal/breng toggle
wachtwoord.spec.ts, wachtwoordzin.spec.ts Generators (length, charsets, copy via clipboard API)
wifi-qr.spec.ts Mode switcher, canvas QR rendering, hidden / open network
link-check.spec.ts Phishing detection, IDN/Punycode, Cmd+Enter, Clear, examples
avg-check.spec.ts, back-up-check.spec.ts Quiz flows, scoring bands, language switch
foto-verkleinen.spec.ts Image upload, preview, slider re-process, reset
faq.spec.ts Search filter, category chips, expand/collapse
contact-form.spec.ts Formspree mock; success + error paths

CI uploads playwright-report/ and test-results/ (videos, traces, screenshots) as artifacts retained 14 days. Locally, after a run open npm run e2e:report to browse the HTML report.

CI / CD on GitHub

Workflows live in .github/workflows/:

Workflow Trigger What it runs
ci.yml every push + every PR verify job: lint:oxlintformat:checktest:coveragebuild. Uploads coverage artifact + (on main) static out/. e2e job: Playwright on chromium with video recording — runs after verify passes; uploads HTML report + videos as artifacts.

Deploy and PR previews are handled by Cloudflare Pages (no workflow in this repo): every push to main triggers a production build at https://struisict.com/; every PR gets a preview URL posted by the Cloudflare GitHub app.

dependabot.yml opens grouped weekly dependency-update PRs (Mondays).

Conventional commits

Use prefixes (feat, fix, chore, docs, test, refactor, style, ci, perf) on every commit. Examples:

  • fix(tarieven): correct €0.50/km calc
  • feat(tools): add wifi-qr generator
  • feat!: … for breaking changes

Branch protection (manual one-time setup in GitHub)

To enforce "CI must pass before merge", go to Settings → Branches → Add branch ruleset in GitHub and require these checks on main:

  • Require a pull request before merging
  • Require status checks to pass: tick verify (the CI job)
  • Require branches to be up to date before merging
  • (Optionally) Require conversation resolution before merging

Without that ruleset configured, CI still runs but isn't enforced — the workflow file alone can't block a merge.

Pre-commit hook

.husky/pre-commit runs npx lint-staged, which pipes staged files through oxfmt --write then oxlint --fix. So local commits arrive at GitHub already-formatted and lint-clean — CI is a backstop, not the only check.

Deploy

Deploys go through Cloudflare Pages, connected to this GitHub repo. The Pages project's build settings:

  • Framework preset: Next.js (Static HTML Export)
  • Build command: npm run build
  • Build output directory: out
  • Environment variable: NODE_VERSION = 24.15.0 (matches package.json)

Every push to main builds and deploys to https://struisict.com/; every PR gets a Cloudflare-hosted preview URL.

Two CFP-specific files in public/ are copied into out/ at build time:

  • _redirects — 301s for the Catch a Break legacy paths (now at https://catch-a-break.web.app/) and the *.html*/ URL scheme migration.
  • _headers — security headers (CSP, HSTS, X-Frame-Options, Permissions-Policy, etc.). HTTPS enforcement, gzip/brotli, and edge caching come from Cloudflare itself.

i18n

Class-based show/hide. Every translatable element renders both NL and EN spans/paragraphs in the DOM; globals.css hides the inactive one based on body.lang-en. A synchronous script in <head> (defined inline in app/layout.tsx) sets the initial lang-en class from localStorage or navigator.languages to prevent flash. The LangProvider is implemented inside components/SiteScripts.tsx (the toggle handler updates the body class + localStorage).

For per-page document titles, drop a <TitleSwap nl="…" en="…" /> at the top of the page; it sets data-title-nl / data-title-en on <html> and updates document.title when the language changes.

Layout & components

  • app/layout.tsx — owns <Nav>, <Footer>, <WhatsAppButton>, <BackToTop>, <SiteScripts>. Pages do not render any of these.
  • components/SiteScripts.tsx (client) — single useEffect keyed on pathname that wires up scroll reveal, smooth-scroll anchors, theme/lang toggle, hamburger menu, contact-form submit, and the tarieven calculator. Re-runs on every route change so observers attach to fresh DOM.
  • components/Icon.tsx<Icon name="i-foo" /><svg class="icon"><use href="/icons.svg#i-foo"/></svg>. The sprite lives at public/icons.svg.
  • Per-tool client widgets live alongside their server page.tsx (e.g. app/tools/wachtwoord/Generator.tsx).

Routes

Route Source
/ app/page.tsx (LocalBusiness JSON-LD)
/apps/ app/apps/page.tsx
/daniel/ app/daniel/page.tsx
/myfleet/ app/myfleet/page.tsx (SoftwareApplication + FAQPage JSON-LD)
/tarieven/ app/tarieven/page.tsx (price calculator)
/tools/ app/tools/page.tsx (helpcentrum landing)
/tools/faq/ app/tools/faq/page.tsx + Search.tsx + faq-data.ts
/tools/wachtwoord/ app/tools/wachtwoord/page.tsx + Generator.tsx
/tools/wachtwoordzin/ app/tools/wachtwoordzin/page.tsx + Generator.tsx + wordlist.ts
/tools/wifi-qr/ app/tools/wifi-qr/page.tsx + WifiQR.tsx
/tools/foto-verkleinen/ app/tools/foto-verkleinen/page.tsx + ImageResizer.tsx
/tools/avg-check/ app/tools/avg-check/page.tsx + AvgCheck.tsx
/tools/back-up-check/ app/tools/back-up-check/page.tsx + BackupCheck.tsx
/tools/link-check/ app/tools/link-check/page.tsx + LinkCheck.tsx
/voorwaarden/ app/voorwaarden/page.tsx
/404.html app/not-found.tsx (referenced by htaccess ErrorDocument 404)

Notes

  • Favicons (favicon.ico, favicon-32.png, favicon-16.png, apple-touch-icon.png) are not in this repo — they were historically uploaded to the production server out-of-band. They are referenced in app/layout.tsx's metadata. Add them to public/ if you want them included in the build.
  • The Catch a Break files are no longer part of this repo; that project lives at Struis112/catch-a-break-web and deploys to https://catch-a-break.web.app/. The _redirects file issues 301s from the old URLs to that host.

About

struisict.com — public website (Daniel Struis ICT)

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors