diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..96e4f580 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +* @anyulled + +# Documentation and Architecture +/ARCHITECTURE.md @anyulled +/SECURITY.md @anyulled +/docs/ @anyulled + +# Core Configuration +/package.json @anyulled +/.github/workflows/ @anyulled diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..add22519 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Description + +Please describe the changes in this PR. Include motivation, problem addressed, and details of the solution. + +## Checklist + +- [ ] Tests pass (Unit and E2E) +- [ ] Linter & Formatter pass (`npm run lint`, `npm run format:check`) +- [ ] SonarQube findings have been addressed +- [ ] No unhandled Promise rejections, `any` types eradicated +- [ ] Small Batch: This PR is focused and logically independent. + +## Testing Performed + +Please provide a brief output or description of the tests run to verify the change. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b8e1a091..28535821 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,15 +5,55 @@ updates: schedule: interval: "daily" groups: - react: + eslint: patterns: - - "react*" - - "@types/react*" + - "eslint*" + - "@eslint*" + - "@typescript-eslint*" + - "@next/eslint-plugin-next" + vercel: + patterns: + - "@vercel/*" testing: patterns: - "@testing-library*" - "cypress*" - "jest*" + - "@stryker-mutator*" + - "@jazzer.js*" + - "ts-jest" + - "fast-check" + lint-and-format: + patterns: + - "stylelint*" + - "prettier" + - "husky" + - "lint-staged" + - "@commitlint*" + animations: + patterns: + - "framer-motion" + - "gsap" + - "aos" + - "@types/aos" + - "wowjs" + sliders-and-ui: + patterns: + - "swiper" + - "slick-carousel" + - "react-slick" + - "@types/react-slick" + react: + patterns: + - "react" + - "react-dom" + - "react-countup" + - "react-modal-video" + - "@types/react*" + typescript: + patterns: + - "typescript" + - "@types/*" all-others: patterns: - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df2cfa33..a3a62c90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,11 +17,19 @@ jobs: node-version: 20 cache: "npm" - name: Install Dependencies - run: npm ci + run: npm ci --legacy-peer-deps - name: Lint run: npm run lint - name: Format Check run: npm run format:check + - name: Security Audit + run: npm audit --audit-level=high || true + - name: Docs Sync Check + run: npm run check:docs + - name: Link Checker + uses: lycheeverse/lychee-action@v2 + with: + args: --offline --root-dir . --exclude-path node_modules '**/*.md' test: runs-on: ubuntu-latest @@ -33,7 +41,7 @@ jobs: node-version: 20 cache: "npm" - name: Install Dependencies - run: npm ci + run: npm ci --legacy-peer-deps - name: Unit Tests run: npm run test:coverage - name: Upload to Codecov @@ -51,6 +59,6 @@ jobs: node-version: 20 cache: "npm" - name: Install Dependencies - run: npm ci + run: npm ci --legacy-peer-deps - name: Build run: npm run build diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index aca41431..5ed28af3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -24,11 +24,12 @@ jobs: cache: "npm" - name: Install Dependencies - run: npm ci + run: npm ci --legacy-peer-deps - name: Cypress Run uses: cypress-io/github-action@v7 with: + install: false build: npm run build start: npm start wait-on: "http://localhost:3000" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..84c45770 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,24 @@ +name: AI Harness Scorecard + +on: + push: + branches: [main] + schedule: + - cron: "0 6 * * 1" + +jobs: + scorecard: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + - uses: markmishaev76/ai-harness-scorecard@v1 + id: scorecard + - name: Commit badge and report + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add scorecard-badge.json scorecard-report.md + git diff --cached --quiet || git commit -m "chore: update scorecard badge and report" + git push diff --git a/.gitignore b/.gitignore index 716c0d53..a7fc7a28 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,10 @@ dist .DS_Store next-env.d.ts +# PWA service worker (generated by @ducanh2912/next-pwa) +public/sw.js +public/sw.js.map +public/workbox-*.js +public/workbox-*.js.map +public/swe-worker-*.js +public/swe-worker-*.js.map diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 00000000..c160a771 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit ${1} diff --git a/.husky/pre-push b/.husky/pre-push index d477bdd4..fa239a6a 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,5 +1,5 @@ export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" -npm test +npm run test:coverage npm run build \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3f697bbd --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# AI Agent Instructions + +This document provides system instructions for AI coding assistants working on the DevBcn project. + +## Code Style & Constraints + +- **Architectural Boundaries**: React Components must not import from `app/`. Data must flow down via props. +- **Typing**: Use strict TypeScript. Avoid `any` at all costs. +- **Error Handling Policy**: Avoid unhandled promise rejections. Do not use generic catch-all statements without logging or handling the error properly. +- **Comments**: Code must be self-documenting. DO NOT add inline comments explaining _what_ code does. Only explain _why_ non-obvious decisions were made. +- **Styling**: SCSS must be used. No Tailwind or CSS Modules. + +## AI Usage Norms + +- No unchecked AI-generated code should be pushed to main. All code must pass the test suite. +- Ensure code changes are manually reviewed as per branch protection rules. +- Agents must never bypass `pre-commit`, `commit-msg`, or `pre-push` Git hooks. +- Do not disable eslint rules. +- Check the build and tests always pass before completing a task. +- Check SonarQube findings and resolve them before claiming any task is done. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..51c61d36 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,155 @@ +# Architecture + +This document describes the high-level architecture of the DevBcn website. +If you want to familiarize yourself with the codebase, this is a good place to start. + +## Bird's Eye View + +A Next.js 16 App Router application serving the DevBcn conference website. +Static and multi-year: every past and current edition lives under `/{year}`. +Speaker, talk, and schedule data come from the **Sessionize API** at runtime. +Edition-specific configuration (dates, feature flags, sponsors) is defined in code. +Deployed to Vercel with PWA support via `@ducanh2912/next-pwa`. + +``` +Sessionize API ──► hooks/ ──► app/[year]/ pages + ▲ +config/editions/ ─────────────────┘ +``` + +## Module Map + +### `app/` + +Next.js App Router routes. Two routing layers: + +- **Top-level** (`app/(global)/`, `app/about/`, `app/blog/`, …): year-independent pages. +- **`app/[year]/`**: year-scoped pages (speakers, talks, schedule, sponsors, etc.). + Contains its own `layout.tsx` that resolves the edition config from the URL param. + +The `app/[year]/@modal/` directory uses Next.js parallel routes for modal intercepts. + +`app/layout.tsx` is the root layout: fonts, global CSS imports, analytics, PWA meta. + +### `config/` + +Static, compile-time configuration. Three sub-modules: + +- **`config/editions/`**: Per-year config files (`2023.ts` … `2026.ts`) implementing + `EditionConfig`. Controls feature flags, dates, sponsor data, Sessionize URLs. + `CURRENT_EDITION` constant determines the default year. +- **`config/navigation/`**: Centralized nav links consumed by header and mobile menu. +- **`config/job-offers/`**: Job listing data. + +**Constraint**: `config/` must remain pure data — no React, no side effects, no fetching. + +### `hooks/` + +Server-side data-fetching functions (despite the directory name, these are **not** +React hooks). Each wraps a Sessionize API endpoint with `react.cache()`: + +- `useSpeakers.ts` → `/view/Speakers` +- `useTalks.ts` → `/view/Sessions` +- `useSchedule.ts` → `/view/GridSmart` + +Types for Sessionize responses live in `hooks/types.ts`. + +**Constraint**: these functions are designed for Server Components only. They use +`fetch` with `next: { revalidate: 3600 }` — do not call them from Client Components. + +### `components/` + +React components, organized by purpose: + +- **`layout/`**: Shell components — header, footer, mobile menu, breadcrumb, speaker/talk + cards, filter bars. Most are Client Components (`'use client'`). +- **`elements/`**: Small interactive widgets — countdown, back-to-top, theme switch, + video player, track badges. +- **`sections/`**: Full page sections composed for home page variants (`home1/` … `home10/`), + FAQ, and venue. +- **`ui/`**: Generic UI primitives (Modal). +- **`speakers/`**, **`talks/`**, **`schedule/`**, **`slider/`**, **`skeletons/`**: Domain-specific + component groups. + +**Constraint**: components must not import from `app/`. Data flows down from route +pages through props. + +### `context/` + +Client-side React Context. Currently only `ScheduleContext` — manages saved sessions +via `localStorage`. Provided by `ClientLayout` at the root. + +**Constraint**: contexts are always Client Components. Keep them minimal to avoid +unnecessary re-renders. + +### `lib/shared/` + +Pure utility functions: markdown rendering, JSON-LD generation, analytics helpers, +slugification, speaker/talk filter logic. + +**Constraint**: no React imports, no state, no side effects. Must be importable from +both server and client code. + +### `styles/` + +SCSS-based styling system: + +- `main.scss`: entry point. +- `theme/`: design tokens and variables. +- `components/`: component-specific styles. +- `layout/`: structural layout styles. +- `utils/`: mixins and helpers. +- `vendor/`: third-party CSS (Bootstrap, AOS, Slick, etc.). + +**Constraint**: no CSS Modules, no Tailwind. All styles flow through the SCSS pipeline. + +### `types/` + +Shared TypeScript interfaces for component props (`HeaderProps`, `BreadcrumbProps`, +`BackToTopProps`). Sessionize domain types live in `hooks/types.ts` instead. + +### `team/` + +Static team member data (`TeamMembers.ts`). + +### `__tests__/` + +Jest unit tests mirroring the source tree structure. +Cypress E2E tests live in `cypress/`. + +## Cross-Cutting Concerns + +### Multi-Year Support + +The `[year]` dynamic segment is the backbone of the site. Each year's page resolves +its `EditionConfig` via `getEditionConfig(year)` and passes it (or derived data) +down through props. Navigation links are prefixed with the active year. + +### External Data + +All speaker, talk, and schedule data comes from Sessionize. The API URL is stored +per-edition in `config/editions/`. Data is cached at the `fetch` level +(`revalidate: 3600`) and at the React level (`cache()`). + +### Quality Tooling + +- **ESLint** (flat config) + **Prettier** + **Stylelint** enforced via Husky pre-commit hooks. +- **lint-staged** runs formatters and linters on staged files. +- Git hooks must never be bypassed. + +## Invariants + +1. `config/` is pure data — no React, no fetching, no side effects. +2. `hooks/` fetch functions are server-only — never call from Client Components. +3. Components never import from `app/` — data flows down via props. +4. Styles use SCSS exclusively — no CSS Modules, no Tailwind. +5. Every edition must implement the full `EditionConfig` interface. +6. Git hooks (pre-commit, commit-msg, pre-push) are never bypassed. + +## Module Boundaries + +- `components/` never depends on `app/`. +- `hooks/` never depends on `components/` or `app/`. +- `config/` never depends on any other workspace module. +- `styles/` never depends on any other workspace module. +- `lib/` never depends on `app/` or `components/`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..96df03b0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +Thank you for contributing to DevBcn! + +## Small Batch Enforcement + +Please submit Pull Requests in small, cohesive batches. Do not submit monolithic PRs containing thousands of lines of changes, especially when AI coding assistants are used. Small PRs are easier to review and less likely to introduce subtle regressions. + +## Git Hooks + +Zero Tolerance: Never bypass git hooks (`--no-verify`). We focus on quality over speed. +All commits must successfully pass the pre-commit linters and tests. + +## Code Standards + +- Ensure 90% test coverage. +- Apply SOLID, DRY, KISS, and YAGNI principles. +- Follow the Law of Demeter and "Tell, Don't Ask" pattern. +- Fix any and all SonarQube issues before opening a PR. diff --git a/README.md b/README.md index 876dcc09..f6ac003b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# eventify nextjs +# Devbcn -# made by alithemes.com +[![AI Harness Scorecard](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fanyulled%2Fdevbcn-nextjs%2Frefs%2Fheads%2Fmain%2Fscorecard-badge.json)](scorecard-report.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..69210e46 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +Only the latest `main` branch is actively supported with security updates. + +## Reporting a Vulnerability + +If you discover a security vulnerability within this project, please responsibly disclose it by contacting the repository maintainers. Do not open public issues for security vulnerabilities. + +The `CODEOWNERS` file lists the individuals responsible for reviewing and merging code, particularly around security-critical paths such as caching logic, analytics, and dependencies. diff --git a/__tests__/components/Footer8.test.tsx b/__tests__/components/Footer8.test.tsx index b0f02abd..c598bc9d 100644 --- a/__tests__/components/Footer8.test.tsx +++ b/__tests__/components/Footer8.test.tsx @@ -4,7 +4,6 @@ import "@testing-library/jest-dom/jest-globals"; import { render, screen } from "@testing-library/react"; import React from "react"; -// Mock next/navigation BEFORE imports jest.mock("next/navigation", () => ({ __esModule: true, usePathname: jest.fn(() => "/2026"), @@ -19,4 +18,24 @@ describe("Footer8", () => { expect(img).toBeInTheDocument(); expect(img.getAttribute("src")).toContain("header-bg21.png"); }); + + it("renders layer1 decorative image with explicit dimensions", async () => { + const Footer8 = (await import("@/components/layout/footer/Footer8")).default; + const { container } = render(); + + const layer1 = container.querySelector('img[src*="layer1.png"]'); + expect(layer1).toBeInTheDocument(); + expect(layer1?.getAttribute("width")).toBe("1440"); + expect(layer1?.getAttribute("height")).toBe("230"); + }); + + it("renders logo image with explicit dimensions", async () => { + const Footer8 = (await import("@/components/layout/footer/Footer8")).default; + render(); + + const logo = screen.getByRole("img", { name: "devBcn" }); + expect(logo).toBeInTheDocument(); + expect(logo.getAttribute("width")).toBe("150"); + expect(logo.getAttribute("height")).toBe("76"); + }); }); diff --git a/__tests__/components/elements/AboutCounter.test.tsx b/__tests__/components/elements/AboutCounter.test.tsx new file mode 100644 index 00000000..cb839a7b --- /dev/null +++ b/__tests__/components/elements/AboutCounter.test.tsx @@ -0,0 +1,16 @@ +/** + * @jest-environment jsdom + */ + +import { describe, expect, it } from "@jest/globals"; +import "@testing-library/jest-dom"; +import "@testing-library/jest-dom/jest-globals"; +import { render } from "@testing-library/react"; +import AboutCounter from "@/components/elements/AboutCounter"; + +describe("AboutCounter Component", () => { + it("matches snapshot", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/__tests__/components/elements/BuyTicketButton.test.tsx b/__tests__/components/elements/BuyTicketButton.test.tsx new file mode 100644 index 00000000..87b8de37 --- /dev/null +++ b/__tests__/components/elements/BuyTicketButton.test.tsx @@ -0,0 +1,25 @@ +/** + * @jest-environment jsdom + */ + +import { describe, expect, it } from "@jest/globals"; +import "@testing-library/jest-dom"; +import "@testing-library/jest-dom/jest-globals"; +import { render } from "@testing-library/react"; +import BuyTicketButton from "@/components/elements/BuyTicketButton"; + +describe("BuyTicketButton Component", () => { + it("matches snapshot with default props", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("matches snapshot with custom props", () => { + const { container } = render( + + Child Node + + ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/__tests__/components/elements/CircleText.test.tsx b/__tests__/components/elements/CircleText.test.tsx new file mode 100644 index 00000000..dc8853ba --- /dev/null +++ b/__tests__/components/elements/CircleText.test.tsx @@ -0,0 +1,16 @@ +/** + * @jest-environment jsdom + */ + +import { describe, expect, it } from "@jest/globals"; +import "@testing-library/jest-dom"; +import "@testing-library/jest-dom/jest-globals"; +import { render } from "@testing-library/react"; +import CircleText from "@/components/elements/CircleText"; + +describe("CircleText Component", () => { + it("matches snapshot", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/__tests__/components/elements/__snapshots__/AboutCounter.test.tsx.snap b/__tests__/components/elements/__snapshots__/AboutCounter.test.tsx.snap new file mode 100644 index 00000000..c486a261 --- /dev/null +++ b/__tests__/components/elements/__snapshots__/AboutCounter.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`AboutCounter Component matches snapshot 1`] = ` +
+
+
+

+ + 0 + + + +

+
+

+ Our Journalist +

+
+
+

+ + 0 + + + +

+
+

+ Our Speaker +

+
+
+

+ + 0 + + K+ +

+
+

+ Attendees +

+
+
+
+`; diff --git a/__tests__/components/elements/__snapshots__/BuyTicketButton.test.tsx.snap b/__tests__/components/elements/__snapshots__/BuyTicketButton.test.tsx.snap new file mode 100644 index 00000000..f6a87790 --- /dev/null +++ b/__tests__/components/elements/__snapshots__/BuyTicketButton.test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`BuyTicketButton Component matches snapshot with custom props 1`] = ` + +`; + +exports[`BuyTicketButton Component matches snapshot with default props 1`] = ` + +`; diff --git a/__tests__/components/elements/__snapshots__/CircleText.test.tsx.snap b/__tests__/components/elements/__snapshots__/CircleText.test.tsx.snap new file mode 100644 index 00000000..bf887f7b --- /dev/null +++ b/__tests__/components/elements/__snapshots__/CircleText.test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`CircleText Component matches snapshot 1`] = ` +
+
+ + t + + + e + + + s + + + t + + +   + + + c + + + i + + + r + + + c + + + l + + + e + + +   + + + t + + + e + + + x + + + t + +
+
+`; diff --git a/__tests__/components/ui/Modal.test.tsx b/__tests__/components/ui/Modal.test.tsx new file mode 100644 index 00000000..ab5117e1 --- /dev/null +++ b/__tests__/components/ui/Modal.test.tsx @@ -0,0 +1,32 @@ +/** + * @jest-environment jsdom + */ + +import { describe, expect, it, jest } from "@jest/globals"; +import "@testing-library/jest-dom"; +import "@testing-library/jest-dom/jest-globals"; +import { render } from "@testing-library/react"; +import React from "react"; + +// Mock next/navigation +jest.mock("next/navigation", () => ({ + __esModule: true, + useRouter: jest.fn(() => ({ + replace: jest.fn(), + push: jest.fn(), + prefetch: jest.fn(), + back: jest.fn(), + })), +})); + +describe("Modal Component", () => { + it("matches snapshot", async () => { + const Modal = (await import("@/components/ui/Modal")).default; + const { container } = render( + +
Test Modal Content
+
+ ); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/__tests__/components/ui/__snapshots__/Modal.test.tsx.snap b/__tests__/components/ui/__snapshots__/Modal.test.tsx.snap new file mode 100644 index 00000000..b27ec9d5 --- /dev/null +++ b/__tests__/components/ui/__snapshots__/Modal.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Modal Component matches snapshot 1`] = ` +
+
+
+ +
+ Test Modal Content +
+
+
+
+`; diff --git a/__tests__/config/navigation.test.ts b/__tests__/config/navigation.test.ts index 337eecec..8ea25f2f 100644 --- a/__tests__/config/navigation.test.ts +++ b/__tests__/config/navigation.test.ts @@ -43,28 +43,29 @@ describe("Navigation Configuration", () => { describe("mainNavLinks", () => { it("should contain main navigation items", () => { - expect(mainNavLinks).toHaveLength(2); - expect(mainNavLinks.map((link) => link.label)).toEqual(["About Us", "Code of Conduct"]); + expect(mainNavLinks).toHaveLength(4); + expect(mainNavLinks.map((link) => link.label)).toEqual(["About Us", "Code of Conduct", "Sponsors", "Travel"]); }); it("should have valid hrefs", () => { mainNavLinks.forEach((link) => { expect(link.href).toBeTruthy(); - expect(link.href).toMatch(/^\//); + expect(link.href).toMatch(/^[/#]/); }); }); - it("should not require year prefix", () => { - mainNavLinks.forEach((link) => { - expect(link.requiresYear).toBe(false); - }); + it("should correctly handle year prefix requirement", () => { + expect(mainNavLinks[0].requiresYear).toBe(false); + expect(mainNavLinks[1].requiresYear).toBe(false); + expect(mainNavLinks[2].requiresYear).toBe(false); + expect(mainNavLinks[3].requiresYear).toBe(true); }); }); describe("yearSpecificNavLinks", () => { it("should contain year-specific navigation items", () => { expect(yearSpecificNavLinks).toHaveLength(3); - expect(yearSpecificNavLinks.map((link) => link.label)).toEqual(["Sponsors", "Speakers", "Talks"]); + expect(yearSpecificNavLinks.map((link) => link.label)).toEqual(["Speakers", "Talks", "Schedule"]); }); it("should require year prefix", () => { @@ -76,8 +77,8 @@ describe("Navigation Configuration", () => { describe("newsDropdownLinks", () => { it("should contain news dropdown items", () => { - expect(newsDropdownLinks).toHaveLength(5); - expect(newsDropdownLinks.map((link) => link.label)).toEqual(["CFP", "Sponsorship", "Diversity", "Job Offers", "Travel"]); + expect(newsDropdownLinks).toHaveLength(4); + expect(newsDropdownLinks.map((link) => link.label)).toEqual(["CFP", "Sponsorship", "Diversity", "Job Offers"]); }); it("should have correct year requirement flags", () => { @@ -89,8 +90,6 @@ describe("Navigation Configuration", () => { expect(newsDropdownLinks[2].requiresYear).toBe(true); // Job Offers expect(newsDropdownLinks[3].requiresYear).toBe(true); - // Travel - expect(newsDropdownLinks[4].requiresYear).toBe(true); }); }); @@ -122,8 +121,9 @@ describe("Navigation Configuration", () => { expect(result).toHaveLength(yearSpecificNavLinks.length); result.forEach((link, index) => { - expect(link.href).toBe(`/${year}${yearSpecificNavLinks[index].href}`); - expect(link.label).toBe(yearSpecificNavLinks[index].label); + const sourceLink = yearSpecificNavLinks.at(index); + expect(link.href).toBe(`/${year}${sourceLink?.href}`); + expect(link.label).toBe(sourceLink?.label); }); }); @@ -131,9 +131,8 @@ describe("Navigation Configuration", () => { const years = ["2023", "2024", "2025", "2026"]; years.forEach((year) => { const result = getNavLinksWithYear(year); - expect(result[0].href).toBe(`/${year}/#sponsors`); - expect(result[1].href).toBe(`/${year}/speakers`); - expect(result[2].href).toBe(`/${year}/talks`); + expect(result[0].href).toBe(`/${year}/speakers`); + expect(result[1].href).toBe(`/${year}/talks`); }); }); }); @@ -149,8 +148,9 @@ describe("Navigation Configuration", () => { expect(result[0].label).toBe("About Us"); expect(result[1].label).toBe("Code of Conduct"); expect(result[2].label).toBe("Sponsors"); - expect(result[3].label).toBe("Speakers"); - expect(result[4].label).toBe("Talks"); + expect(result[3].label).toBe("Travel"); + expect(result[4].label).toBe("Speakers"); + expect(result[5].label).toBe("Talks"); }); }); }); diff --git a/__tests__/fuzz/slugify.fuzz.ts b/__tests__/fuzz/slugify.fuzz.ts new file mode 100644 index 00000000..93a2d643 --- /dev/null +++ b/__tests__/fuzz/slugify.fuzz.ts @@ -0,0 +1,14 @@ +import { expect, describe, it } from "@jest/globals"; +import { slugify } from "../../lib/shared/slugify"; +import fc from "fast-check"; + +describe("slugify fuzz tests", () => { + it("should never throw an error on any string input", () => { + fc.assert( + fc.property(fc.string(), (text: string) => { + const result = slugify(text); + expect(typeof result).toBe("string"); + }) + ); + }); +}); diff --git a/__tests__/snapshots/Menu.test.tsx b/__tests__/snapshots/Menu.test.tsx new file mode 100644 index 00000000..eb920226 --- /dev/null +++ b/__tests__/snapshots/Menu.test.tsx @@ -0,0 +1,22 @@ +import { expect, describe, it, jest } from "@jest/globals"; +import React from "react"; +import { render } from "@testing-library/react"; +import Menu from "../../components/layout/Menu"; +import "@testing-library/jest-dom"; + +// Mock router since Next.js navigation works differently in tests +jest.mock("next/navigation", () => ({ + usePathname: () => "/", + useRouter: () => ({ + push: jest.fn(), + replace: jest.fn(), + prefetch: jest.fn(), + }), +})); + +describe("Menu Component", () => { + it("matches the snapshot", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/__tests__/snapshots/__snapshots__/Menu.test.tsx.snap b/__tests__/snapshots/__snapshots__/Menu.test.tsx.snap new file mode 100644 index 00000000..e80ed1ba --- /dev/null +++ b/__tests__/snapshots/__snapshots__/Menu.test.tsx.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Menu Component matches the snapshot 1`] = ` + +`; diff --git a/any-errors-with-files.txt b/any-errors-with-files.txt deleted file mode 100644 index 5ef2d78f..00000000 --- a/any-errors-with-files.txt +++ /dev/null @@ -1,129 +0,0 @@ -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/.agent/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/.agent/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/.agent/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__mocks__/styleMock.js -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/BackgroundCarousel.test.tsx - 10:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 15:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/BrandSlider.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/Footer8.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/SpeakerCard.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/SpeakersList.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/TalksList.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/elements/VideoPlayer.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/job-offers/JobOffersAccordion.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/components/schedule/ScheduleGrid.perf.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/config/data/job-offers/index.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/config/navigation.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/editions.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/hooks.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/hooks_performance.test.ts - 7:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 7:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 12:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 24:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 50:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 51:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 79:15 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/shared/jsonld.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/shared/markdown.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/shared/slugify.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/shared/talk-filters.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/utils/jsonld.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/utils/markdown.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/utils/slugify.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/lib/utils/talk-filters.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/diversity.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/job-offers.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/speaker-detail.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/speakers-list.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/sponsorship.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/talk-detail.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/talks-list.test.tsx - 30:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/travel.test.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/pages/year-index.test.tsx - 11:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 23:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 27:74 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 35:40 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/schedule_performance.test.ts - 7:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 7:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 12:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 24:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 52:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 53:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/tag-page.test.tsx - 19:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 64:51 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/__tests__/talks-utils.test.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/@modal/(.)speakers/[speaker_id]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/@modal/(.)talks/[talk_id]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/cfp/cfpData.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/cfp/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/diversity/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/schedule/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/speakers/[speaker_id]/opengraph-image.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/speakers/[speaker_id]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/speakers/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/sponsorship/SponsorshipClient.tsx - 64:11 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/tags/[tag]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/talks/[talk_id]/opengraph-image.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/talks/[talk_id]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/talks/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/travel/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/[year]/workshops/[workshop_id]/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/blog-single/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/blog/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/event-single/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/global-error.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/index5/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/kcd/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/app/speakers-single/page.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/elements/BackToTop.tsx - 4:47 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/Breadcrumb.tsx - 1:57 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/Layout.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/TrackFilter.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header1.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header10.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header2.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header3.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header4.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header5.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header6.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header7.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header8.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/layout/header/Header9.tsx - 3:101 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/schedule/ScheduleContainer.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/schedule/ScheduleGrid.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/faq/FaqContent.tsx - 13:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home1/section3.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home2/section5.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home3/section4.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home3/section7.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home4/section4.tsx - 12:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home7/section6.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home8/section3.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/sections/home9/section3.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/components/talks/TalkContent.tsx -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/config/data/job-offers/2023.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/config/data/job-offers/2024.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/config/data/job-offers/2025.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/config/data/job-offers/index.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/config/data/job-offers/types.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/cypress.config.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/cypress/support/e2e.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/data/AboutData.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/hooks/useSpeakers.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/hooks/useTalks.ts -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/lib/shared/analytics.ts - 24:51 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any - 25:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any -/Users/anyulled/IdeaProjects/Eventify_v1.0.0_Unzip-First/1.Eventify_nextjs_template/lib/shared/markdown.tsx diff --git a/app/[year]/cfp/cfpData2026.ts b/app/[year]/cfp/cfpData2026.ts index 6b456752..acb4f63d 100644 --- a/app/[year]/cfp/cfpData2026.ts +++ b/app/[year]/cfp/cfpData2026.ts @@ -26,6 +26,7 @@ export const cfpData2026: CfpTrack[] = [ { name: "François Martin", linkedIn: "https://www.linkedin.com/in/frlan%C3%A7oismartin/", + twitter: "https://x.com/fmartin_", photo: "/assets/img/all-images/cfp/francois_martin.jpg", }, ], diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..fa584fb6 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +export default { extends: ["@commitlint/config-conventional"] }; diff --git a/components/BackgroundCarousel.tsx b/components/BackgroundCarousel.tsx index dc189851..5661470d 100644 --- a/components/BackgroundCarousel.tsx +++ b/components/BackgroundCarousel.tsx @@ -36,7 +36,7 @@ interface BackgroundCarouselProps { export default function BackgroundCarousel({ children, className = "" }: Readonly) { const [prefersReducedMotion, setPrefersReducedMotion] = useState(() => { - if (typeof globalThis.window !== "undefined") { + if (globalThis.window !== undefined) { return globalThis.window.matchMedia("(prefers-reduced-motion: reduce)").matches; } return false; diff --git a/components/elements/Countdown.tsx b/components/elements/Countdown.tsx index ffd323c1..43691517 100644 --- a/components/elements/Countdown.tsx +++ b/components/elements/Countdown.tsx @@ -22,26 +22,39 @@ interface CountdownProps { } export default function Countdown({ style, eventDate }: Readonly) { - const [timeDif, setTimeDif] = useState(() => { - const now = Date.now(); - const targetDate = new Date(eventDate); - return targetDate.getTime() - now; - }); + /* + * Initialize with null to ensure server and initial client render match (empty) + * This avoids hydration errors caused by Date.now() mismatch + */ + const [timeDif, setTimeDif] = useState(null); useEffect(() => { + const targetTime = new Date(eventDate).getTime(); + + const calculateTimeDif = () => { + const now = Date.now(); + const diff = targetTime - now; + return Math.max(0, diff); + }; + + // Set initial time immediately on mount + setTimeDif(calculateTimeDif()); + const interval = setInterval(() => { - setTimeDif((prev) => { - const updatedTime = prev - 1000; - if (updatedTime <= 0) { - clearInterval(interval); - return 0; - } - return updatedTime; - }); + const diff = calculateTimeDif(); + setTimeDif(diff); + if (diff <= 0) { + clearInterval(interval); + } }, 1000); return () => clearInterval(interval); - }, []); + }, [eventDate]); + + // Don't render anything until we have calculated the time on the client + if (timeDif === null) { + return null; + } const timeParts = getPartsOfTimeDuration(timeDif); diff --git a/components/layout/ClientLayout.tsx b/components/layout/ClientLayout.tsx index 1b3ce25b..9d65b93a 100644 --- a/components/layout/ClientLayout.tsx +++ b/components/layout/ClientLayout.tsx @@ -5,7 +5,7 @@ import AOS from "aos"; import { useEffect } from "react"; import AddClassBody from "../elements/AddClassBody"; -export default function ClientLayout({ children }: { children: React.ReactNode }) { +export default function ClientLayout({ children }: Readonly<{ children: React.ReactNode }>) { useEffect(() => { AOS.init(); }, []); diff --git a/components/layout/Layout.tsx b/components/layout/Layout.tsx index 4e7a7f2d..7f289889 100644 --- a/components/layout/Layout.tsx +++ b/components/layout/Layout.tsx @@ -99,8 +99,8 @@ export default function Layout({ headerStyle, footerStyle, breadcrumbTitle: _bre news: newsDropdownLinks, }; - const resolvedHeaderStyle = headerStyle ? headerStyle : 1; - const resolvedFooterStyle = footerStyle ? footerStyle : 1; + const resolvedHeaderStyle = headerStyle || 1; + const resolvedFooterStyle = footerStyle || 1; const headerRenderer = headerRenderers[resolvedHeaderStyle] ?? headerRenderers[1]; const headerElement = headerRenderer({ scroll, isSearch, handleSearch }, defaultNavigation); const footerElement = footerComponents[resolvedFooterStyle] ?? footerComponents[1]; diff --git a/components/layout/Menu.tsx b/components/layout/Menu.tsx index d7030c9e..70f750ee 100644 --- a/components/layout/Menu.tsx +++ b/components/layout/Menu.tsx @@ -6,15 +6,13 @@ export default function Menu() { const pathname = usePathname(); return ( - <> -
    - - Home Default - - - Home Interior - -
- +
    + + Home Default + + + Home Interior + +
); } diff --git a/components/layout/PageHeader.tsx b/components/layout/PageHeader.tsx index a6149b62..8c3a8a17 100644 --- a/components/layout/PageHeader.tsx +++ b/components/layout/PageHeader.tsx @@ -2,13 +2,9 @@ import Link from "next/link"; import Image from "next/image"; interface PageHeaderProps { - /** The main heading text displayed in the header */ title: string; - /** The breadcrumb text shown after "Home >" */ breadcrumbText: string; - /** Background image number (1-13), defaults to 6 */ backgroundImageId?: number; - /** Bootstrap column class for the content container, defaults to "col-lg-5" */ contentColClass?: string; } @@ -18,7 +14,7 @@ interface PageHeaderProps { * @example * */ -export default function PageHeader({ title, breadcrumbText, backgroundImageId = 6, contentColClass = "col-lg-5" }: PageHeaderProps) { +export default function PageHeader({ title, breadcrumbText, backgroundImageId = 6, contentColClass = "col-lg-5" }: Readonly) { return (
Background diff --git a/components/layout/PageSidebar.tsx b/components/layout/PageSidebar.tsx index fc08d97d..62eb5065 100644 --- a/components/layout/PageSidebar.tsx +++ b/components/layout/PageSidebar.tsx @@ -4,7 +4,7 @@ interface PageSidebarProps { year: string; } -export default function PageSidebar({ year }: PageSidebarProps) { +export default function PageSidebar({ year }: Readonly) { return (
diff --git a/components/layout/Popup.tsx b/components/layout/Popup.tsx index 9102ff38..b1211c4d 100644 --- a/components/layout/Popup.tsx +++ b/components/layout/Popup.tsx @@ -4,9 +4,9 @@ import { useEffect } from "react"; export default function Popup() { useEffect(() => { - const popup = document.getElementById("popup") as HTMLElement | null; - const closeBtn = document.getElementById("close-popup") as HTMLElement | null; - const noThanksBtn = document.querySelector(".no-thanks") as HTMLElement | null; + const popup = document.getElementById("popup"); + const closeBtn = document.getElementById("close-popup"); + const noThanksBtn = document.querySelector(".no-thanks"); if (popup) { setTimeout(() => { @@ -41,43 +41,41 @@ export default function Popup() { }, []); return ( - <> -