A sample event/artist ticketing frontend built on Next.js 14 (App Router), TypeScript, Redux Toolkit, and a headless-CMS-shaped content model: browse shows on the home page, drill into an artist's page to pick a venue/date/time, or land on a venue page grouped by its upcoming shows. It exists to show the pattern end to end - CMS-style content envelopes, dynamic slug-based routing, i18n, and GSAP-driven, media-heavy layouts - rather than to be a product in its own right.
This is a portfolio/demo repository. There is no private backend: every "CMS" call is served locally by a small mock client (
src/lib/cms/) shaped exactly like a real headless CMS response, so the app runs fully standalone with zero configuration.
- CMS-style content modeling - fixture data is reshaped into the same
{ data: { id, attributes } }envelope a real headless CMS (e.g. Strapi) returns, including relations and media fields, so swapping in a real backend touches one file (src/lib/cms/client.ts), not the components. - Dynamic, ambiguous-slug routing - artists and venues share the same top-level slug space
(
/[artistSlug]); the venue route resolves which one a given slug means and renders accordingly, a realistic constraint for a CMS where editors pick slugs independently. - An interactive ticket picker - venue, date, and time selection (
ShowDateTime) with an active-state reveal animation, deep-linkable via?activeEvent=. - Full i18n, RTL by default -
i18nextwith Hebrew (default, RTL) and English locales; the document rendersdir="rtl"out of the box. - GSAP-driven motion - scroll-triggered reveals, a page-load sequence, and hover interactions,
without any custom
window.innerWidth/resize-listener logic. - Resilient media - every image goes through
next/image, including a fallback component that swaps to a local "image not found" asset on load error, so a broken CMS media URL never breaks layout. - Explicit error and empty states - the CMS client exposes a test-only simulated-failure hook so the rejected-thunk path is exercised for real, not just assumed to work.
- Strict TypeScript - the full
strictcompiler family plusexactOptionalPropertyTypes/noUncheckedIndexedAccess, zero-warning ESLint.
All captured from the app running locally against the mock CMS layer (npm run dev).
The hero banner (rotates through featured shows on each load), followed by the categorized show rows.
An artist's hero plus the interactive venue/date/time ticket picker.
A venue page, resolved from the same /[artistSlug] route segment artists use.
The full-screen search overlay, showing a live suggestion as the query is typed.
The home page at a 390px viewport - single-column cards, RTL-aware stacking.
| Technology | Role |
|---|---|
| Next.js 14 (App Router) | Routing, Server Components, rendering |
| TypeScript 5 (strict) | Type safety |
| Redux Toolkit | Application state, createAsyncThunk-backed slices |
| GSAP + ScrollTrigger | Scroll-triggered and page-load animation |
| Swiper | Show/venue carousels and the ticket date/time picker |
| Ant Design | Form primitives (search input, newsletter form) |
| i18next / react-i18next | Internationalization (Hebrew default, English fallback) |
| dayjs | Date formatting and locale-aware day names |
| cheerio | Stripping HTML from CMS-style rich-text fields for card previews |
| Jest + React Testing Library | Unit and integration tests |
| ESLint, Prettier, Husky, commitlint, Gitleaks | Code quality and security gates |
flowchart LR
UI["React UI\n(app/, components/)"]
RTK["Redux Toolkit slice\n(createAsyncThunk)"]
Client["CMS client\n(src/lib/cms/client.ts)"]
Shapes["Envelope shaping\n(src/lib/cms/shapes.ts)"]
Fixtures["Fixture data\n(src/lib/cms/fixtures)"]
UI -->|dispatch thunk, or await directly in a Server Component| RTK
RTK --> Client
UI -->|Server Components call it directly| Client
Client --> Shapes
Shapes --> Fixtures
Fixtures -->|CMS-shaped envelope| Client
Client -->|typed result| RTK
RTK -->|selector| UI
Server Components (the route files themselves) call src/lib/cms/client.ts directly and await
the result during render, rather than dispatching through Redux - the store is a client-side
singleton, and Next.js serves concurrent requests on the same process, so a Server Component
dispatch could race another request's dispatch. Client components that need CMS data after the
initial render (Header, Footer, ArtistTickets) go through the Redux slices instead. Either
path ends at the same client.ts functions, so swapping fixtures for a real CMS is a one-file
change (see Mock CMS layer).
src/
app/ Next.js App Router routes - see the route map below
components/ Route-level and shared UI components
redux/ Redux Toolkit store and slices
lib/cms/ Mock CMS client + fixtures + envelope shaping (see below)
i18n/ i18next config and locale resources
assets/ Icons and images bundled with the app
helpers/, utils/ Small framework-agnostic utilities
public/images/fixtures/ Placeholder artwork referenced by the CMS fixtures
docs/screenshots/ README screenshots
| Route | File | Purpose |
|---|---|---|
/ |
src/app/(HomeLayout)/page.tsx |
Home: hero banner, categorized show rows, newsletter CTA |
/[artistSlug] |
src/app/[artistSlug]/(venue)/page.tsx |
Venue page for that slug, or falls back to the artist at that slug (see below) |
/[artistSlug]/[artistId] |
src/app/[artistSlug]/[artistId]/page.tsx |
A specific artist + show: hero, ticket/date picker |
(venue) is a
route group - it
does not appear in the URL. It exists so /[artistSlug] and /[artistSlug]/[artistId] can be
sibling segments under the same dynamic parent without conflicting.
Resolving an ambiguous /[artistSlug]:
Venue route receives :artistSlug
-> queries both the venue collection and the artist collection for that slug
-> neither matches -> notFound()
-> an artist matches -> renders the artist page's component directly
-> that component redirects to the artist's next show, /[artistSlug]/[artistId]
-> otherwise -> treated as a venue, its grouped show listing renders
Loading the artist detail route:
/[artistSlug]/[artistId] Server Component
-> await cms.getAllArtistData(artistSlug), await cms.getArtistShowsDetail(artistId)
-> either missing -> notFound()
-> ShowDateTime renders the venue/date/time picker from the resolved show data
-> selecting a slot updates the URL (?activeEvent=...) so the picked slot is deep-linkable
See .env.example. The only variable the app reads is NEXT_PUBLIC_IMAGE_URL, an optional prefix
for CMS-hosted media URLs - it's unused while running against the local fixtures, and only becomes
relevant once client.ts is pointed at a real CMS.
src/lib/cms/ stands in for a real headless CMS:
fixtures/entities.ts- a small, flat "database" of artists, venues, and shows. This is the only file you'd touch to change the sample content.shapes.ts- reshapes that flat data into the same{ data: { id, attributes } }envelope a real headless CMS returns, including relations (Artists: { data: [...] }) and media fields ({ data: { attributes: { url } } }).client.ts- one async function per endpoint the app uses (getSettings,getAllArtistData,getVenueShows, etc.), each resolving fixture data with a simulated network delay. It also exposes__setSimulatedFailure(), a test-only hook used to exercise error states without a real network mock.
To point this at a real CMS, replace the bodies of the functions in client.ts with
fetch/axios calls against your CMS's API - the envelope shapes, and everything that consumes
them (components, Redux slices), stay the same.
src/i18n/index.ts configures i18next with two locales, he (default, RTL) and en, loaded
from src/i18n/locales/<lng>/home.json. The app renders RTL by default (<html lang="he" dir="rtl">
in src/app/layout.tsx).
To add a locale: create src/i18n/locales/<lng>/home.json, add it to the resources map and
supportedLngs array in src/i18n/index.ts, and add its locale code to next-i18next.config.mjs.
- GSAP drives page and scroll-triggered animation (
gsap.registerPlugin(ScrollTrigger)inHeader/Footer), plus a page loader sequence incomponents/Layout. - Fonts load via
next/font/google(Heebofor Hebrew + Latin body copy,Oswaldfor condensed headings) insrc/app/layout.tsx. Both are self-hosted by Next at build time - no runtime requests to Google Fonts, and no font files checked into the repo. - Images go through
next/imageeverywhere, including a smallFallBackImgcomponent (src/components/common/Image/FallBackImg.tsx) that swaps to a local "image not found" asset on load error. - Video is optional and data-driven - the hero banner plays a video when the current fixture
show has one (
Show_Video), and falls back to a poster image when it doesn't - the same branch a real CMS-backed deployment with actual show footage would use.
Requires the Node version pinned in .nvmrc, plus
Gitleaks 8.30.x on PATH before your first commit
- the pre-commit hook refuses to run without it.
npm install
npm run prepare # installs the Husky git hooks (also runs automatically after npm install)
npm run devOpen http://localhost:3000. No environment variables are required to run, build, or test the app - see Environment variables above.
| Command | Purpose |
|---|---|
npm run dev |
Start the Next.js dev server. |
npm run build |
Type-checked production build (no ignoreBuildErrors escape hatch). |
npm start |
Serve the production build. |
npm run quality |
format:check + lint + typecheck, all zero-tolerance (what the pre-commit hook runs). |
npm run format / format:check |
Prettier write / check. |
npm run lint |
ESLint, flat config, --max-warnings=0. |
npm run typecheck |
tsc --noEmit. |
npm test |
Jest + React Testing Library. |
npm run security:audit |
npm audit --audit-level=high. |
npm testJest (via next/jest) + React Testing Library, covering:
- dynamic route data resolution from fixtures (known + unknown slugs/ids)
- i18n key lookup across locales, including fallback behavior
- the missing-image fallback path
- the CMS client's simulated-failure / rejected-thunk path
This repo's tooling is adapted from Webvoltz's internal Engineering Standards, with the runtime deliberately left on Next.js 14 / React 18 / TypeScript 5 and CI kept on GitHub Actions rather than the standard's GitLab pipeline - both were already in place before this pass and weren't worth the risk of an unrelated major-version bump.
A Husky pre-commit hook runs Gitleaks secret scanning, lint-staged, the full quality check, and
a production build before any commit is allowed through; commit-msg enforces Conventional Commits
via commitlint. The same gates run in CI (.github/workflows/ci.yml) - secret scan and dependency
audit first, then quality and commit-message lint, then tests, then the production build.
TypeScript strictness: every flag from the standard's strict preset is on -
noUncheckedIndexedAccess, exactOptionalPropertyTypes, noPropertyAccessFromIndexSignature,
noUnusedLocals/noUnusedParameters, noImplicitReturns, noImplicitOverride, and friends - and
next.config.mjs's typescript.ignoreBuildErrors escape hatch (present in the original codebase)
is gone; next build now type-checks for real.
One deliberate exception: skipLibCheck stays true. Turning it off surfaces type declaration
bugs in third-party packages this repo depends on (@reduxjs/toolkit, antd, even next itself)
once exactOptionalPropertyTypes is on - none of which are fixable without editing node_modules.
Documented here rather than silently dropped.
Tracked, not done in this pass:
@typescript-eslint/no-explicit-anyis off. This codebase inherited roughly 85: anycall sites from before this cleanup; turning the rule on today would fail--max-warnings=0across the whole repo. Everything else the standard requires (no-unsafe-*,no-floating-promises, exhaustive switches, etc.) is a larger, separate pass once those call sites are typed properly.- The runtime major-version bump (Next 16 / React 19 / TypeScript 6), called for by the standard, is out of scope here - it'd be a separate migration with its own review, not a drive-by tooling change.
- GitLab CI. This repo lives on GitHub; the standard's GitLab quality jobs (secret-scan,
dependency-audit, quality, commitlint, test, build) are reproduced as GitHub Actions jobs in
.github/workflows/ci.ymlinstead of being ported as-is.
- Server Components call the CMS client directly instead of dispatching Redux -
src/redux/store.tsis a client-side singleton; Next.js serves concurrent requests (including the browser's own<Link>prefetch racing a real navigation) on the same process, so a Server Component dispatch could read back another request's state. Routesawait cms.*()directly instead; only client components that need CMS data post-render go through Redux. - CMS-shaped fixtures instead of ad hoc mock data -
src/lib/cms/shapes.tsreshapes flat fixture data into the same envelope a real headless CMS returns, so the components, Redux slices, and TypeScript interfaces are all written against the shape a real integration would actually produce - swapping the mock for a live CMS doesn't ripple through the codebase. - A shared
IMAGE_BASE_URLconstant, never a bare template string - every CMS-hosted image URL is built as`${IMAGE_BASE_URL}${path}`;IMAGE_BASE_URLfalls back to''so fixture-relative paths resolve correctly whenNEXT_PUBLIC_IMAGE_URLisn't set, instead of rendering the literal string"undefined"in front of every image URL. - Decorative placeholder art renders its label from i18next, not baked into the image - the
gift-card and gallery placeholder graphics are inline SVG React components
(
src/components/common/PlaceholderArt) that pull their caption from the current locale, so a purely decorative label still follows the site's language instead of being frozen in whichever language the asset happened to be generated in. skipLibCheck: false-equivalent rigor, with one documented exception - see Code quality and security.
- A ticket card's heading/date never appears - some of this app's reveal animations are gated on
an
activeclass that has to be applied by the component using them (see the fix inShowDateTime); a component that renders.text_box_wrapwithout ever addingactivegets stuck in the CSS's permanently-hidden end state. - An image renders full-bleed instead of its intended size - check for a same-named CSS class
(e.g.
.image_box_wrap) reused by a different component elsewhere in the same page bundle; Next.js bundles all component CSS per route, so two components sharing a generic class name can leak styles into each other even though neither one is scoped incorrectly on its own. - Pre-commit hook fails immediately - it refuses to run without Gitleaks on
PATH(gitleaks versionshould print8.30.x); see Code quality and security. - A CMS URL renders the literal text
"undefined"-NEXT_PUBLIC_IMAGE_URLisn't set and the code path building the URL isn't using the sharedIMAGE_BASE_URLconstant (see Design decisions).
- A shows/tickets listing on the venue page itself (currently hero-only; grouped show data is fetched but not yet rendered there).
- A real gallery section wired up to the artist gallery fixture data (
GalleryPlaceholderexists but has no current consumer). - Real backend integration guide once a production headless CMS instance exists.
- Checkout/payment flow beyond the ticket picker.
Portfolio/demonstration repository - no license file is included.




