diff --git a/eslint.config.mjs b/eslint.config.mjs index 9896220..cc44469 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,17 +1,27 @@ -import { dirname } from 'path' -import { fileURLToPath } from 'url' -import { FlatCompat } from '@eslint/eslintrc' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) - -const compat = new FlatCompat({ - baseDirectory: __dirname, -}) +import nextVitals from 'eslint-config-next/core-web-vitals' +import tseslint from 'typescript-eslint' const eslintConfig = [ - ...compat.extends('next/core-web-vitals', 'next/typescript'), + ...nextVitals, { + files: ['**/*.{js,jsx,ts,tsx}'], + plugins: { + react: nextVitals[0].plugins.react, + 'react-hooks': nextVitals[0].plugins['react-hooks'], + }, + rules: { + 'react/no-unescaped-entities': 'warn', + 'react-hooks/immutability': 'warn', + 'react-hooks/refs': 'warn', + 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/static-components': 'warn', + }, + }, + { + files: ['**/*.{ts,tsx,mts,cts}'], + plugins: { + '@typescript-eslint': tseslint.plugin, + }, rules: { '@typescript-eslint/ban-ts-comment': 'warn', '@typescript-eslint/no-empty-object-type': 'warn', diff --git a/src/Header/Component.client.tsx b/src/Header/Component.client.tsx index a4bd658..8c84b77 100644 --- a/src/Header/Component.client.tsx +++ b/src/Header/Component.client.tsx @@ -2,7 +2,7 @@ import { useHeaderTheme } from '@/providers/HeaderTheme' import Link from 'next/link' import { usePathname } from 'next/navigation' -import React, { useEffect, useState } from 'react' +import React, { useEffect } from 'react' import type { Header } from '@/payload-types' @@ -14,20 +14,13 @@ interface HeaderClientProps { } export const HeaderClient: React.FC = ({ data }) => { - /* Storing the value in a useState to avoid hydration errors */ - const [theme, setTheme] = useState(null) const { headerTheme, setHeaderTheme } = useHeaderTheme() const pathname = usePathname() + const theme = headerTheme || null useEffect(() => { setHeaderTheme(null) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pathname]) - - useEffect(() => { - if (headerTheme && headerTheme !== theme) setTheme(headerTheme) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [headerTheme]) + }, [pathname, setHeaderTheme]) return (
diff --git a/src/app/(frontend)/[[...slug]]/page.tsx b/src/app/(frontend)/[[...slug]]/page.tsx index 2f2c3ab..a7e2f68 100644 --- a/src/app/(frontend)/[[...slug]]/page.tsx +++ b/src/app/(frontend)/[[...slug]]/page.tsx @@ -6,6 +6,7 @@ import { notFound } from 'next/navigation' import NextApp from '@/spa/NextApp' import { normalizeFooterData, normalizeHeaderData } from '@/spa/cmsNavigation' +import type { CmsRouteDoc } from '@/spa/cmsRoute' export const dynamic = 'force-dynamic' @@ -36,7 +37,7 @@ function routeTarget(pathname: string): { collection: RouteCollection; slug: str async function findRouteDoc(payload: Awaited>, pathname: string, draft: boolean) { const target = routeTarget(pathname) const result = await payload.find({ - collection: target.collection as any, + collection: target.collection, depth: 1, draft, overrideAccess: draft, @@ -56,7 +57,7 @@ async function findRouteDoc(payload: Awaited>, pat async function findList(payload: Awaited>, collection: RouteCollection, draft: boolean) { const result = await payload.find({ - collection: collection as any, + collection, depth: 1, draft, overrideAccess: draft, @@ -90,11 +91,11 @@ export default async function Page({ params }: Args) { footer={normalizeFooterData(footer)} cmsRoute={{ collection: route.collection, - doc: route.doc as any, + doc: route.doc as unknown as CmsRouteDoc, lists: { - preistraeger: preistraeger as any, - events: events as any, - posts: posts as any, + preistraeger: preistraeger as unknown as CmsRouteDoc[], + events: events as unknown as CmsRouteDoc[], + posts: posts as unknown as CmsRouteDoc[], }, }} /> diff --git a/src/blocks/Form/Component.tsx b/src/blocks/Form/Component.tsx index 428265e..c7f0a85 100644 --- a/src/blocks/Form/Component.tsx +++ b/src/blocks/Form/Component.tsx @@ -131,8 +131,9 @@ export const FormBlock: React.FC< {formFromProps && formFromProps.fields && formFromProps.fields?.map((field, index) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const Field: React.FC = fields?.[field.blockType as keyof typeof fields] + const Field = fields?.[ + field.blockType as keyof typeof fields + ] as React.ComponentType> | undefined if (Field) { return (
diff --git a/src/collections/Events.ts b/src/collections/Events.ts index d3c477b..b5c9ef0 100644 --- a/src/collections/Events.ts +++ b/src/collections/Events.ts @@ -31,14 +31,14 @@ export const Events: CollectionConfig = { url: ({ data, req }) => generatePreviewPath({ slug: data?.spaPath || data?.slug, - collection: 'events' as any, + collection: 'events', req, }), }, preview: (data, { req }) => generatePreviewPath({ slug: ((data?.spaPath as string | undefined) || (data?.slug as string)), - collection: 'events' as any, + collection: 'events', req, }), useAsTitle: 'title', diff --git a/src/collections/Posts/hooks/populateAuthors.ts b/src/collections/Posts/hooks/populateAuthors.ts index 40b4fe5..67e0ead 100644 --- a/src/collections/Posts/hooks/populateAuthors.ts +++ b/src/collections/Posts/hooks/populateAuthors.ts @@ -5,7 +5,7 @@ import { User } from 'src/payload-types' // This means that we need to populate the authors manually here to protect user privacy // GraphQL will not return mutated user data that differs from the underlying schema // So we use an alternative `populatedAuthors` field to populate the user data, hidden from the admin UI -export const populateAuthors: CollectionAfterReadHook = async ({ doc, req, req: { payload } }) => { +export const populateAuthors: CollectionAfterReadHook = async ({ doc, req: { payload } }) => { if (doc?.authors && doc?.authors?.length > 0) { const authorDocs: User[] = [] diff --git a/src/collections/Preistraeger.ts b/src/collections/Preistraeger.ts index a50d385..7e2a56e 100644 --- a/src/collections/Preistraeger.ts +++ b/src/collections/Preistraeger.ts @@ -31,14 +31,14 @@ export const Preistraeger: CollectionConfig = { url: ({ data, req }) => generatePreviewPath({ slug: data?.spaPath || data?.slug, - collection: 'preistraeger' as any, + collection: 'preistraeger', req, }), }, preview: (data, { req }) => generatePreviewPath({ slug: ((data?.spaPath as string | undefined) || (data?.slug as string)), - collection: 'preistraeger' as any, + collection: 'preistraeger', req, }), useAsTitle: 'title', diff --git a/src/components/Card/index.tsx b/src/components/Card/index.tsx index 6c7878e..1ecdbfd 100644 --- a/src/components/Card/index.tsx +++ b/src/components/Card/index.tsx @@ -18,7 +18,10 @@ export const Card: React.FC<{ showCategories?: boolean title?: string }> = (props) => { - const { card, link } = useClickableCard({}) + const { + card: { ref: cardRef }, + link: { ref: linkRef }, + } = useClickableCard({}) const { className, doc, relationTo, showCategories, title: titleFromProps } = props const { slug, categories, meta, title } = doc || {} @@ -35,7 +38,7 @@ export const Card: React.FC<{ 'border border-border rounded-lg overflow-hidden bg-card hover:cursor-pointer', className, )} - ref={card.ref} + ref={cardRef} >
{!metaImage &&
No image
} @@ -67,7 +70,7 @@ export const Card: React.FC<{ {titleToUse && (

- + {titleToUse}

diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index c8c82af..0280aa4 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx' import React from 'react' +import Image from '@/spa/components/ui/UnoptimizedImage' interface Props { className?: string @@ -14,8 +15,7 @@ export const Logo = (props: Props) => { const priority = priorityFromProps || 'low' return ( - /* eslint-disable @next/next/no-img-element */ - Payload Logo { const { setTheme } = useTheme() - const [value, setValue] = useState('') + const [value, setValue] = useState(() => + typeof window === 'undefined' ? 'auto' : window.localStorage.getItem(themeLocalStorageKey) ?? 'auto', + ) const onThemeChange = (themeToSet: Theme & 'auto') => { if (themeToSet === 'auto') { @@ -28,10 +30,6 @@ export const ThemeSelector: React.FC = () => { } } - React.useEffect(() => { - const preference = window.localStorage.getItem(themeLocalStorageKey) - setValue(preference ?? 'auto') - }, []) return ( { + setF(true); + props.onFocus?.(e); + }} + onBlur={(e) => { + setF(false); + props.onBlur?.(e); + }} + style={{ + width: '100%', + height: 44, + padding: '0 14px', + fontFamily: FF, + fontSize: 18, + color: '#fff', + border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`, + background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)', + outline: 'none', + boxSizing: 'border-box', + transition: 'border-color 0.15s, background 0.15s', + ...props.style, + }} + /> + ); +} + const Press: React.FC = () => { const isMobile = useIsMobile(); const cmsEvents = useCmsCollection('events'); @@ -236,38 +268,6 @@ const Press: React.FC = () => { } }; - // DarkInput defined inline as required - function DarkInput(props: React.InputHTMLAttributes) { - const [f, setF] = React.useState(false); - return ( - { - setF(true); - props.onFocus?.(e); - }} - onBlur={(e) => { - setF(false); - props.onBlur?.(e); - }} - style={{ - width: '100%', - height: 44, - padding: '0 14px', - fontFamily: FF, - fontSize: 18, - color: '#fff', - border: `1px solid ${f ? 'rgba(239,191,4,0.8)' : 'rgba(255,255,255,0.12)'}`, - background: f ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.05)', - outline: 'none', - boxSizing: 'border-box', - transition: 'border-color 0.15s, background 0.15s', - ...props.style, - }} - /> - ); - } - return (
@@ -282,7 +282,7 @@ const Press: React.FC = () => { background: NAVY, }} > - Presse BMP { > {/* Left – image cell */}
- {event.title} + {event.title}
{event.status}
diff --git a/src/utilities/useClickableCard.ts b/src/utilities/useClickableCard.ts index 7950ceb..1316b47 100644 --- a/src/utilities/useClickableCard.ts +++ b/src/utilities/useClickableCard.ts @@ -49,8 +49,7 @@ function useClickableCard({ } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [router, card, link, timeDown], + [], ) const handleMouseUp = useCallback( @@ -71,8 +70,7 @@ function useClickableCard({ } } }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [router, card, link, timeDown], + [external, newTab, router, scroll], ) useEffect(() => { @@ -92,8 +90,7 @@ function useClickableCard({ return () => { abortController.abort() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [card, link, router]) + }, [handleMouseDown, handleMouseUp]) return { card: { diff --git a/tests/e2e/admin.e2e.spec.ts b/tests/e2e/admin.e2e.spec.ts index 59cf0d4..a6b9c42 100644 --- a/tests/e2e/admin.e2e.spec.ts +++ b/tests/e2e/admin.e2e.spec.ts @@ -5,7 +5,7 @@ import { seedTestUser, cleanupTestUser, testUser } from '../helpers/seedUser' test.describe('Admin Panel', () => { let page: Page - test.beforeAll(async ({ browser }, testInfo) => { + test.beforeAll(async ({ browser }) => { await seedTestUser() const context = await browser.newContext() diff --git a/tests/e2e/frontend.e2e.spec.ts b/tests/e2e/frontend.e2e.spec.ts index 46495d5..d636b34 100644 --- a/tests/e2e/frontend.e2e.spec.ts +++ b/tests/e2e/frontend.e2e.spec.ts @@ -3,12 +3,12 @@ import { test, expect, Page } from '@playwright/test' test.describe('Frontend', () => { let page: Page - test.beforeAll(async ({ browser }, testInfo) => { + test.beforeAll(async ({ browser }) => { const context = await browser.newContext() page = await context.newPage() }) - test('can load homepage', async ({ page }) => { + test('can load homepage', async () => { await page.goto('http://localhost:3000') await expect(page).toHaveTitle(/Payload Website Template/) const heading = page.locator('h1').first()