feat: setup globals (Footer), sqlite migration, seed update and access rules
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# Problemy i Wymagane Rozbudowy Pluginu ipal-kit
|
||||
|
||||
Zgodnie z zasadą A0b, poniżej znajduje się rejestr realnych braków i problemów napotkanych podczas integracji z `@intecion/ipal-kit`. Zgłoszenia te powinny docelowo trafić do autorów pluginu, a po ich wydaniu należy usunąć lokalne obejścia (workarounds).
|
||||
|
||||
## 1. Brak client-safe entry pointu dla i18n utils
|
||||
|
||||
**Opis problemu:**
|
||||
Brak client-safe entry pointu dla i18n utils (`switchLocalePath`, `getConfiguredLocales`, `buildLocalizedPath`). Główny plik `index.js` ciągnie `panelSmtpAdapter` i inne zależności Node-only (`fs`, `net`, `dns`) do bundla klienckiego przez Webpacka. Użycie ich w komponentach z `"use client"` powoduje natychmiastowy błąd kompilacji.
|
||||
|
||||
**Wymagane rozwiązanie:**
|
||||
Potrzebny jest nowy subpath, np. `@intecion/ipal-kit/client` (lub `@intecion/ipal-kit/i18n`), zawierający wyłącznie czyste, bezpieczne dla przeglądarki utilsy.
|
||||
|
||||
**Obejście na ten moment:**
|
||||
Lokalna kopia logiki wyliczania ścieżek znajduje się w pliku `src/utils/clientSwitchLocalePath.ts` oraz kontekst `LanguageSlugsProvider` do przesyłania danych.
|
||||
**Akcja po naprawie pluginu:** DO USUNIĘCIA `clientSwitchLocalePath.ts` i zamiana na bezpośredni import z nowo dodanego entry pointu `ipal-kit`.
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
description: Zasady dotyczące decyzji architektonicznych i infrastrukturalnych
|
||||
---
|
||||
|
||||
# Zgłaszanie decyzji infrastrukturalnych
|
||||
|
||||
1. **Brak zgody na samowolkę architektoniczną**: Nigdy nie podejmuj decyzji o wyborze bazy danych (np. Postgres vs SQLite), usług hostingowych, czy providerów zewnętrznych (np. Resend vs SMTP) na podstawie własnych domysłów, nawet jeśli oficjalna dokumentacja wybranego stacku (np. Payload CMS, Next.js) mocno sugeruje dane rozwiązanie.
|
||||
2. **Pytaj przed wdrożeniem**: Każdą nową decyzję infrastrukturalną lub wybór narzędzia/usługi trzeciej zawsze zasygnalizuj jako otwarte pytanie do użytkownika ZANIM wygenerujesz kod inicjalizacyjny (np. argumenty `create-payload-app`). Brak zakazu w briefie nie oznacza domyślnej zgody.
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
DATABASE_URL=postgres://127.0.0.1:5432/j_kedzierski
|
||||
DATABASE_URI=file:./dev.db
|
||||
PAYLOAD_SECRET=YOUR_SECRET_HERE
|
||||
NEXT_PUBLIC_WHATSAPP_NUMBER=
|
||||
|
||||
@@ -48,3 +48,4 @@ node_modules/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
dev.db
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
|
||||
|
||||
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -1,43 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
payload:
|
||||
image: node:20-alpine
|
||||
ports:
|
||||
- '3000:3000'
|
||||
volumes:
|
||||
- .:/home/node/app
|
||||
- node_modules:/home/node/app/node_modules
|
||||
working_dir: /home/node/app/
|
||||
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
||||
depends_on:
|
||||
- mongo
|
||||
# - postgres
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# Ensure your DATABASE_URL uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- '27017:27017'
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
- data:/data/db
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
# Uncomment the following to use postgres
|
||||
# postgres:
|
||||
# restart: always
|
||||
# image: postgres:latest
|
||||
# volumes:
|
||||
# - pgdata:/var/lib/postgresql/data
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
# pgdata:
|
||||
node_modules:
|
||||
+10
-1
@@ -20,18 +20,27 @@
|
||||
"dependencies": {
|
||||
"@intecion/ipal-kit": "^1.1.2",
|
||||
"@payloadcms/db-postgres": "latest",
|
||||
"@payloadcms/db-sqlite": "^3.88.0",
|
||||
"@payloadcms/next": "latest",
|
||||
"@payloadcms/plugin-mcp": "latest",
|
||||
"@payloadcms/richtext-lexical": "latest",
|
||||
"@payloadcms/ui": "latest",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"clsx": "^2.1.1",
|
||||
"cross-env": "10.1.0",
|
||||
"dotenv": "16.4.7",
|
||||
"framer-motion": "^13.1.1",
|
||||
"graphql": "^16.8.1",
|
||||
"lucide-react": "^1.37.0",
|
||||
"next": "16.3.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"payload": "latest",
|
||||
"postcss": "^8.5.26",
|
||||
"react": "19.2.6",
|
||||
"react-dom": "19.2.6",
|
||||
"sharp": "0.34.2"
|
||||
"sharp": "0.34.2",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.59.1",
|
||||
|
||||
Generated
+725
-74
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '../src/payload.config'
|
||||
import { toSlug } from '@intecion/ipal-kit'
|
||||
|
||||
const PAGES = [
|
||||
{ pl: 'Strona główna', de: 'Startseite', isHome: true },
|
||||
{ pl: 'Usługi', de: 'Leistungen', isHome: false },
|
||||
{ pl: 'Jak się umówić', de: 'Terminvereinbarung', isHome: false },
|
||||
{ pl: 'O mnie', de: 'Über mich', isHome: false },
|
||||
{ pl: 'FAQ', de: 'FAQ', isHome: false },
|
||||
{ pl: 'Kontakt', de: 'Kontakt', isHome: false },
|
||||
{ pl: 'Stowarzyszenie', de: 'Verein', isHome: false },
|
||||
{ pl: 'Polityka prywatności', de: 'Datenschutzerklärung', isHome: false },
|
||||
{ pl: 'Polityka cookies', de: 'Cookie-Richtlinie', isHome: false },
|
||||
{ pl: 'Impressum', de: 'Impressum', isHome: false },
|
||||
];
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const existingPages = await payload.find({
|
||||
collection: 'pages',
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
if (existingPages.totalDocs === 0) {
|
||||
console.log('Seeding pages...')
|
||||
|
||||
const pageIds: number[] = [];
|
||||
|
||||
for (const page of PAGES) {
|
||||
// @ts-ignore
|
||||
const createdPage = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: page.pl,
|
||||
slug: page.isHome ? 'home' : toSlug(page.pl),
|
||||
layout: [
|
||||
{
|
||||
blockType: 'TextSection',
|
||||
text: page.isHome
|
||||
? 'Witaj na nowej stronie głównej opartej o mechanizm klocków (blocks) i Payload CMS.'
|
||||
: 'Treść w przygotowaniu',
|
||||
}
|
||||
]
|
||||
},
|
||||
locale: 'pl',
|
||||
});
|
||||
|
||||
// Rzutowanie na number: Payload domyślnie może traktować ID jako 'string | number',
|
||||
// jednak przy adapterze Postgres (domyślnie serial) ID to zawsze 'number',
|
||||
// czego z kolei ściśle wymaga pole relacyjne w updateGlobal.
|
||||
pageIds.push(createdPage.id as unknown as number);
|
||||
|
||||
// @ts-ignore
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: createdPage.id,
|
||||
data: {
|
||||
title: page.de,
|
||||
slug: page.isHome ? 'home' : toSlug(page.de),
|
||||
layout: [
|
||||
{
|
||||
blockType: 'TextSection',
|
||||
text: page.isHome
|
||||
? 'Willkommen auf der neuen Startseite basierend auf dem Blöcke-Mechanismus und Payload CMS.'
|
||||
: 'Inhalt in Vorbereitung',
|
||||
}
|
||||
]
|
||||
},
|
||||
locale: 'de',
|
||||
});
|
||||
|
||||
console.log(`- Created ${page.pl} / ${page.de}`);
|
||||
}
|
||||
|
||||
// Dodanie ich do globalnego nagłówka (pominąć 'Stowarzyszenie' i strony prawne jako główne menu wg życzenia)
|
||||
// "Domyślna zawartość na start: Strona główna / Usługi / Jak się umówić / O mnie / FAQ / Kontakt"
|
||||
console.log('Seeding global header...')
|
||||
|
||||
// @ts-ignore
|
||||
await payload.updateGlobal({
|
||||
slug: 'header',
|
||||
data: {
|
||||
navigation: pageIds.slice(0, 6).map(id => ({ page: id })),
|
||||
},
|
||||
});
|
||||
|
||||
console.log('Seeding global company info...')
|
||||
// @ts-ignore
|
||||
await payload.updateGlobal({
|
||||
slug: 'company',
|
||||
data: {
|
||||
address: 'Beratungsstelle b.b.h.\nLohnsteuerhilfeverein e.V.\nJanusz Kedzierski\nPrzykładowa ulica 123\n12345 Przykładowe Miasto',
|
||||
phone: '+49 123 456 789',
|
||||
email: '[email protected]',
|
||||
hours: 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
|
||||
whatsappNumber: '49123456789',
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Seeding global footer...')
|
||||
// Wszystkie pozycje z headera + 3 strony prawne (Indeksy: od 0 do 5 oraz od 7 do 9)
|
||||
const footerNavIndices = [0, 1, 2, 3, 4, 5, 7, 8, 9];
|
||||
const footerNavPages = footerNavIndices.map(index => ({ page: pageIds[index] }));
|
||||
|
||||
// @ts-ignore
|
||||
await payload.updateGlobal({
|
||||
slug: 'footer',
|
||||
data: {
|
||||
navigation: footerNavPages,
|
||||
association: {
|
||||
title: 'Członek b.b.h. Lohnsteuerhilfeverein e.V.',
|
||||
link: 'https://bbh-lohnsteuerhilfe.de',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Seeding site-settings (homepage)...')
|
||||
// @ts-ignore
|
||||
await payload.updateGlobal({
|
||||
slug: 'site-settings',
|
||||
data: {
|
||||
siteName: 'Janusz Kędzierski',
|
||||
titleOrder: 'page-first',
|
||||
titleSeparator: '|',
|
||||
homepage: pageIds[0], // Pierwsza strona to Strona główna (isHome: true)
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Seed completed.')
|
||||
} else {
|
||||
console.log('Pages already seeded.')
|
||||
}
|
||||
}
|
||||
|
||||
run().catch(console.error).finally(() => process.exit(0))
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import { resolveRoute } from '@intecion/ipal-kit'
|
||||
import { getLocalizedSlugs } from '@intecion/ipal-kit'
|
||||
import { i18n } from '../../../../i18n.config'
|
||||
import { SetLanguageSlugs } from '../../../../components/LanguageSlugsProvider'
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ locale: string; slug?: string[] }>
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const { locale, slug } = params;
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
// Rozwiązanie routingu z użyciem mechanizmów ipal-kit
|
||||
const route = await resolveRoute({ payload, locale, segments: slug || [] })
|
||||
|
||||
// Jeśli strona nie istnieje, 404
|
||||
if (!route) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
// Pobranie pełnego rekordu strony we wszystkich językach (locale: 'all'),
|
||||
// by przekazać prawidłowe zlokalizowane slugi do LanguageSwitcher
|
||||
const doc = await payload.findByID({
|
||||
collection: ('collection' in route ? route.collection : 'pages') as 'pages',
|
||||
id: route.doc.id as string,
|
||||
locale: 'all',
|
||||
})
|
||||
|
||||
// getLocalizedSlugs oczekuje pola slug jako obiektu ze slugami we wszystkich językach
|
||||
// Rzutowanie na any jest zabronione przez wytyczne, rzutujemy na Record<string, unknown> (zgodnie z .d.ts)
|
||||
const slugs = getLocalizedSlugs({
|
||||
config: i18n,
|
||||
slugField: doc.slug as unknown as Record<string, unknown>,
|
||||
})
|
||||
|
||||
// Dla obecnego języka potrzebujemy konkretnych danych do renderowania (fallback default locale behavior)
|
||||
const docForRender = await payload.findByID({
|
||||
collection: ('collection' in route ? route.collection : 'pages') as 'pages',
|
||||
id: route.doc.id as string,
|
||||
locale: locale as any, // Payload locale typ nie zawsze przyjmuje string
|
||||
})
|
||||
|
||||
const layout = docForRender.layout || []
|
||||
|
||||
return (
|
||||
<div className="pt-8 pb-24">
|
||||
{/* Rejestracja zlokalizowanych slugów w globalnym kontekście dla LanguageSwitcher */}
|
||||
<SetLanguageSlugs slugs={slugs} />
|
||||
|
||||
{layout.length === 0 && (
|
||||
<div className="container-premium text-center text-muted-foreground mt-20">
|
||||
Strona jest pusta. Skonfiguruj układ (layout) w panelu CMS.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{layout.map((block: any, index: number) => {
|
||||
if (block.blockType === 'TextSection') {
|
||||
return (
|
||||
<div key={index} className="container-premium py-12">
|
||||
<p className="text-lg">{block.text}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div key={index}>Nieznany blok: {block.blockType}</div>
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,64 @@
|
||||
import React from 'react'
|
||||
import './styles.css'
|
||||
import { Inter, Outfit } from 'next/font/google'
|
||||
import { Providers } from '../../components/Providers'
|
||||
import { LanguageSlugsProvider } from '../../components/LanguageSlugsProvider'
|
||||
import { Header } from '../../components/Header'
|
||||
import { Footer } from '../../components/Footer'
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '@payload-config'
|
||||
import '../globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-sans' })
|
||||
const outfit = Outfit({ subsets: ['latin'], variable: '--font-heading' })
|
||||
|
||||
export const metadata = {
|
||||
description: 'A blank template using Payload in a Next.js app.',
|
||||
title: 'Payload Blank Template',
|
||||
title: 'Janusz Kędzierski - Beratungsstelle b.b.h. Lohnsteuerhilfeverein e.V.',
|
||||
description: 'Profesjonalne doradztwo podatkowe.',
|
||||
}
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
// Pobieramy Globals z locale 'all', by od razu otrzymać zlokalizowane dane we wszystkich obsługiwanych językach
|
||||
// Używamy overrideAccess: false, żeby Payload nie zwracał stron, które nie są opublikowane
|
||||
const [headerData, footerData, companyData] = await Promise.all([
|
||||
payload.findGlobal({
|
||||
slug: 'header',
|
||||
locale: 'all',
|
||||
depth: 1,
|
||||
overrideAccess: false,
|
||||
}),
|
||||
payload.findGlobal({
|
||||
slug: 'footer',
|
||||
locale: 'all',
|
||||
depth: 1,
|
||||
overrideAccess: false,
|
||||
}),
|
||||
payload.findGlobal({
|
||||
slug: 'company',
|
||||
locale: 'all',
|
||||
overrideAccess: false,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Filtrujemy tylko poprawnie spopulowane strony (chroni przed brakiem dostępu lub usuniętymi relacjami)
|
||||
const headerNavItems = (headerData.navigation || []).map((item: any) => item.page).filter((page: any) => typeof page === 'object' && page !== null);
|
||||
const footerNavItems = (footerData.navigation || []).map((item: any) => item.page).filter((page: any) => typeof page === 'object' && page !== null);
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="pl" suppressHydrationWarning className={`${inter.variable} ${outfit.variable}`}>
|
||||
<body>
|
||||
<main>{children}</main>
|
||||
<Providers>
|
||||
<LanguageSlugsProvider>
|
||||
<Header navItems={headerNavItems} />
|
||||
<main className="min-h-screen pt-24">{children}</main>
|
||||
<Footer navItems={footerNavItems} companyInfo={companyData} association={footerData.association} />
|
||||
</LanguageSlugsProvider>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { headers as getHeaders } from 'next/headers.js'
|
||||
import Image from 'next/image'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import config from '@/payload.config'
|
||||
import './styles.css'
|
||||
|
||||
export default async function HomePage() {
|
||||
const headers = await getHeaders()
|
||||
const payloadConfig = await config
|
||||
const payload = await getPayload({ config: payloadConfig })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<div className="content">
|
||||
<picture>
|
||||
<source srcSet="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg" />
|
||||
<Image
|
||||
alt="Payload Logo"
|
||||
height={65}
|
||||
src="https://raw.githubusercontent.com/payloadcms/payload/main/packages/ui/src/assets/payload-favicon.svg"
|
||||
width={65}
|
||||
/>
|
||||
</picture>
|
||||
{!user || !('email' in user) ? (
|
||||
<h1>Welcome to your new project.</h1>
|
||||
) : (
|
||||
<h1>Welcome back, {user.email}</h1>
|
||||
)}
|
||||
<div className="links">
|
||||
<a
|
||||
className="admin"
|
||||
href={payloadConfig.routes.admin}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Go to admin panel
|
||||
</a>
|
||||
<a
|
||||
className="docs"
|
||||
href="https://payloadcms.com/docs"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<p>Update this page by editing</p>
|
||||
<a className="codeLink" href={fileURL}>
|
||||
<code>app/(frontend)/page.tsx</code>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +1,9 @@
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { MaskedField as MaskedField_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
|
||||
import { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
/** @type import('payload').ImportMap */
|
||||
export const importMap = {
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@intecion/ipal-kit/client#MaskedField": MaskedField_630f031911beff97691b963f10a20a86,
|
||||
"@intecion/ipal-kit/client#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
|
||||
+67
-54
@@ -1,59 +1,72 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
@theme {
|
||||
--color-background: var(--bg-color);
|
||||
--color-foreground: var(--fg-color);
|
||||
--color-accent: var(--accent-color);
|
||||
--color-accent-hover: var(--accent-hover-color);
|
||||
--color-card: var(--card-color);
|
||||
--color-border: var(--border-color);
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-heading: 'Outfit', sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
:root {
|
||||
/* Jasny motyw */
|
||||
--bg-color: #FAFAFA;
|
||||
--fg-color: #18181B;
|
||||
--card-color: #FFFFFF;
|
||||
--border-color: #E4E4E7;
|
||||
|
||||
/* Ciemniejszy złoty/ochra dla zachowania kontrastu na jasnym tle */
|
||||
--accent-color: #7B5C35;
|
||||
--accent-hover-color: #614828;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Ciemny motyw: głęboki, ciepły granat/antracyt */
|
||||
--bg-color: #0F172A;
|
||||
--fg-color: #F8FAFC;
|
||||
--card-color: #1E293B;
|
||||
--border-color: #334155;
|
||||
|
||||
/* Jasny złoty/ochra dla zachowania kontrastu na ciemnym tle */
|
||||
--accent-color: #C5A880;
|
||||
--accent-hover-color: #D4B995;
|
||||
}
|
||||
|
||||
/* Globalne style dla HTML i Body */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--fg-color);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Płynne przejścia przy zmianie motywu, ale bez migotania */
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-heading);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* Stylizacja elementów interaktywnych dla "premium feel" */
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center justify-center rounded-lg px-6 py-3 font-medium text-white transition-all duration-300;
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--accent-hover-color);
|
||||
@apply shadow-lg transform -translate-y-0.5;
|
||||
}
|
||||
|
||||
/* Utility dla szerokich kontenerów i dużego oddechu (whitespace) */
|
||||
.container-premium {
|
||||
@apply max-w-7xl mx-auto px-6 md:px-12 lg:px-24;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { buildSlugField } from '@intecion/ipal-kit'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
@@ -9,10 +16,22 @@ export const Pages: CollectionConfig = {
|
||||
localized: true,
|
||||
required: true,
|
||||
},
|
||||
buildSlugField(),
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
name: 'layout',
|
||||
type: 'blocks',
|
||||
minRows: 1,
|
||||
blocks: [
|
||||
{
|
||||
slug: 'TextSection',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'text',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { i18n } from '../../i18n.config';
|
||||
import { LocalizedLink } from '../LocalizedLink';
|
||||
import { MapPin, Phone, Mail, Clock, ExternalLink } from "lucide-react";
|
||||
|
||||
export function Footer({
|
||||
navItems = [],
|
||||
companyInfo = {},
|
||||
association = {}
|
||||
}: {
|
||||
navItems?: any[];
|
||||
companyInfo?: any;
|
||||
association?: any;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
// Zabezpieczenie przed błędem, gdy pathname jest puste podczas SSR
|
||||
const currentLocaleCode = (pathname?.split('/').filter(Boolean)[0]) || i18n.defaultLocale;
|
||||
|
||||
// Helper do pobierania tekstów z lokalizowanego obiektu (locale: 'all')
|
||||
const getLocText = (field: any) => field?.[currentLocaleCode] || field?.pl || '';
|
||||
|
||||
const address = getLocText(companyInfo.address);
|
||||
const phone = getLocText(companyInfo.phone);
|
||||
const email = getLocText(companyInfo.email);
|
||||
const hours = getLocText(companyInfo.hours);
|
||||
const whatsapp = companyInfo.whatsappNumber?.[currentLocaleCode] || companyInfo.whatsappNumber?.pl || process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '';
|
||||
|
||||
const assocTitle = getLocText(association.title);
|
||||
const assocLink = association.link;
|
||||
const assocLogo = association.logo;
|
||||
|
||||
return (
|
||||
<footer className="bg-muted py-12 md:py-16 border-t border-border mt-auto">
|
||||
<div className="container-premium grid grid-cols-1 md:grid-cols-3 gap-12 md:gap-8">
|
||||
|
||||
{/* Kolumna 1: Dane kontaktowe */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Link href={`/${currentLocaleCode}`} className="group flex items-center gap-3 w-fit mb-2">
|
||||
<div className="w-10 h-10 rounded bg-[var(--accent-color)] text-white flex items-center justify-center font-heading font-bold text-xl group-hover:bg-[var(--accent-hover-color)] transition-colors shadow-sm">
|
||||
JK
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
||||
Janusz Kędzierski
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
||||
Doradztwo Podatkowe
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<ul className="flex flex-col gap-3 text-sm text-muted-foreground mt-2">
|
||||
{address && (
|
||||
<li className="flex items-start gap-3">
|
||||
<MapPin size={18} className="shrink-0 text-foreground/50 mt-0.5" />
|
||||
<span className="whitespace-pre-line">{address}</span>
|
||||
</li>
|
||||
)}
|
||||
{phone && (
|
||||
<li className="flex items-center gap-3">
|
||||
<Phone size={18} className="shrink-0 text-foreground/50" />
|
||||
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="hover:text-foreground transition-colors">{phone}</a>
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li className="flex items-center gap-3">
|
||||
<Mail size={18} className="shrink-0 text-foreground/50" />
|
||||
<a href={`mailto:${email}`} className="hover:text-foreground transition-colors">{email}</a>
|
||||
</li>
|
||||
)}
|
||||
{hours && (
|
||||
<li className="flex items-start gap-3">
|
||||
<Clock size={18} className="shrink-0 text-foreground/50 mt-0.5" />
|
||||
<span className="whitespace-pre-line">{hours}</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Kolumna 2: Nawigacja / Linki */}
|
||||
<div className="flex flex-col gap-4 md:px-8">
|
||||
<h3 className="font-heading font-semibold text-lg text-foreground">
|
||||
{currentLocaleCode === 'de' ? 'Navigation' : 'Nawigacja'}
|
||||
</h3>
|
||||
<nav className="flex flex-col gap-3">
|
||||
{navItems.map((page, index) => (
|
||||
<LocalizedLink
|
||||
key={index}
|
||||
page={page}
|
||||
currentLocaleCode={currentLocaleCode}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors w-fit"
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Kolumna 3: Stowarzyszenie */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="font-heading font-semibold text-lg text-foreground">
|
||||
{currentLocaleCode === 'de' ? 'Mitgliedschaft' : 'Członkostwo'}
|
||||
</h3>
|
||||
<div className="bg-background border border-border p-5 rounded-xl shadow-sm flex flex-col gap-4 items-start">
|
||||
<span className="text-sm text-muted-foreground leading-relaxed">
|
||||
{assocTitle}
|
||||
</span>
|
||||
{assocLogo && typeof assocLogo === 'object' && assocLogo.url && (
|
||||
<img
|
||||
src={assocLogo.url}
|
||||
alt={assocLogo.alt || 'Logo'}
|
||||
className="h-12 w-auto object-contain"
|
||||
/>
|
||||
)}
|
||||
{assocLink && (
|
||||
<a
|
||||
href={assocLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 text-sm font-semibold text-[var(--accent-color)] hover:text-[var(--accent-hover-color)] transition-colors mt-2 group"
|
||||
>
|
||||
<span>{currentLocaleCode === 'de' ? 'Website besuchen' : 'Odwiedź stronę'}</span>
|
||||
<ExternalLink size={14} className="group-hover:translate-x-0.5 group-hover:-translate-y-0.5 transition-transform" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="container-premium mt-12 pt-8 border-t border-border/50 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-muted-foreground">
|
||||
<p>© {new Date().getFullYear()} Janusz Kędzierski. {currentLocaleCode === 'de' ? 'Alle Rechte vorbehalten.' : 'Wszelkie prawa zastrzeżone.'}</p>
|
||||
<p className="flex items-center gap-1">
|
||||
{currentLocaleCode === 'de' ? 'Erstellt von' : 'Realizacja:'}
|
||||
<a href="#" className="font-medium hover:text-foreground transition-colors">Intecion</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ThemeSwitcher } from "../ThemeSwitcher";
|
||||
import { LanguageSwitcher } from "../LanguageSwitcher";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Menu, X, MessageCircle } from "lucide-react";
|
||||
import { i18n } from '../../i18n.config';
|
||||
import { LocalizedLink } from '../LocalizedLink';
|
||||
|
||||
export function Header({ navItems = [] }: { navItems?: any[] }) {
|
||||
const pathname = usePathname();
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
// Zabezpieczenie przed błędem, gdy pathname jest puste podczas SSR
|
||||
const currentLocaleCode = (pathname?.split('/').filter(Boolean)[0]) || i18n.defaultLocale;
|
||||
const currentLocale = i18n.locales.find(l => l.code === currentLocaleCode) || i18n.locales[0];
|
||||
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-40 bg-background/80 backdrop-blur-md border-b border-border">
|
||||
<div className="container-premium h-24 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Link href={`/${currentLocale.code}`} className="group flex items-center gap-3 z-50 relative">
|
||||
<div className="w-10 h-10 rounded bg-[var(--accent-color)] text-white flex items-center justify-center font-heading font-bold text-xl group-hover:bg-[var(--accent-hover-color)] transition-colors shadow-sm">
|
||||
JK
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
||||
Janusz Kędzierski
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
||||
Doradztwo Podatkowe
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden lg:flex items-center gap-8 absolute left-1/2 -translate-x-1/2">
|
||||
{navItems.map((page, index) => (
|
||||
<LocalizedLink
|
||||
key={index}
|
||||
page={page}
|
||||
currentLocaleCode={currentLocale.code}
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Right Actions & Hamburger */}
|
||||
<div className="flex items-center gap-3 z-50 relative">
|
||||
<div className="hidden sm:flex items-center gap-3 mr-2">
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_WHATSAPP_NUMBER ? `https://wa.me/${process.env.NEXT_PUBLIC_WHATSAPP_NUMBER}` : '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-green-500 hover:bg-green-600 text-white transition-colors"
|
||||
aria-label="WhatsApp"
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
</Link>
|
||||
<div className="w-px h-6 bg-border" />
|
||||
</div>
|
||||
|
||||
<ThemeSwitcher />
|
||||
<LanguageSwitcher />
|
||||
|
||||
<button
|
||||
className="lg:hidden w-10 h-10 flex items-center justify-center text-foreground rounded-full hover:bg-accent transition-colors ml-2"
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="lg:hidden absolute top-24 left-0 right-0 bg-background border-b border-border p-6 shadow-xl flex flex-col gap-6 animate-in fade-in slide-in-from-top-4 duration-200">
|
||||
<nav className="flex flex-col gap-4">
|
||||
{navItems.map((page, index) => (
|
||||
<LocalizedLink
|
||||
key={index}
|
||||
page={page}
|
||||
currentLocaleCode={currentLocale.code}
|
||||
className="text-lg font-medium text-foreground py-2 border-b border-border/50"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center justify-center sm:hidden mt-4 pt-4 border-t border-border/50">
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_WHATSAPP_NUMBER ? `https://wa.me/${process.env.NEXT_PUBLIC_WHATSAPP_NUMBER}` : '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-full transition-colors w-full justify-center"
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
<span className="font-medium">Napisz na WhatsApp</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { ArrowRight, Calculator } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden pt-12 md:pt-20 lg:pt-32 pb-16 lg:pb-32">
|
||||
{/* Subtelne tło dekoracyjne, bez przesadzonych efektów - oddychający layout */}
|
||||
<div className="absolute inset-0 z-0 bg-[radial-gradient(ellipse_at_top_right,var(--accent-color)_0%,transparent_50%)] opacity-[0.03] dark:opacity-[0.05]" />
|
||||
|
||||
<div className="container-premium relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
|
||||
{/* Typografia i treść */}
|
||||
<div className="flex flex-col items-start max-w-2xl">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--accent-color)]/10 text-[var(--accent-color)] text-sm font-medium mb-6">
|
||||
<Calculator className="w-4 h-4" />
|
||||
Lohnsteuerhilfeverein e.V.
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl lg:text-6xl/tight text-foreground mb-6">
|
||||
Twój spokój w świecie{" "}
|
||||
<span className="text-[var(--accent-color)]">niemieckich podatków</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg md:text-xl text-muted-foreground mb-10 leading-relaxed font-light max-w-xl">
|
||||
Profesjonalne doradztwo podatkowe, rozliczenia i pełne wsparcie w kontaktach z Finanzamt.
|
||||
Zadbamy o Twoje finanse, abyś Ty mógł skupić się na tym, co ważne.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||
<Link href="/kontakt" className="btn-primary group">
|
||||
Umów konsultację
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/uslugi"
|
||||
className="inline-flex items-center justify-center rounded-lg px-6 py-3 font-medium text-foreground bg-transparent border border-border hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||
>
|
||||
Poznaj nasze usługi
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Miejsce na realne zdjęcie Janusza - neutralny placeholder */}
|
||||
<div className="relative mx-auto lg:mx-0 w-full max-w-md aspect-[4/5] rounded-2xl overflow-hidden bg-black/5 dark:bg-white/5 border border-border flex items-center justify-center shadow-2xl">
|
||||
<div className="text-center text-muted-foreground opacity-50 flex flex-col items-center">
|
||||
<span className="text-8xl font-heading font-bold mb-4">JK</span>
|
||||
<span className="text-sm font-medium uppercase tracking-widest">Miejsce na zdjęcie</span>
|
||||
<span className="text-xs mt-1">Zostanie dodane przez panel CMS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
type Slugs = Record<string, string>;
|
||||
|
||||
interface LanguageSlugsContextType {
|
||||
slugs: Slugs;
|
||||
setSlugs: (slugs: Slugs) => void;
|
||||
}
|
||||
|
||||
const LanguageSlugsContext = createContext<LanguageSlugsContextType>({
|
||||
slugs: {},
|
||||
setSlugs: () => {},
|
||||
});
|
||||
|
||||
export function LanguageSlugsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [slugs, setSlugs] = useState<Slugs>({});
|
||||
|
||||
return (
|
||||
<LanguageSlugsContext.Provider value={{ slugs, setSlugs }}>
|
||||
{children}
|
||||
</LanguageSlugsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLanguageSlugs() {
|
||||
return useContext(LanguageSlugsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponent pomocniczy do wstrzykiwania slugów danej strony z Server Component (page.tsx)
|
||||
* do globalnego kontekstu (z którego czyta Header/LanguageSwitcher).
|
||||
*/
|
||||
export function SetLanguageSlugs({ slugs }: { slugs: Slugs }) {
|
||||
const { setSlugs } = useLanguageSlugs();
|
||||
|
||||
useEffect(() => {
|
||||
setSlugs(slugs);
|
||||
return () => setSlugs({}); // czyszczenie przy unmount
|
||||
}, [slugs, setSlugs]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { i18n } from "../../i18n.config";
|
||||
import { clientSwitchLocalePath } from "../../utils/clientSwitchLocalePath";
|
||||
import { useLanguageSlugs } from "../LanguageSlugsProvider";
|
||||
import Link from "next/link";
|
||||
import { Globe } from "lucide-react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const pathname = usePathname();
|
||||
const { slugs } = useLanguageSlugs();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Zamykanie po kliknięciu na zewnątrz
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Obecny język z pierwszego segmentu URL, default to 'pl'
|
||||
const currentLocaleCode = pathname.split("/")[1] || i18n.defaultLocale;
|
||||
const currentLocale = i18n.locales.find((l) => l.code === currentLocaleCode) || i18n.locales[0];
|
||||
|
||||
return (
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 p-2 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label="Wybierz język"
|
||||
>
|
||||
<Globe className="w-5 h-5 text-[var(--accent-color)]" />
|
||||
<span className="text-sm font-medium uppercase">{currentLocale.code}</span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute right-0 mt-2 w-32 bg-card rounded-xl shadow-lg border border-border overflow-hidden z-50"
|
||||
>
|
||||
{i18n.locales.map((locale) => {
|
||||
const newPath = clientSwitchLocalePath(pathname, locale.code, slugs);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={locale.code}
|
||||
href={newPath}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`block px-4 py-2 text-sm transition-colors hover:bg-black/5 dark:hover:bg-white/10 ${
|
||||
currentLocaleCode === locale.code
|
||||
? "text-[var(--accent-color)] font-semibold"
|
||||
: "text-foreground"
|
||||
}`}
|
||||
>
|
||||
{locale.label}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Link from 'next/link';
|
||||
import { clientSwitchLocalePath, clientGetLocalizedSlugs } from '../../utils/clientSwitchLocalePath';
|
||||
|
||||
type LocalizedLinkProps = {
|
||||
page: any;
|
||||
currentLocaleCode: string;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function LocalizedLink({ page, currentLocaleCode, className, onClick, children }: LocalizedLinkProps) {
|
||||
// Wykorzystujemy czysty clientGetLocalizedSlugs by pozbyć się pustych slugów / wyłuskać poprawny format
|
||||
const slugs = clientGetLocalizedSlugs(page.slug);
|
||||
|
||||
// Budowanie poprawnego URL dla bieżącego języka
|
||||
const targetHref = clientSwitchLocalePath(`/${currentLocaleCode}`, currentLocaleCode, slugs);
|
||||
|
||||
// Ponieważ używamy locale: 'all', title jest obiektem z kluczami języków (fallbacks w razie braku)
|
||||
const title = page.title?.[currentLocaleCode] || page.title?.pl || 'Brak tytułu';
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={targetHref}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children || title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeSwitcher() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// Omijamy problem z hydratacją – renderujemy przycisk po zamontowaniu na kliencie
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="w-9 h-9" />; // Placeholder zapobiegający CLS
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
className="p-2 rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
|
||||
aria-label="Przełącz motyw"
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Sun className="w-5 h-5 text-[var(--accent-color)]" />
|
||||
) : (
|
||||
<Moon className="w-5 h-5 text-[var(--accent-color)]" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import type { GlobalConfig } from 'payload'
|
||||
|
||||
export const Company: GlobalConfig = {
|
||||
slug: 'company',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'address',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
export const Footer: GlobalConfig = {
|
||||
slug: 'footer',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'navigation',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'page',
|
||||
type: 'relationship',
|
||||
relationTo: 'pages',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'association',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
defaultValue: 'Członek b.b.h. Lohnsteuerhilfeverein e.V.',
|
||||
},
|
||||
{
|
||||
name: 'logo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'link',
|
||||
type: 'text',
|
||||
defaultValue: 'https://bbh-lohnsteuerhilfe.de',
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { GlobalConfig } from 'payload'
|
||||
|
||||
export const Header: GlobalConfig = {
|
||||
slug: 'header',
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'navigation',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'page',
|
||||
type: 'relationship',
|
||||
relationTo: 'pages',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
+504
-59
@@ -64,13 +64,14 @@ export type SupportedTimezones =
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
'payload-mcp-api-keys': PayloadMcpApiKeyAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
media: Media;
|
||||
folders: Folder;
|
||||
tags: Tag;
|
||||
pages: Page;
|
||||
'payload-mcp-api-keys': PayloadMcpApiKey;
|
||||
'payload-kv': PayloadKv;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
@@ -80,24 +81,40 @@ export interface Config {
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
folders: FoldersSelect<false> | FoldersSelect<true>;
|
||||
tags: TagsSelect<false> | TagsSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
'payload-mcp-api-keys': PayloadMcpApiKeysSelect<false> | PayloadMcpApiKeysSelect<true>;
|
||||
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: ('false' | 'none' | 'null') | false | null | 'en' | 'en'[];
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: 'en';
|
||||
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('pl' | 'de') | ('pl' | 'de')[];
|
||||
globals: {
|
||||
company: Company;
|
||||
header: Header;
|
||||
footer: Footer;
|
||||
'site-settings': SiteSetting;
|
||||
'site-integrations': SiteIntegration;
|
||||
'cookie-settings': CookieSetting;
|
||||
notifications: Notification;
|
||||
};
|
||||
globalsSelect: {
|
||||
company: CompanySelect<false> | CompanySelect<true>;
|
||||
header: HeaderSelect<false> | HeaderSelect<true>;
|
||||
footer: FooterSelect<false> | FooterSelect<true>;
|
||||
'site-settings': SiteSettingsSelect<false> | SiteSettingsSelect<true>;
|
||||
'site-integrations': SiteIntegrationsSelect<false> | SiteIntegrationsSelect<true>;
|
||||
'cookie-settings': CookieSettingsSelect<false> | CookieSettingsSelect<true>;
|
||||
notifications: NotificationsSelect<false> | NotificationsSelect<true>;
|
||||
};
|
||||
locale: 'pl' | 'de';
|
||||
widgets: {
|
||||
collections: CollectionsWidget;
|
||||
};
|
||||
user: User;
|
||||
user: User | PayloadMcpApiKey;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
@@ -121,8 +138,30 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
export interface PayloadMcpApiKeyAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -147,10 +186,8 @@ export interface User {
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
id: number;
|
||||
alt: string;
|
||||
_h_folders?: (string | null) | Folder;
|
||||
_h_tags?: (string | Tag)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
@@ -165,32 +202,59 @@ export interface Media {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders".
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Folder {
|
||||
id: string;
|
||||
_h_folders?: (string | null) | Folder;
|
||||
name: string;
|
||||
export interface Page {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Auto-generated from the title when left empty. You can override it.
|
||||
*/
|
||||
slug: string;
|
||||
layout?:
|
||||
| {
|
||||
text?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'TextSection';
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_h_slugPath?: string | null;
|
||||
_h_titlePath?: string | null;
|
||||
}
|
||||
/**
|
||||
* API keys control which collections, resources, tools, and prompts MCP clients can access
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-mcp-api-keys".
|
||||
*/
|
||||
export interface PayloadMcpApiKey {
|
||||
id: number;
|
||||
/**
|
||||
* The user that the API key is associated with.
|
||||
*/
|
||||
user: number | User;
|
||||
/**
|
||||
* A useful label for the API key.
|
||||
*/
|
||||
label?: string | null;
|
||||
/**
|
||||
* The purpose of the API key.
|
||||
*/
|
||||
description?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
enableAPIKey?: boolean | null;
|
||||
apiKey?: string | null;
|
||||
apiKeyIndex?: string | null;
|
||||
collection: 'payload-mcp-api-keys';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tags".
|
||||
* via the `definition` "payload-kv".
|
||||
*/
|
||||
export interface Tag {
|
||||
id: string;
|
||||
_h_tags?: (string | null) | Tag;
|
||||
name: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_h_slugPath?: string | null;
|
||||
_h_titlePath?: string | null;
|
||||
}
|
||||
export interface PayloadKv {
|
||||
id: string;
|
||||
id: number;
|
||||
key: string;
|
||||
data:
|
||||
| {
|
||||
@@ -207,28 +271,33 @@ export interface PayloadKv {
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
id: number;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
value: number | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'folders';
|
||||
value: string | Folder;
|
||||
relationTo: 'pages';
|
||||
value: number | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tags';
|
||||
value: string | Tag;
|
||||
relationTo: 'payload-mcp-api-keys';
|
||||
value: number | PayloadMcpApiKey;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
user:
|
||||
| {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
}
|
||||
| {
|
||||
relationTo: 'payload-mcp-api-keys';
|
||||
value: number | PayloadMcpApiKey;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
@@ -238,10 +307,15 @@ export interface PayloadLockedDocument {
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
id: number;
|
||||
user:
|
||||
| {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
value: number | User;
|
||||
}
|
||||
| {
|
||||
relationTo: 'payload-mcp-api-keys';
|
||||
value: number | PayloadMcpApiKey;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
@@ -261,7 +335,7 @@ export interface PayloadPreference {
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
id: number;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
@@ -295,8 +369,6 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
alt?: T;
|
||||
_h_folders?: T;
|
||||
_h_tags?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
@@ -311,28 +383,43 @@ export interface MediaSelect<T extends boolean = true> {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "folders_select".
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface FoldersSelect<T extends boolean = true> {
|
||||
_h_folders?: T;
|
||||
name?: T;
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
layout?:
|
||||
| T
|
||||
| {
|
||||
TextSection?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_h_slugPath?: T;
|
||||
_h_titlePath?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tags_select".
|
||||
* via the `definition` "payload-mcp-api-keys_select".
|
||||
*/
|
||||
export interface TagsSelect<T extends boolean = true> {
|
||||
_h_tags?: T;
|
||||
name?: T;
|
||||
export interface PayloadMcpApiKeysSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
label?: T;
|
||||
description?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_h_slugPath?: T;
|
||||
_h_titlePath?: T;
|
||||
enableAPIKey?: T;
|
||||
apiKey?: T;
|
||||
apiKeyIndex?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-kv_select".
|
||||
*/
|
||||
export interface PayloadKvSelect<T extends boolean = true> {
|
||||
key?: T;
|
||||
data?: T;
|
||||
@@ -369,6 +456,364 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "company".
|
||||
*/
|
||||
export interface Company {
|
||||
id: number;
|
||||
address?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
hours?: string | null;
|
||||
whatsappNumber?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "header".
|
||||
*/
|
||||
export interface Header {
|
||||
id: number;
|
||||
navigation?:
|
||||
| {
|
||||
page: number | Page;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "footer".
|
||||
*/
|
||||
export interface Footer {
|
||||
id: number;
|
||||
navigation?:
|
||||
| {
|
||||
page: number | Page;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
association?: {
|
||||
title?: string | null;
|
||||
logo?: (number | null) | Media;
|
||||
link?: string | null;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "site-settings".
|
||||
*/
|
||||
export interface SiteSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Used in page titles and Open Graph metadata.
|
||||
*/
|
||||
siteName: string;
|
||||
/**
|
||||
* Which comes first in browser tabs.
|
||||
*/
|
||||
titleOrder?: ('page-first' | 'site-first') | null;
|
||||
/**
|
||||
* Separates the page title from the site name in browser tabs.
|
||||
*/
|
||||
titleSeparator?: ('|' | '–' | '-' | '·' | '/') | null;
|
||||
/**
|
||||
* Primary site logo. Also used for Organization structured data (buildOrganizationJsonLd).
|
||||
*/
|
||||
logo?: (number | null) | Media;
|
||||
/**
|
||||
* Fallback Open Graph image when a page has none.
|
||||
*/
|
||||
defaultShareImage?: (number | null) | Media;
|
||||
/**
|
||||
* Square icon, PNG or SVG, min. 48×48px (Google requires this to show it in search). Rendered via buildIconsMetadata.
|
||||
*/
|
||||
favicon?: (number | null) | Media;
|
||||
/**
|
||||
* Theme applied on a visitor’s first visit.
|
||||
*/
|
||||
defaultTheme: 'light' | 'dark';
|
||||
/**
|
||||
* Show a light/dark switch on the site.
|
||||
*/
|
||||
allowThemeToggle?: boolean | null;
|
||||
/**
|
||||
* Page served at the locale root (e.g. /pl, /en).
|
||||
*/
|
||||
homepage?: (number | null) | Page;
|
||||
/**
|
||||
* Page linked as the privacy policy.
|
||||
*/
|
||||
privacyPolicy?: (number | null) | Page;
|
||||
/**
|
||||
* Page linked from the cookie consent banner.
|
||||
*/
|
||||
cookiePolicy?: (number | null) | Page;
|
||||
/**
|
||||
* Page linked as the terms of service / site regulations.
|
||||
*/
|
||||
termsOfService?: (number | null) | Page;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "site-integrations".
|
||||
*/
|
||||
export interface SiteIntegration {
|
||||
id: number;
|
||||
/**
|
||||
* Google Analytics 4 Measurement ID.
|
||||
*/
|
||||
ga4MeasurementId?: string | null;
|
||||
/**
|
||||
* Google Tag Manager container ID.
|
||||
*/
|
||||
gtmContainerId?: string | null;
|
||||
/**
|
||||
* Public site key rendered in the Turnstile widget.
|
||||
*/
|
||||
turnstileSiteKey?: string | null;
|
||||
/**
|
||||
* Secret key used for server-side verification.
|
||||
*/
|
||||
turnstileSecretKey?: string | null;
|
||||
/**
|
||||
* How outbound email is sent. "Microsoft Graph" is only available when configured by the administrator (Intecion).
|
||||
*/
|
||||
emailTransport?: ('smtp' | 'graph') | null;
|
||||
smtpHost?: string | null;
|
||||
smtpPort?: number | null;
|
||||
/**
|
||||
* SMTP account username.
|
||||
*/
|
||||
smtpUser?: string | null;
|
||||
/**
|
||||
* SMTP account password.
|
||||
*/
|
||||
smtpPassword?: string | null;
|
||||
/**
|
||||
* Default "from" address for outgoing mail.
|
||||
*/
|
||||
smtpFromAddress?: string | null;
|
||||
/**
|
||||
* Default "from" display name.
|
||||
*/
|
||||
smtpFromName?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "cookie-settings".
|
||||
*/
|
||||
export interface CookieSetting {
|
||||
id: number;
|
||||
/**
|
||||
* Main consent message shown in the banner.
|
||||
*/
|
||||
message?: string | null;
|
||||
/**
|
||||
* Heading for the detailed settings panel.
|
||||
*/
|
||||
settingsTitle?: string | null;
|
||||
/**
|
||||
* Button labels.
|
||||
*/
|
||||
buttons?: {
|
||||
acceptAll?: string | null;
|
||||
reject?: string | null;
|
||||
settings?: string | null;
|
||||
save?: string | null;
|
||||
back?: string | null;
|
||||
};
|
||||
/**
|
||||
* Per-category titles and descriptions.
|
||||
*/
|
||||
categories?:
|
||||
| {
|
||||
key: 'necessary' | 'functional' | 'analytics' | 'marketing';
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "notifications".
|
||||
*/
|
||||
export interface Notification {
|
||||
id: number;
|
||||
/**
|
||||
* Messages shown after a form is submitted. Leave a field empty to use the built-in default.
|
||||
*/
|
||||
form?: {
|
||||
success?: string | null;
|
||||
error?: string | null;
|
||||
rateLimited?: string | null;
|
||||
turnstile?: string | null;
|
||||
/**
|
||||
* Shown on a validation error. Use {field} to insert the offending field name.
|
||||
*/
|
||||
validation?: string | null;
|
||||
/**
|
||||
* Shown when the GDPR consent checkbox is left unchecked.
|
||||
*/
|
||||
consent?: string | null;
|
||||
notFound?: string | null;
|
||||
};
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "company_select".
|
||||
*/
|
||||
export interface CompanySelect<T extends boolean = true> {
|
||||
address?: T;
|
||||
phone?: T;
|
||||
email?: T;
|
||||
hours?: T;
|
||||
whatsappNumber?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "header_select".
|
||||
*/
|
||||
export interface HeaderSelect<T extends boolean = true> {
|
||||
navigation?:
|
||||
| T
|
||||
| {
|
||||
page?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "footer_select".
|
||||
*/
|
||||
export interface FooterSelect<T extends boolean = true> {
|
||||
navigation?:
|
||||
| T
|
||||
| {
|
||||
page?: T;
|
||||
id?: T;
|
||||
};
|
||||
association?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
logo?: T;
|
||||
link?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "site-settings_select".
|
||||
*/
|
||||
export interface SiteSettingsSelect<T extends boolean = true> {
|
||||
siteName?: T;
|
||||
titleOrder?: T;
|
||||
titleSeparator?: T;
|
||||
logo?: T;
|
||||
defaultShareImage?: T;
|
||||
favicon?: T;
|
||||
defaultTheme?: T;
|
||||
allowThemeToggle?: T;
|
||||
homepage?: T;
|
||||
privacyPolicy?: T;
|
||||
cookiePolicy?: T;
|
||||
termsOfService?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "site-integrations_select".
|
||||
*/
|
||||
export interface SiteIntegrationsSelect<T extends boolean = true> {
|
||||
ga4MeasurementId?: T;
|
||||
gtmContainerId?: T;
|
||||
turnstileSiteKey?: T;
|
||||
turnstileSecretKey?: T;
|
||||
emailTransport?: T;
|
||||
smtpHost?: T;
|
||||
smtpPort?: T;
|
||||
smtpUser?: T;
|
||||
smtpPassword?: T;
|
||||
smtpFromAddress?: T;
|
||||
smtpFromName?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "cookie-settings_select".
|
||||
*/
|
||||
export interface CookieSettingsSelect<T extends boolean = true> {
|
||||
message?: T;
|
||||
settingsTitle?: T;
|
||||
buttons?:
|
||||
| T
|
||||
| {
|
||||
acceptAll?: T;
|
||||
reject?: T;
|
||||
settings?: T;
|
||||
save?: T;
|
||||
back?: T;
|
||||
};
|
||||
categories?:
|
||||
| T
|
||||
| {
|
||||
key?: T;
|
||||
title?: T;
|
||||
description?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "notifications_select".
|
||||
*/
|
||||
export interface NotificationsSelect<T extends boolean = true> {
|
||||
form?:
|
||||
| T
|
||||
| {
|
||||
success?: T;
|
||||
error?: T;
|
||||
rateLimited?: T;
|
||||
turnstile?: T;
|
||||
validation?: T;
|
||||
consent?: T;
|
||||
notFound?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "collections_widget".
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
||||
import { ipalKit } from '@intecion/ipal-kit'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
@@ -11,6 +11,8 @@ import { Users } from './collections/Users'
|
||||
import { Media } from './collections/Media'
|
||||
import { Pages } from './collections/Pages'
|
||||
import { Company } from './globals/Company'
|
||||
import { Header } from './globals/Header'
|
||||
import { Footer } from './globals/Footer'
|
||||
import { i18n } from './i18n.config'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
@@ -24,15 +26,15 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
collections: [Users, Media, Pages],
|
||||
globals: [Company],
|
||||
globals: [Company, Header, Footer],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
db: postgresAdapter({
|
||||
pool: {
|
||||
connectionString: process.env.DATABASE_URL || '',
|
||||
db: sqliteAdapter({
|
||||
client: {
|
||||
url: process.env.DATABASE_URI || '',
|
||||
},
|
||||
}),
|
||||
sharp,
|
||||
@@ -41,5 +43,5 @@ export default buildConfig({
|
||||
fallback: true,
|
||||
defaultLocale: i18n.defaultLocale,
|
||||
},
|
||||
plugins: [mcpPlugin({}), ipalKit({ i18n })],
|
||||
plugins: [mcpPlugin({}), ipalKit({ i18n, pages: { slug: 'pages' } })],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { i18n } from "../i18n.config";
|
||||
|
||||
type LocalizedSlugs = Record<string, string>;
|
||||
|
||||
export function clientGetLocalizedSlugs(slugField: any): LocalizedSlugs {
|
||||
const result: LocalizedSlugs = {};
|
||||
if (!slugField || typeof slugField !== 'object') {
|
||||
return result;
|
||||
}
|
||||
for (const [locale, value] of Object.entries(slugField)){
|
||||
if (!i18n.locales.some(l => l.code === locale)) {
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
const slug = value.trim();
|
||||
if (!slug) {
|
||||
continue;
|
||||
}
|
||||
result[locale] = slug;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Klient-safe wersja funkcji switchLocalePath z ipal-kit, omijająca problem
|
||||
* importowania modułów Node (np. fs, net) z głównego pliku index.js ipal-kit
|
||||
* w komponentach "use client".
|
||||
*/
|
||||
export function clientSwitchLocalePath(
|
||||
pathname: string,
|
||||
targetLocale: string,
|
||||
slugs?: LocalizedSlugs,
|
||||
prefix?: LocalizedSlugs,
|
||||
homeSlug: string = "home"
|
||||
): string {
|
||||
// Jeśli mamy zlokalizowane slugi ze strony (przekazane z page.tsx -> Context -> LanguageSwitcher)
|
||||
if (slugs && slugs[targetLocale]) {
|
||||
const targetSlug = slugs[targetLocale];
|
||||
|
||||
// Jeśli mamy prefix (dla kolekcji, np. /pl/uslugi/...)
|
||||
if (prefix && prefix[targetLocale]) {
|
||||
return `/${targetLocale}/${prefix[targetLocale]}/${targetSlug}`;
|
||||
}
|
||||
|
||||
if (targetSlug === homeSlug) {
|
||||
return `/${targetLocale}`;
|
||||
}
|
||||
return `/${targetLocale}/${targetSlug}`;
|
||||
}
|
||||
|
||||
// Fallback (zwykły prefix swap), użyteczny dla stron nienależących do CMS lub
|
||||
// gdy slugs nie zostały (jeszcze) przekazane do kontekstu
|
||||
const segments = pathname.split('/').filter(Boolean);
|
||||
if (segments.length > 0 && i18n.locales.some((l: any) => l.code === segments[0])) {
|
||||
segments[0] = targetLocale;
|
||||
} else {
|
||||
segments.unshift(targetLocale);
|
||||
}
|
||||
return `/${segments.join('/')}`;
|
||||
}
|
||||
Reference in New Issue
Block a user