Compare commits

...
7 Commits
42 changed files with 16467 additions and 131 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules
.next
.git
.env
.env.local
dev.db
*.db
.DS_Store
npm-debug.log*
.vscode
.gemini
.agents
+18 -1
View File
@@ -1,2 +1,19 @@
DATABASE_URI=file:./dev.db
# Baza danych
DATABASE_URI=postgres://user:password@localhost:5432/dbname
# Dla lokalnego dewelopmentu:
# DATABASE_URI=file:./dev.db
# Payload
PAYLOAD_SECRET=YOUR_SECRET_HERE
# Opcjonalne: SMTP (do formularzy)
# SMTP_HOST=
# SMTP_PORT=
# SMTP_USER=
# SMTP_PASS=
# SMTP_FROM_ADDRESS=
# SMTP_FROM_NAME=
# Opcjonalne: Turnstile (Cloudflare)
# NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# TURNSTILE_SECRET_KEY=
+2367
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -7,6 +7,7 @@ const __filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(__filename)
const nextConfig: NextConfig = {
output: 'standalone',
async headers() {
const { buildSecurityHeaders } = await import('@intecion/ipal-kit')
return [
@@ -41,6 +42,10 @@ const nextConfig: NextConfig = {
turbopack: {
root: path.resolve(dirname),
},
experimental: {
// @ts-expect-error - Undocumented Next.js feature required for SEO streaming
htmlLimitedBots: /Googlebot|bingbot|yandex|baiduspider/i,
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })
+14
View File
@@ -0,0 +1,14 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', locale: 'pl', limit: 100 })
for (const p of pages.docs) {
if (!p.layout) continue;
const blocks = p.layout.map(b => b.blockType).join(', ')
console.log(`Page: ${p.slug} | Blocks: ${blocks}`)
}
process.exit(0)
}
run().catch(console.error)
+13
View File
@@ -0,0 +1,13 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const homePl = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'pl' })
const homeEn = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'en' })
console.log("PL Layout length:", homePl.docs[0]?.layout?.length)
console.log("EN Layout length:", homeEn.docs[0]?.layout?.length)
process.exit(0)
}
run().catch(console.error)
+15
View File
@@ -0,0 +1,15 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const home = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'pl' })
const blocks = home.docs[0].layout?.map(b => b.blockType)
console.log("HOME PL BLOCKS:", blocks)
const homeEn = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'en' })
console.log("HOME EN BLOCKS:", homeEn.docs[0].layout?.map(b => b.blockType))
process.exit(0)
}
run().catch(console.error)
+10
View File
@@ -0,0 +1,10 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const enPage = await payload.find({ collection: 'pages', locale: 'en', where: { slug: { equals: 'kontakt' } } })
console.log("EN KONTAKT BLOCKS:", enPage.docs[0]?.layout?.map(b => b.blockType))
process.exit(0)
}
run().catch(console.error)
+16
View File
@@ -0,0 +1,16 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
async function run() {
const payload = await getPayload({ config: configPromise })
const header = await payload.findGlobal({ slug: 'header', locale: 'pl' })
const footer = await payload.findGlobal({ slug: 'footer', locale: 'pl' })
const pages = await payload.find({ collection: 'pages', locale: 'pl' })
fs.writeFileSync('content_dump.json', JSON.stringify({ header, footer, pages }, null, 2))
console.log("Dumped to content_dump.json")
process.exit(0)
}
run().catch(console.error)
+13
View File
@@ -0,0 +1,13 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
async function run() {
const payload = await getPayload({ config: configPromise })
const home = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'pl' })
if (home.docs[0]) {
fs.writeFileSync('scripts/home_dump.json', JSON.stringify(home.docs[0].layout, null, 2))
}
process.exit(0)
}
run().catch(console.error)
+15
View File
@@ -0,0 +1,15 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
async function run() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', limit: 100, locale: 'pl' })
const dump = pages.docs.filter(p => !['home'].includes(p.slug)).map(p => ({
slug: p.slug,
layout: p.layout
}))
fs.writeFileSync('scripts/remaining_dump.json', JSON.stringify(dump, null, 2))
process.exit(0)
}
run().catch(console.error)
+18
View File
@@ -0,0 +1,18 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', locale: 'all', limit: 100 })
for (const p of pages.docs) {
if (!p.layout) continue;
for (const b of p.layout) {
if (b.blockType === 'contactSection') {
console.log(`Page ${p.slug} has contactSection in some locale.`)
break;
}
}
}
process.exit(0)
}
run().catch(console.error)
+124
View File
@@ -0,0 +1,124 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
import path from 'path'
// Helper to remove ids deeply from layout blocks so Payload recreates them
function removeIds(obj: any): any {
if (Array.isArray(obj)) return obj.map(removeIds)
if (obj !== null && typeof obj === 'object') {
const newObj: any = {}
for (const key of Object.keys(obj)) {
if (key !== 'id') {
newObj[key] = removeIds(obj[key])
}
}
return newObj
}
return obj
}
async function run() {
const payload = await getPayload({ config: configPromise })
// 1. Ensure we have an uploaded image to use
const mediaDir = path.join(process.cwd(), 'media')
const files = fs.readdirSync(mediaDir).filter(f => f.endsWith('.webp'))
let heroImageId: number | null = null
let aboutImageId: number | null = null
if (files.length > 0) {
try {
const created1 = await payload.create({
collection: 'media',
data: { alt: 'Hero background' },
file: {
data: fs.readFileSync(path.join(mediaDir, files[0])),
mimetype: 'image/webp',
name: 'hero-bg.webp',
size: fs.statSync(path.join(mediaDir, files[0])).size
}
})
heroImageId = created1.id as number
console.log(`Uploaded hero image ID: ${heroImageId}`)
if (files.length > 1) {
const created2 = await payload.create({
collection: 'media',
data: { alt: 'About section image' },
file: {
data: fs.readFileSync(path.join(mediaDir, files[1])),
mimetype: 'image/webp',
name: 'about-img.webp',
size: fs.statSync(path.join(mediaDir, files[1])).size
}
})
aboutImageId = created2.id as number
console.log(`Uploaded about image ID: ${aboutImageId}`)
}
} catch(e) {
console.error("Media upload error", e)
}
}
// 2. Load original layouts from snapshot
const snapshotPath = path.join(process.cwd(), 'scripts', 'content-snapshot.json')
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'))
const homeSnapshot = snapshot.pages.find((p: any) => p.pl.slug === 'home')
if (!homeSnapshot) {
console.error("Home snapshot not found")
process.exit(1)
}
const homeDoc = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'pl' })
if (!homeDoc.docs[0]) {
console.error("Home page not found in DB")
process.exit(1)
}
const homeId = homeDoc.docs[0].id
const updateLayout = async (loc: 'pl' | 'de' | 'en', baseLoc: 'pl' | 'de') => {
let layout = removeIds(homeSnapshot[baseLoc].layout)
// Inject images and EN translations
layout = layout.map((block: any) => {
if (block.blockType === 'hero') {
block.image = heroImageId
if (loc === 'en') {
block.title = 'Your Trusted Accounting Office in Germany'
block.titleAccent = 'We speak English and Polish'
block.subtitle = 'Professional bookkeeping, tax settlements, and business consulting for companies in Germany.'
block.primaryCta = 'Contact Us'
block.secondaryCta = { text: 'Our Services', url: '/uslugi' }
}
} else if (block.blockType === 'about') {
block.image = aboutImageId || heroImageId
}
return block
})
try {
await payload.update({
collection: 'pages',
id: homeId,
locale: loc,
data: {
title: loc === 'en' ? 'Accounting Services in Germany' : homeSnapshot[baseLoc].title,
layout
}
})
console.log(`Successfully fixed home page layout for ${loc}`)
} catch(e) {
console.error(`Failed to fix ${loc} home page`, e)
}
}
await updateLayout('pl', 'pl')
await updateLayout('de', 'de')
await updateLayout('en', 'pl') // Clone EN from PL base
process.exit(0)
}
run().catch(console.error)
+94
View File
@@ -0,0 +1,94 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
import path from 'path'
// Helper to remove ids
function removeIds(obj: any): any {
if (Array.isArray(obj)) return obj.map(removeIds)
if (obj !== null && typeof obj === 'object') {
const newObj: any = {}
for (const key of Object.keys(obj)) {
if (key !== 'id') {
newObj[key] = removeIds(obj[key])
}
}
return newObj
}
return obj
}
async function run() {
const payload = await getPayload({ config: configPromise })
// We already uploaded media 8 and 9 in previous script. Let's find any media.
const allMedia = await payload.find({ collection: 'media', limit: 10 })
const defaultImageId = allMedia.docs[0]?.id || 8
// Load snapshot
const snapshotPath = path.join(process.cwd(), 'scripts', 'content-snapshot.json')
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'))
const enTitles: Record<string, string> = {
'home': 'Home',
'uslugi': 'Services',
'jak-sie-umowic': 'How to make an appointment',
'o-nas': 'About Us',
'faq': 'FAQ',
'kontakt': 'Contact',
'stowarzyszenie': 'Association',
'polityka-prywatnosci': 'Privacy Policy',
'polityka-cookies': 'Cookie Policy',
'impressum': 'Imprint'
}
for (const pageSnapshot of snapshot.pages) {
const slug = pageSnapshot.pl.slug
if (slug === 'home') continue // Already fixed
const dbPage = await payload.find({ collection: 'pages', where: { slug: { equals: slug } }, locale: 'pl' })
if (!dbPage.docs[0]) continue
const pageId = dbPage.docs[0].id
const updateLayout = async (loc: 'pl' | 'de' | 'en', baseLoc: 'pl' | 'de') => {
let layout = removeIds(pageSnapshot[baseLoc]?.layout || pageSnapshot.pl.layout)
// Inject images
layout = layout.map((block: any) => {
if (block.blockType === 'hero' || block.blockType === 'pageHeader' || block.blockType === 'about' || block.blockType === 'contactSection') {
block.image = defaultImageId
}
if (block.blockType === 'servicesList') {
if (block.items) {
block.items = block.items.map((i: any) => ({ ...i, image: defaultImageId }))
}
}
return block
})
try {
await payload.update({
collection: 'pages',
id: pageId,
locale: loc,
data: {
title: loc === 'en' ? (enTitles[slug] || pageSnapshot[baseLoc].title) : (pageSnapshot[baseLoc]?.title || pageSnapshot.pl.title),
layout
}
})
console.log(`Successfully fixed ${slug} layout for ${loc}`)
} catch(e) {
console.error(`Failed to fix ${slug} for ${loc}`)
}
}
await updateLayout('pl', 'pl')
if (pageSnapshot.de) {
await updateLayout('de', 'de')
}
await updateLayout('en', 'pl') // Clone EN from PL base
}
process.exit(0)
}
run().catch(console.error)
+43
View File
@@ -0,0 +1,43 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const home = await payload.find({ collection: 'pages', where: { slug: { equals: 'home' } }, locale: 'pl' })
if (home.docs[0] && home.docs[0].layout) {
const enLayout = home.docs[0].layout.map(block => {
// Remove id so Payload generates a new one
const { id, blockName, ...rest } = block as any
if (rest.blockType === 'hero') {
return {
...rest,
title: 'Your Trusted Accounting Office in Germany',
titleAccent: 'We speak English and Polish',
subtitle: 'Professional bookkeeping, tax settlements, and business consulting for companies in Germany.',
primaryCta: 'Contact Us',
secondaryCta: { text: 'Our Services', url: '/uslugi' }
}
}
return rest
})
try {
await payload.update({
collection: 'pages',
id: home.docs[0].id,
locale: 'en',
data: {
title: 'Accounting Services in Germany',
layout: enLayout
}
})
console.log("Successfully fixed EN home page")
} catch (e) {
console.error("Error fixing EN home page", e)
}
}
process.exit(0)
}
run().catch(console.error)
+69
View File
@@ -0,0 +1,69 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const plPages = await payload.find({ collection: 'pages', locale: 'pl', where: { slug: { equals: 'kontakt' } } })
const plPage = plPages.docs[0]
if (!plPage) return;
const layout = plPage.layout || []
const enLayout = layout.map(block => {
const b = JSON.parse(JSON.stringify(block))
// strip populated
for (const key of Object.keys(b)) {
if (b[key] && typeof b[key] === 'object' && b[key].id !== undefined) {
b[key] = b[key].id
}
}
if (b.blockType === 'pageHeader') {
b.title = "Contact"
b.subtitle = "Do you have questions about tax settlement in Germany? Contact us — we will be happy to help."
}
if (b.blockType === 'contactSection') {
b.address = "TO BE CONFIRMED BY CLIENT"
b.hours = "Mon-Fri: 9:00 - 17:00\nBy appointment"
}
return b
})
await payload.update({
collection: 'pages',
id: plPage.id,
locale: 'en',
data: { layout: enLayout }
})
console.log("Forced EN layout for kontakt")
const deLayout = layout.map(block => {
const b = JSON.parse(JSON.stringify(block))
// strip populated
for (const key of Object.keys(b)) {
if (b[key] && typeof b[key] === 'object' && b[key].id !== undefined) {
b[key] = b[key].id
}
}
if (b.blockType === 'pageHeader') {
b.title = "Kontakt"
b.subtitle = "Haben Sie Fragen zur Steuererklärung in Deutschland? Kontaktieren Sie uns — wir helfen Ihnen gerne weiter."
}
if (b.blockType === 'contactSection') {
b.address = "TO BE CONFIRMED BY CLIENT"
b.hours = "Mo-Fr: 9:00 - 17:00\nNach Vereinbarung"
}
return b
})
await payload.update({
collection: 'pages',
id: plPage.id,
locale: 'de',
data: { layout: deLayout }
})
console.log("Forced DE layout for kontakt")
process.exit(0)
}
run().catch(console.error)
+309
View File
@@ -0,0 +1,309 @@
[
{
"id": "6aac6aa8a53b4a5a4f6600ed",
"title": "Odbierz zwrot podatku z Niemiec",
"titleAccent": "Rozliczymy Cię po polsku",
"subtitle": "Ponad 20 lat doświadczenia w rozliczeniach dla Polaków pracujących w Niemczech. Szybko, rzetelnie i w pełni po polsku.",
"primaryCta": "Napisz na WhatsApp",
"image": {
"id": 16,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:35:53.986Z",
"createdAt": "2026-09-17T22:35:53.986Z",
"url": "/api/media/file/d0c63f69-9a73-48ba-a03b-15dafd55b5cf-3.webp",
"thumbnailURL": null,
"filename": "d0c63f69-9a73-48ba-a03b-15dafd55b5cf-3.webp",
"mimeType": "image/webp",
"filesize": 69160,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"imageSecondary": {
"id": 1,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:31:47.560Z",
"createdAt": "2026-09-17T22:31:47.559Z",
"url": "/api/media/file/38670a24-644b-42db-ac36-fd77f78352ba-2.webp",
"thumbnailURL": null,
"filename": "38670a24-644b-42db-ac36-fd77f78352ba-2.webp",
"mimeType": "image/webp",
"filesize": 74328,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "hero",
"secondaryCta": {
"text": "Zobacz jak to działa",
"url": "#jak-to-dziala"
}
},
{
"id": "6aac6aa8a53b4a5a4f6600f2",
"blockName": null,
"items": [
{
"id": "6aac6aa8a53b4a5a4f6600ee",
"text": "Działamy w ramach b.b.h.",
"icon": "shield"
},
{
"id": "6aac6aa8a53b4a5a4f6600ef",
"text": "Doradztwo 100% po polsku",
"icon": "message"
},
{
"id": "6aac6aa8a53b4a5a4f6600f0",
"text": "Wsparcie w Urzędzie Skarbowym",
"icon": "check"
},
{
"id": "6aac6aa8a53b4a5a4f6600f1",
"text": "Wieloletnie doświadczenie",
"icon": "star"
}
],
"blockType": "trustBar"
},
{
"id": "6aac6aa8a53b4a5a4f6600f5",
"eyebrow": null,
"title": "Rozliczenia bez bariery językowej",
"description": "Niemieckie prawo podatkowe bywa skomplikowane. Przeprowadzimy Cię przez cały proces w Twoim ojczystym języku, dbając o maksymalny zwrot i zero stresu.",
"image": {
"id": 16,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:35:53.986Z",
"createdAt": "2026-09-17T22:35:53.986Z",
"url": "/api/media/file/d0c63f69-9a73-48ba-a03b-15dafd55b5cf-3.webp",
"thumbnailURL": null,
"filename": "d0c63f69-9a73-48ba-a03b-15dafd55b5cf-3.webp",
"mimeType": "image/webp",
"filesize": 69160,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"features": [
{
"id": "6aac6aa8a53b4a5a4f6600f3",
"text": "Bez barier językowych"
},
{
"id": "6aac6aa8a53b4a5a4f6600f4",
"text": "Indywidualne podejście"
}
],
"blockType": "uniqueValue"
},
{
"id": "6aac6aa8a53b4a5a4f6600f9",
"title": null,
"subtitle": null,
"blockName": null,
"items": [
{
"id": "6aac6aa8a53b4a5a4f6600f6",
"stat": "20+",
"label": "Lat doświadczenia",
"description": "w prawie podatkowym i rozliczeniach dla Polaków w Niemczech"
},
{
"id": "6aac6aa8a53b4a5a4f6600f7",
"stat": "100%",
"label": "Obsługa po polsku",
"description": "bezpośredni kontakt i wyjaśnienie każdej kwestii podatkowej"
},
{
"id": "6aac6aa8a53b4a5a4f6600f8",
"stat": "§4 Nr. 11",
"label": "Stowarzyszenie b.b.h.",
"description": "oficjalna podstawa prawna doradztwa zgodnie ze StBerG"
}
],
"blockType": "stats"
},
{
"id": "6aac6aa8a53b4a5a4f6600fd",
"eyebrow": "Usługi",
"title": "Czym się zajmujemy",
"blockName": null,
"services": [
{
"id": "6aac6aa8a53b4a5a4f6600fa",
"title": "Deklaracje podatkowe",
"description": "Kompleksowe sporządzenie i złożenie zeznania rocznego (Einkommensteuererklärung).",
"linkText": "Zobacz więcej",
"linkUrl": null
},
{
"id": "6aac6aa8a53b4a5a4f6600fb",
"title": "Kontrola decyzji",
"description": "Szczegółowa weryfikacja decyzji podatkowej (Steuerbescheid) z urzędu skarbowego.",
"linkText": "Zobacz więcej",
"linkUrl": null
},
{
"id": "6aac6aa8a53b4a5a4f6600fc",
"title": "Ulgi i dopłaty",
"description": "Pomoc w uzyskaniu Kindergeld, Riester-Rente oraz odliczaniu kosztów dojazdów i podwójnego gospodarstwa.",
"linkText": "Zobacz więcej",
"linkUrl": null
}
],
"blockType": "servicesList"
},
{
"id": "6aac6aa8a53b4a5a4f660101",
"eyebrow": null,
"title": "Jak to działa",
"subtitle": "Prosty i przejrzysty proces rozliczenia w 3 krokach — bez zbędnych formalności.",
"ctaText": "Zobacz cały proces",
"ctaUrl": "/pl/jak-sie-umowic",
"image": {
"id": 15,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:35:53.848Z",
"createdAt": "2026-09-17T22:35:53.848Z",
"url": "/api/media/file/ac4e2098-0b88-4f6f-98ed-72461a36784c-3.webp",
"thumbnailURL": null,
"filename": "ac4e2098-0b88-4f6f-98ed-72461a36784c-3.webp",
"mimeType": "image/webp",
"filesize": 55450,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"steps": [
{
"id": "6aac6aa8a53b4a5a4f6600fe",
"title": "Napisz na WhatsApp",
"description": "Wyślij krótką wiadomość lub zadzwoń, by rozpocząć proces."
},
{
"id": "6aac6aa8a53b4a5a4f6600ff",
"title": "Przekaż dokumenty",
"description": "Prześlij nam wymagane dokumenty bezpiecznie online lub dostarcz osobiście."
},
{
"id": "6aac6aa8a53b4a5a4f660100",
"title": "Odbierz zwrot",
"description": "Zajmiemy się resztą i poinformujemy Cię o spodziewanym zwrocie z urzędu skarbowego."
}
],
"blockType": "process"
},
{
"id": "6aac6aa8a53b4a5a4f660102",
"eyebrow": "Zaufanie",
"title": "Zaufanie i bezpieczeństwo",
"description": "Działamy w ramach b.b.h. Lohnsteuerhilfeverein e.V. (Beratungsstelle). Oferujemy profesjonalną pomoc w sprawach podatku dochodowego w ramach uprawnień wynikających z §4 Nr. 11 StBerG.",
"logo": null,
"linkText": "Poznaj stowarzyszenie",
"linkUrl": "/pl/o-nas",
"blockName": null,
"trustPoints": [],
"blockType": "affiliation"
},
{
"id": "6aac6aa8a53b4a5a4f660106",
"title": "Opinie klientów",
"blockName": null,
"testimonials": [
{
"id": "6aac6aa8a53b4a5a4f660103",
"quote": "Pan Janusz pomógł mi odzyskać podatek za 3 ostatnie lata. Wszystko jasno wytłumaczone po polsku.",
"author": "Piotr, Monachium",
"source": null
},
{
"id": "6aac6aa8a53b4a5a4f660104",
"quote": "Szybki kontakt przez WhatsApp i pełne wsparcie przy odwołaniu od decyzji Finanzamt. Polecam!",
"author": "Anna, Berlin",
"source": null
},
{
"id": "6aac6aa8a53b4a5a4f660105",
"quote": "Ogromna wiedza o ulgach dla pracowników budowlanych. Zwrot był znacznie wyższy niż się spodziewałem.",
"author": "Krzysztof, Dortmund",
"source": null
}
],
"blockType": "testimonials"
},
{
"id": "6aac6aa8a53b4a5a4f66010f",
"title": "Częste pytania",
"ctaText": null,
"ctaUrl": null,
"blockName": null,
"questions": [
{
"id": "6aac6aa8a53b4a5a4f660107",
"category": null,
"question": "Czy muszę przyjeżdżać osobiście do biura?",
"answer": "Nie, wszystkie formalności możemy załatwić w 100% zdalnie. Dokumenty przesyłasz online przez bezpieczne kanały, a konsultacje prowadzimy telefonicznie lub przez WhatsApp."
},
{
"id": "6aac6aa8a53b4a5a4f660108",
"category": null,
"question": "Do kiedy muszę złożyć deklarację podatkową w Niemczech?",
"answer": "Standardowy termin to 31 sierpnia kolejnego roku, jednak korzystając z usług doradcy podatkowego, zyskujesz dodatkowy czas – nawet do końca lutego lub maja następnego roku (w zależności od ustaleń na dany rok)."
},
{
"id": "6aac6aa8a53b4a5a4f660109",
"category": null,
"question": "Jak długo czeka się na zwrot podatku z urzędu skarbowego?",
"answer": "Czas oczekiwania na decyzję i przelew zależy od urzędu skarbowego – najczęściej wynosi on od 3 do 8 tygodni po złożeniu kompletnej deklaracji."
},
{
"id": "6aac6aa8a53b4a5a4f66010a",
"category": null,
"question": "Czy załatwiacie również zasiłek Kindergeld?",
"answer": "Tak, pomagamy w formalnościach związanych z pozyskaniem zasiłku rodzinnego Kindergeld dla pracowników zatrudnionych na terenie Niemiec."
},
{
"id": "6aac6aa8a53b4a5a4f66010b",
"category": null,
"question": "Jakie dokumenty są potrzebne do rozliczenia?",
"answer": "Do podstawowego rozliczenia potrzebujesz Lohnsteuerbescheinigung (karty podatkowej od pracodawcy), zaświadczenia o dochodach w Polsce (EU/EWR) oraz dokumentów potwierdzających koszty (np. dojazdy, podwójne gospodarstwo)."
},
{
"id": "6aac6aa8a53b4a5a4f66010c",
"category": null,
"question": "Czy muszę przychodzić osobiście do biura?",
"answer": "Nie, cały proces możemy przeprowadzić w 100% online. Dokumenty możesz przesłać nam bezpiecznie przez WhatsApp, e-mail lub pocztą tradycyjną."
},
{
"id": "6aac6aa8a53b4a5a4f66010d",
"category": null,
"question": "Ile kosztuje rozliczenie podatku z Niemiec?",
"answer": "Koszt rozliczenia ustalamy indywidualnie, bazując na stopniu skomplikowania sprawy i rodzaju deklarowanych dochodów. Zawsze informujemy o cenie przed rozpoczęciem prac."
},
{
"id": "6aac6aa8a53b4a5a4f66010e",
"category": null,
"question": "Czy pomagacie przy wnioskach o Kindergeld?",
"answer": "Tak, pomagamy w formalnościach związanych z pozyskaniem zasiłku rodzinnego Kindergeld dla pracowników zatrudnionych na terenie Niemiec."
}
],
"blockType": "faqTeaser"
},
{
"id": "6aac6aa8a53b4a5a4f660110",
"eyebrow": "Kontakt",
"title": "Gotowy na wyższy zwrot podatku?",
"subtitle": "Napisz do nas już teraz. Odpowiemy najszybciej jak to możliwe.",
"buttonText": "Rozpocznij na WhatsApp",
"blockName": null,
"blockType": "finalCta"
}
]
+10
View File
@@ -0,0 +1,10 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const forms = await payload.find({ collection: 'forms' })
console.log("FORMS:", forms.docs.map(f => ({ id: f.id, title: f.title })))
process.exit(0)
}
run().catch(console.error)
+473
View File
@@ -0,0 +1,473 @@
[
{
"slug": "uslugi",
"layout": [
{
"id": "6aac6b4ada77e55afbdd567a",
"title": "Kompleksowe rozliczenia podatkowe dla Polaków w Niemczech",
"subtitle": "Profesjonalne sporządzanie deklaracji, weryfikacja decyzji podatkowych i bieżące wsparcie w kontaktach z niemieckim urzędem skarbowym.",
"image": {
"id": 10,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:35:52.987Z",
"createdAt": "2026-09-17T22:35:52.987Z",
"url": "/api/media/file/38670a24-644b-42db-ac36-fd77f78352ba-3.webp",
"thumbnailURL": null,
"filename": "38670a24-644b-42db-ac36-fd77f78352ba-3.webp",
"mimeType": "image/webp",
"filesize": 74328,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "pageHeader"
},
{
"id": "6aac6b4ada77e55afbdd5682",
"title": "Czym się zajmujemy",
"subtitle": "Zapewniamy rzetelne i kompleksowe wsparcie na każdym etapie rozliczenia podatku dochodowego w Niemczech.",
"blockName": null,
"services": [
{
"id": "6aac6b4ada77e55afbdd567b",
"title": "Sporządzenie deklaracji podatkowej (Einkommensteuererklärung)",
"anchor": "sporzadzenie-deklaracji-podatkowej-einkommensteuererklarung",
"description": "Przygotowujemy kompletne zeznanie podatkowe, uwzględniając wszystkie przysługujące Ci ulgi, koszty uzyskania przychodu oraz odliczenia. Dbamy o maksymalizację Twojego zwrotu i pełną zgodność z niemieckim prawem.",
"icon": "fileText"
},
{
"id": "6aac6b4ada77e55afbdd567c",
"title": "Wstępne obliczenie przewidywanego zwrotu",
"anchor": "wstepne-obliczenie-przewidywanego-zwrotu",
"description": "Przed wysłaniem dokumentów do urzędu wyliczamy dokładną kwotę spodziewanego zwrotu lub ewentualnej dopłaty. Dzięki temu wiesz dokładnie, czego się spodziewać jeszcze przed wydaniem decyzji przez urzędu skarbowego.",
"icon": "calculator"
},
{
"id": "6aac6b4ada77e55afbdd567d",
"title": "Kontrola decyzji podatkowej (Steuerbescheid)",
"anchor": "kontrola-decyzji-podatkowej-steuerbescheid",
"description": "Szczegółowo weryfikujemy otrzymaną decyzję z urzędu skarbowego pod kątem ewentualnych błędów urzędników. Sprawdzamy, czy urząd uwzględnił wszystkie zgłoszone koszty i ulgi.",
"icon": "shieldCheck"
},
{
"id": "6aac6b4ada77e55afbdd567e",
"title": "Składanie odwołań (Einspruch)",
"anchor": "skladanie-odwolan-einspruch",
"description": "W przypadku niekorzystnej lub błędnej decyzji urzędu sporządzamy oficjalne odwołanie i reprezentujemy Cię w postępowaniu wyjaśniającym, skutecznie dochodząc Twoich praw.",
"icon": "scale"
},
{
"id": "6aac6b4ada77e55afbdd567f",
"title": "Całoroczne doradztwo podatkowe",
"anchor": "caloroczne-doradztwo-podatkowe",
"description": "Pozostajemy do Twojej dyspozycji przez cały rok podatkowy. Odpowiadamy na pytania, pomagamy w bieżącej korespondencji z urzędem i doradzamy przy zmianie sytuacji życiowej.",
"icon": "helpCircle"
},
{
"id": "6aac6b4ada77e55afbdd5680",
"title": "Doradztwo przy wyborze klasy podatkowej (Steuerklasse)",
"anchor": "doradztwo-przy-wyborze-klasy-podatkowej-steuerklasse",
"description": "Doradzamy optymalny dobór klas podatkowych dla małżeństw i osób pracujących w Niemczech. Pomagamy wybrać wariant (np. III/V lub IV/IV), który przyniesie największe korzyści finansowe.",
"icon": "users"
},
{
"id": "6aac6b4ada77e55afbdd5681",
"title": "Ulgi, dopłaty i odliczenia (Kindergeld, dojazdy, podwójne gospodarstwo)",
"anchor": "ulgi-doplaty-i-odliczenia-kindergeld-dojazdy-podwojne-gospodarstwo",
"description": "Pomagamy w uzyskaniu Kindergeld, Riester-Rente oraz prawidłowym odliczeniu kosztów podwójnego gospodarstwa domowego (doppelter Haushalt) i dojazdów do pracy (Pendlerpauschale).",
"icon": "coins"
}
],
"blockType": "servicesDetailed"
},
{
"id": "6aac6b4ada77e55afbdd568c",
"title": "Dla kogo — zakres uprawnień doradczych wg §4 Nr. 11 StBerG",
"subtitle": "Jako punkt doradczy b.b.h. Lohnsteuerhilfeverein e.V. działamy ściśle w ramach ustawowych uprawnień dla stowarzyszeń pomocy podatkowej.",
"eligibleTitle": "Kto może skorzystać z naszej pomocy",
"limitationsTitle": "Dopuszczalne przychody dodatkowe (najem, kapitał, inne)",
"limitationsText": "Osoby uzyskujące dodatkowe przychody (np. z najmu i dzierżawy, kapitałów pieniężnych, innych źródeł) — doradztwo jest możliwe wyłącznie wtedy, gdy łączne przychody z tych źródeł nie przekraczają 18 000 € (dla osób samotnych) lub 36 000 € (dla małżeństw rozliczających się wspólnie) w roku podatkowym.",
"excludedTitle": "Kogo NIE obejmują nasze uprawnienia (granica ustawowa)",
"legalNotice": "Pomoc podatkowa jest świadczona wyłącznie na rzecz członków stowarzyszenia b.b.h. Lohnsteuerhilfeverein e.V. zgodnie z § 4 Nr. 11 Steuerberatungsgesetz (StBerG).",
"blockName": null,
"eligibleItems": [
{
"id": "6aac6b4ada77e55afbdd5683",
"text": "Pracownicy zatrudnieni na umowę o pracę (Arbeitnehmer) oraz pracownicy tymczasowi"
},
{
"id": "6aac6b4ada77e55afbdd5684",
"text": "Urzędnicy państwowi (Beamte)"
},
{
"id": "6aac6b4ada77e55afbdd5685",
"text": "Emeryci i renciści (Rentner und Pensionäre)"
},
{
"id": "6aac6b4ada77e55afbdd5686",
"text": "Osoby bezrobotne oraz pobierające zasiłki socjalne"
},
{
"id": "6aac6b4ada77e55afbdd5687",
"text": "Odbiorcy świadczeń rodzinnych (Kindergeld, Elterngeld) oraz alimentów"
}
],
"excludedItems": [
{
"id": "6aac6b4ada77e55afbdd5688",
"text": "Osoby prowadzące jednoosobową działalność gospodarczą (Gewerbe)"
},
{
"id": "6aac6b4ada77e55afbdd5689",
"text": "Przedsiębiorcy, spółki oraz osoby samozatrudnione (Selbständige)"
},
{
"id": "6aac6b4ada77e55afbdd568a",
"text": "Osoby uzyskujące przychody z działalności rolniczej i leśnej (Land- und Forstwirtschaft)"
},
{
"id": "6aac6b4ada77e55afbdd568b",
"text": "Osoby z przychodami z najmu lub kapitału przekraczającymi limity 18 000 € / 36 000 €"
}
],
"blockType": "eligibility"
},
{
"id": "6aac6b4ada77e55afbdd568d",
"eyebrow": "Kontakt",
"title": "Masz pytania dotyczące Twojego rozliczenia?",
"subtitle": "Skontaktuj się bezpośrednio przez WhatsApp. Przeanalizujemy Twoją sytuację i pomożemy uzyskać należny zwrot.",
"buttonText": "Napisz na WhatsApp",
"image": {
"id": 10,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:35:52.987Z",
"createdAt": "2026-09-17T22:35:52.987Z",
"url": "/api/media/file/38670a24-644b-42db-ac36-fd77f78352ba-3.webp",
"thumbnailURL": null,
"filename": "38670a24-644b-42db-ac36-fd77f78352ba-3.webp",
"mimeType": "image/webp",
"filesize": 74328,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "finalCta"
}
]
},
{
"slug": "jak-sie-umowic",
"layout": [
{
"id": "6aac6afbef4df35aa15f8d03",
"title": "Jak się umówić",
"subtitle": "Prosty, przejrzysty i w 100% zdalny proces rozliczenia podatkowego. Wszystko załatwisz bez wychodzenia z domu.",
"image": {
"id": 9,
"alt": "About section image",
"updatedAt": "2026-09-17T22:33:12.549Z",
"createdAt": "2026-09-17T22:33:12.549Z",
"url": "/api/media/file/about-img.webp",
"thumbnailURL": null,
"filename": "about-img.webp",
"mimeType": "image/webp",
"filesize": 72208,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "pageHeader"
},
{
"id": "6aac6afbef4df35aa15f8d07",
"eyebrow": null,
"title": "Rozliczenie w 3 prostych krokach",
"subtitle": "Zapewniamy pełne wsparcie i jasną komunikację na każdym etapie.",
"ctaText": null,
"ctaUrl": null,
"image": {
"id": 8,
"alt": "Hero background",
"updatedAt": "2026-09-17T22:33:12.415Z",
"createdAt": "2026-09-17T22:33:12.414Z",
"url": "/api/media/file/hero-bg.webp",
"thumbnailURL": null,
"filename": "hero-bg.webp",
"mimeType": "image/webp",
"filesize": 74328,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"steps": [
{
"id": "6aac6afbef4df35aa15f8d04",
"title": "Napisz na WhatsApp",
"description": "Skontaktuj się z nami bezpośrednio przez WhatsApp lub formularz poniżej, aby rozpocząć i ustalić szczegóły Twojej sytuacji podatkowej."
},
{
"id": "6aac6afbef4df35aa15f8d05",
"title": "Przekaż dokumenty",
"description": "Prześlij zaświadczenia o dochodach (Lohnsteuerbescheinigung, EU/EWR), podatkowy numer identyfikacyjny (IdNr) lub PESEL oraz ewentualne potwierdzenia kosztów."
},
{
"id": "6aac6afbef4df35aa15f8d06",
"title": "Odbierz gotowe rozliczenie do podpisu",
"description": "Przygotowujemy kompletną deklarację, wyliczamy przewidywany zwrot z urzędu skarbowego i przesyłamy gotowy dokument do Twojej akceptacji i podpisu."
}
],
"blockType": "process"
},
{
"id": "6aac6afbef4df35aa15f8d08",
"address": "DO POTWIERDZENIA PRZEZ KLIENTA",
"phone": "+49 123 456 789",
"email": "[email protected]",
"hours": "Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu",
"blockName": null,
"blockType": "contactInfo"
},
{
"id": "6aac6afbef4df35aa15f8d09",
"eyebrow": null,
"title": "Chcesz szybko odzyskać podatek?",
"subtitle": "Nie zwlekaj z rozliczeniem. Napisz teraz, a zajmiemy się formalnościami od początku do końca.",
"buttonText": "Napisz na WhatsApp",
"image": {
"id": 9,
"alt": "About section image",
"updatedAt": "2026-09-17T22:33:12.549Z",
"createdAt": "2026-09-17T22:33:12.549Z",
"url": "/api/media/file/about-img.webp",
"thumbnailURL": null,
"filename": "about-img.webp",
"mimeType": "image/webp",
"filesize": 72208,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "finalCta"
}
]
},
{
"slug": "o-nas",
"layout": [
{
"id": "6aac6afbef4df35aa15f8ce2",
"title": "O nas",
"subtitle": "Ponad 20 lat rzetelnego wsparcia podatkowego dla Polaków pracujących w Niemczech.",
"image": {
"id": 7,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:31:48.622Z",
"createdAt": "2026-09-17T22:31:48.622Z",
"url": "/api/media/file/d0c63f69-9a73-48ba-a03b-15dafd55b5cf-2.webp",
"thumbnailURL": null,
"filename": "d0c63f69-9a73-48ba-a03b-15dafd55b5cf-2.webp",
"mimeType": "image/webp",
"filesize": 69160,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "pageHeader"
},
{
"id": "6aac6afbef4df35aa15f8ce9",
"title": "Janusz Kędzierski",
"image": {
"id": 6,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:31:48.453Z",
"createdAt": "2026-09-17T22:31:48.453Z",
"url": "/api/media/file/ac4e2098-0b88-4f6f-98ed-72461a36784c-2.webp",
"thumbnailURL": null,
"filename": "ac4e2098-0b88-4f6f-98ed-72461a36784c-2.webp",
"mimeType": "image/webp",
"filesize": 55450,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"paragraphs": [
{
"id": "6aac6afbef4df35aa15f8ce3",
"text": "Od ponad dwóch dekad pomagam polskim pracownikom i specjalistom w Niemczech w bezpiecznym, bezstresowym i w pełni legalnym rozliczaniu podatków dochodowych przed urzędem skarbowym."
},
{
"id": "6aac6afbef4df35aa15f8ce4",
"text": "Moim celem jest zdjęcie z Ciebie ciężaru skomplikowanych niemieckich przepisów skarbowych. Cały proces prowadzimy w języku polskim — od wstępnej analizy dokumentów, przez precyzyjne wyliczenia ulg i kosztów uzyskania przychodu, aż po weryfikację końcowej decyzji podatkowej."
}
],
"credentials": [
{
"id": "6aac6afbef4df35aa15f8ce5",
"text": "Ponad 20 lat praktyki w niemieckim prawie podatkowym"
},
{
"id": "6aac6afbef4df35aa15f8ce6",
"text": "Beratungsstellenleiter w b.b.h. Lohnsteuerhilfeverein e.V."
},
{
"id": "6aac6afbef4df35aa15f8ce7",
"text": "Pełna obsługa w języku polskim i niemieckim"
},
{
"id": "6aac6afbef4df35aa15f8ce8",
"text": "Maksymalizacja zwrotu podatku i zero stresu formalnego"
}
],
"blockType": "bio"
},
{
"id": "6aac6afbef4df35aa15f8ceb",
"title": "Nasz zespół",
"subtitle": "Poznaj ekspertów, którzy z zaangażowaniem dbają o Twoje finanse. Każdy z nas wnosi wieloletnie doświadczenie, by zagwarantować Ci spokój i bezpieczeństwo.",
"blockName": null,
"members": [
{
"id": "6aac6afbef4df35aa15f8cea",
"name": "Janusz Kędzierski",
"role": "Właściciel / Beratungsstellenleiter",
"description": "Wieloletni praktyk prawa podatkowego ze specjalizacją w doradztwie dla polskich pracowników w Niemczech.",
"photo": null,
"bookingUrl": "/kontakt"
}
],
"blockType": "team"
},
{
"id": "6aac6afbef4df35aa15f8cec",
"eyebrow": null,
"title": "Gotowy na rozliczenie podatku?",
"subtitle": "Skontaktuj się z nami już dziś. Przeanalizujemy Twoją sytuację i pomożemy uzyskać maksymalny zwrot z niemieckiego urzędu skarbowego.",
"buttonText": "Napisz na WhatsApp",
"image": {
"id": 8,
"alt": "Hero background",
"updatedAt": "2026-09-17T22:33:12.415Z",
"createdAt": "2026-09-17T22:33:12.414Z",
"url": "/api/media/file/hero-bg.webp",
"thumbnailURL": null,
"filename": "hero-bg.webp",
"mimeType": "image/webp",
"filesize": 74328,
"width": 1536,
"height": 1024,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "finalCta"
}
]
},
{
"slug": "faq",
"layout": [
{
"id": "6aac6afbef4df35aa15f8cdf",
"text": "Treść w przygotowaniu",
"blockName": null,
"blockType": "TextSection"
}
]
},
{
"slug": "kontakt",
"layout": [
{
"id": "6aac6b4ada77e55afbdd56a0",
"title": "Kontakt",
"subtitle": "Masz pytania dotyczące rozliczenia podatkowego w Niemczech? Skontaktuj się z nami — chętnie pomożemy.",
"image": {
"id": 5,
"alt": "Obraz",
"updatedAt": "2026-09-17T22:31:48.145Z",
"createdAt": "2026-09-17T22:31:48.145Z",
"url": "/api/media/file/9a2983cc-482c-4ff7-9f1d-31679e4dfc56-2.webp",
"thumbnailURL": null,
"filename": "9a2983cc-482c-4ff7-9f1d-31679e4dfc56-2.webp",
"mimeType": "image/webp",
"filesize": 70318,
"width": 1672,
"height": 941,
"focalX": 50,
"focalY": 50
},
"blockName": null,
"blockType": "pageHeader"
},
{
"id": "6aac6b4ada77e55afbdd56a1",
"address": "DO POTWIERDZENIA PRZEZ KLIENTA",
"phone": "+49 123 456 789",
"email": "[email protected]",
"hours": "Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu",
"blockName": null,
"blockType": "contactInfo"
}
]
},
{
"slug": "stowarzyszenie",
"layout": [
{
"id": "6aac6afbef4df35aa15f8cd6",
"text": "Treść w przygotowaniu",
"blockName": null,
"blockType": "TextSection"
}
]
},
{
"slug": "polityka-prywatnosci",
"layout": [
{
"id": "6aac6afbef4df35aa15f8cd3",
"text": "Treść w przygotowaniu",
"blockName": null,
"blockType": "TextSection"
}
]
},
{
"slug": "polityka-cookies",
"layout": [
{
"id": "6aac6afbef4df35aa15f8cd0",
"text": "Treść w przygotowaniu",
"blockName": null,
"blockType": "TextSection"
}
]
},
{
"slug": "impressum",
"layout": [
{
"id": "6aac6afbef4df35aa15f8ccd",
"text": "Treść w przygotowaniu",
"blockName": null,
"blockType": "TextSection"
}
]
}
]
+77
View File
@@ -0,0 +1,77 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
import fs from 'fs'
import path from 'path'
async function run() {
const payload = await getPayload({ config: configPromise })
const snapshotPath = path.join(process.cwd(), 'scripts', 'content-snapshot.json')
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'))
const kontaktSnap = snapshot.pages.find((p: any) => p.pl.slug === 'kontakt')
if (!kontaktSnap) {
console.error("No kontakt in snapshot")
return
}
// Find actual page ID
const dbPages = await payload.find({ collection: 'pages', where: { slug: { equals: 'kontakt' } } })
const dbPage = dbPages.docs[0]
if (!dbPage) {
console.error("No kontakt in DB")
return
}
for (const loc of ['pl', 'de', 'en']) {
let layout = kontaktSnap[loc]?.layout
if (!layout && loc === 'en') {
// fallback to PL for layout structure
layout = JSON.parse(JSON.stringify(kontaktSnap.pl.layout))
}
if (!layout) continue;
const newLayout = layout.map((block: any) => {
const b = { ...block }
delete b.id // VERY IMPORTANT to avoid SQLite unique constraint errors or table mismatches
// If we find contactInfo, UPGRADE it to contactSection and attach form 1!
if (b.blockType === 'contactInfo' || b.blockType === 'contactSection') {
b.blockType = 'contactSection'
b.form = 1 // We created form 1 earlier
}
// Fix EN translations if this is EN
if (loc === 'en') {
if (b.blockType === 'pageHeader') {
b.title = "Contact"
b.subtitle = "Do you have questions about tax settlement in Germany? Contact us — we will be happy to help."
}
if (b.blockType === 'contactSection') {
b.address = "TO BE CONFIRMED BY CLIENT"
b.hours = "Mon-Fri: 9:00 - 17:00\nBy appointment"
}
}
return b
})
const title = loc === 'en' ? 'Contact' : (loc === 'de' ? 'Kontakt' : 'Kontakt')
await payload.update({
collection: 'pages',
id: dbPage.id,
locale: loc as any,
data: {
title,
layout: newLayout
}
})
console.log(`Restored kontakt in ${loc}`)
}
process.exit(0)
}
run().catch(console.error)
+16
View File
@@ -0,0 +1,16 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const docs = await payload.find({
collection: 'pages',
where: { slug: { equals: 'uslugi' } }, // wait, slug in DE might be different! Let's query by ID 2
locale: 'de'
})
const doc = docs.docs[0]
const cta = doc.layout?.find(b => b.blockType === 'finalCta') as any
console.log("USLUGI DE CTA IMAGE:", typeof cta?.image, cta?.image?.id || cta?.image)
process.exit(0)
}
run().catch(console.error)
+75
View File
@@ -0,0 +1,75 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
// 1. UPDATE GLOBALS
await payload.updateGlobal({
slug: 'footer',
locale: 'en',
data: {
copyright: '© 2024 Accounting Kędzierski. All rights reserved.',
association: {
title: 'Member of b.b.h. Lohnsteuerhilfeverein e.V.'
},
creator: {
label: 'Created by'
}
}
})
console.log("Footer updated")
// 2. UPDATE PAGES (Titles and basic fields)
const translations: Record<string, any> = {
'home': { title: 'Home' },
'uslugi': { title: 'Services' },
'jak-sie-umowic': { title: 'How to make an appointment' },
'o-nas': { title: 'About Us' },
'faq': { title: 'FAQ' },
'kontakt': { title: 'Contact' },
'stowarzyszenie': { title: 'Association' },
'polityka-prywatnosci': { title: 'Privacy Policy' },
'polityka-cookies': { title: 'Cookie Policy' },
'impressum': { title: 'Imprint' }
}
const pages = await payload.find({ collection: 'pages', limit: 100, locale: 'pl' })
for (const page of pages.docs) {
const slug = page.slug || ''
const enData = translations[slug] || { title: page.title }
// For Home, let's keep the hero translations if it's there
if (slug === 'home') {
enData.layout = page.layout?.map(block => {
if (block.blockType === 'hero') {
return {
...block,
title: 'Your Trusted Accounting Office in Germany',
titleAccent: 'We speak English and Polish',
subtitle: 'Professional bookkeeping, tax settlements, and business consulting for companies in Germany.',
primaryCta: 'Contact Us',
secondaryCta: { text: 'Our Services', url: '/uslugi' }
}
}
return block
})
}
try {
await payload.update({
collection: 'pages',
id: page.id,
locale: 'en',
data: enData
})
console.log(`Page updated: ${slug}`)
} catch (e) {
console.error(`Error updating page ${slug}`, e)
}
}
console.log("Done")
process.exit(0)
}
run().catch(console.error)
+271
View File
@@ -0,0 +1,271 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
const translations: Record<string, Record<string, any>> = {
'uslugi': {
en: {
pageHeader: { title: "Comprehensive tax settlements in Germany", subtitle: "Professional tax return preparation, tax assessment verification, and ongoing support." },
servicesDetailed: {
title: "What we do",
subtitle: "We provide reliable and comprehensive support at every stage of income tax settlement in Germany.",
services: [
{ title: "Tax return preparation (Einkommensteuererklärung)", description: "We prepare a complete tax return, taking into account all applicable allowances, deductible expenses, and deductions. We ensure maximum refund and full compliance with German law." },
{ title: "Preliminary calculation of expected refund", description: "Before sending the documents to the tax office, we calculate the exact amount of the expected refund or potential surcharge. You know exactly what to expect." },
{ title: "Tax assessment verification (Steuerbescheid)", description: "We thoroughly verify the received decision from the tax office for any errors by officials. We check if the office has included all reported costs and allowances." },
{ title: "Filing appeals (Einspruch)", description: "In the event of an unfavorable or incorrect decision from the office, we prepare an official appeal and represent you in the clarification proceedings." },
{ title: "Year-round tax consulting", description: "We remain at your disposal throughout the tax year. We answer questions, help in ongoing correspondence with the office and advise when life situations change." },
{ title: "Advice on choosing a tax class (Steuerklasse)", description: "We advise on the optimal selection of tax classes for marriages and people working in Germany. We help to choose the variant (e.g. III/V or IV/IV) that will bring the greatest financial benefits." },
{ title: "Allowances, subsidies and deductions", description: "We help in obtaining Kindergeld, Riester-Rente and correct deduction of the costs of maintaining a double household (doppelter Haushalt) and commuting to work (Pendlerpauschale)." }
]
},
eligibility: {
title: "For whom — scope of advisory powers under §4 No. 11 StBerG",
subtitle: "As an advisory point of b.b.h. Lohnsteuerhilfeverein e.V. we operate strictly within statutory limits for tax assistance associations.",
eligibleTitle: "Who can benefit from our help",
limitationsTitle: "Permissible additional income (rental, capital, other)",
limitationsText: "Persons earning additional income — advice is possible only if the total income from these sources does not exceed €18,000 (singles) or €36,000 (married couples).",
excludedTitle: "Who is NOT covered by our services (statutory limit)",
legalNotice: "Tax assistance is provided only to members of the b.b.h. Lohnsteuerhilfeverein e.V. according to § 4 No. 11 Steuerberatungsgesetz (StBerG).",
eligibleItems: [
{ text: "Employees (Arbeitnehmer) and temporary workers" },
{ text: "Civil servants (Beamte)" },
{ text: "Pensioners and retirees (Rentner und Pensionäre)" },
{ text: "Unemployed persons and social benefit recipients" },
{ text: "Recipients of family benefits (Kindergeld, Elterngeld) and alimony" }
],
excludedItems: [
{ text: "Sole traders (Gewerbe)" },
{ text: "Entrepreneurs, partnerships and self-employed (Selbständige)" },
{ text: "Persons generating income from agriculture and forestry (Land- und Forstwirtschaft)" },
{ text: "Persons with rental or capital income exceeding limits" }
]
}
},
de: {
pageHeader: { title: "Umfassende Steuererklärungen in Deutschland", subtitle: "Professionelle Erstellung, Prüfung von Steuerbescheiden und laufende Unterstützung." },
servicesDetailed: {
title: "Was wir tun",
subtitle: "Wir bieten zuverlässige und umfassende Unterstützung in jeder Phase der Einkommensteuererklärung in Deutschland.",
services: [
{ title: "Erstellung der Einkommensteuererklärung", description: "Wir erstellen eine vollständige Steuererklärung unter Berücksichtigung aller zustehenden Freibeträge, Werbungskosten und Abzüge. Wir sorgen für maximale Rückerstattung und volle Übereinstimmung mit deutschem Recht." },
{ title: "Vorläufige Berechnung der erwarteten Rückerstattung", description: "Bevor wir die Dokumente an das Finanzamt senden, berechnen wir die genaue Höhe der erwarteten Rückerstattung oder einer möglichen Nachzahlung." },
{ title: "Prüfung des Steuerbescheids", description: "Wir prüfen den erhaltenen Bescheid des Finanzamtes eingehend auf Fehler der Sachbearbeiter. Wir prüfen, ob das Amt alle gemeldeten Kosten und Freibeträge berücksichtigt hat." },
{ title: "Einspruchsverfahren", description: "Im Falle eines unvorteilhaften oder falschen Bescheids des Amtes erstellen wir einen offiziellen Einspruch und vertreten Sie im Klärungsverfahren." },
{ title: "Ganzjährige Steuerberatung", description: "Wir stehen Ihnen während des gesamten Steuerjahres zur Verfügung. Wir beantworten Fragen, helfen bei der laufenden Korrespondenz mit dem Amt und beraten bei Veränderungen der Lebenssituation." },
{ title: "Beratung bei der Wahl der Steuerklasse", description: "Wir beraten bei der optimalen Wahl der Steuerklassen für Verheiratete und Arbeitnehmer in Deutschland. Wir helfen bei der Wahl der Variante (z. B. III/V oder IV/IV)." },
{ title: "Freibeträge, Zuschüsse und Abzüge", description: "Wir helfen bei der Erlangung von Kindergeld, Riester-Rente und dem korrekten Abzug der Kosten für eine doppelte Haushaltsführung und Fahrtkosten zur Arbeit (Pendlerpauschale)." }
]
},
eligibility: {
title: "Für wen — Beratungsumfang nach §4 Nr. 11 StBerG",
subtitle: "Als Beratungsstelle des b.b.h. Lohnsteuerhilfeverein e.V. arbeiten wir streng im Rahmen der gesetzlichen Befugnisse.",
eligibleTitle: "Wer unsere Hilfe in Anspruch nehmen kann",
limitationsTitle: "Zulässige Zusatzeinnahmen (Vermietung, Kapital, Sonstige)",
limitationsText: "Personen mit Zusatzeinkünften — eine Beratung ist nur möglich, wenn die Gesamteinnahmen aus diesen Quellen 18.000 € (Alleinstehende) oder 36.000 € (Verheiratete) nicht überschreiten.",
excludedTitle: "Wer NICHT unter unsere Leistungen fällt",
legalNotice: "Die Hilfeleistung in Steuersachen erfolgt ausschließlich für Mitglieder des b.b.h. Lohnsteuerhilfeverein e.V. gem. § 4 Nr. 11 StBerG.",
eligibleItems: [
{ text: "Arbeitnehmer und Leiharbeiter" },
{ text: "Beamte" },
{ text: "Rentner und Pensionäre" },
{ text: "Arbeitslose und Bezieher von Sozialleistungen" },
{ text: "Bezieher von Familienleistungen (Kindergeld, Elterngeld) und Unterhalt" }
],
excludedItems: [
{ text: "Gewerbetreibende" },
{ text: "Unternehmer, Gesellschaften und Selbständige" },
{ text: "Personen mit Einkünften aus Land- und Forstwirtschaft" },
{ text: "Personen mit Miet- oder Kapitaleinkünften, die die Grenzen überschreiten" }
]
}
}
},
'jak-sie-umowic': {
en: {
pageHeader: { title: "How to make an appointment", subtitle: "Simple, transparent, and 100% remote tax settlement process. You can handle everything without leaving your home." },
process: {
title: "Settlement in 3 simple steps",
subtitle: "We provide full support and clear communication at every stage.",
steps: [
{ title: "Message us on WhatsApp", description: "Contact us directly via WhatsApp or the form below to start and discuss the details of your tax situation." },
{ title: "Hand over documents", description: "Send your income certificates (Lohnsteuerbescheinigung, EU/EWR), tax identification number (IdNr), and potential expense confirmations." },
{ title: "Receive ready settlement for signature", description: "We prepare the complete declaration, calculate the expected refund, and send the ready document for your approval and signature." }
]
},
contactInfo: { hours: "Mon-Fri: 9:00 - 17:00\nBy appointment" }
},
de: {
pageHeader: { title: "Termin vereinbaren", subtitle: "Einfacher, transparenter und 100% digitaler Prozess der Steuererklärung. Alles von zu Hause aus." },
process: {
title: "Steuererklärung in 3 einfachen Schritten",
subtitle: "Wir bieten volle Unterstützung und klare Kommunikation in jeder Phase.",
steps: [
{ title: "Auf WhatsApp schreiben", description: "Kontaktieren Sie uns direkt über WhatsApp oder das Formular unten, um zu beginnen und die Details zu besprechen." },
{ title: "Dokumente übergeben", description: "Senden Sie uns Ihre Lohnsteuerbescheinigungen (ggf. EU/EWR), Steueridentifikationsnummer (IdNr) und mögliche Kostenbelege." },
{ title: "Erklärung zur Unterschrift erhalten", description: "Wir erstellen die komplette Erklärung, berechnen die erwartete Erstattung und senden das fertige Dokument zur Freigabe und Unterschrift." }
]
},
contactInfo: { hours: "Mo-Fr: 9:00 - 17:00\nNach Vereinbarung" }
}
},
'o-nas': {
en: {
pageHeader: { title: "About Us", subtitle: "Over 20 years of reliable tax support for Poles working in Germany." },
bio: {
paragraphs: [
{ text: "For over two decades, I have been helping Polish workers and specialists in Germany to safely, stress-free, and fully legally settle income taxes before the tax office." },
{ text: "My goal is to take the burden of complicated German tax regulations off your shoulders. We conduct the entire process in Polish — from initial document analysis, through precise calculations of allowances, to verification of the final tax decision." }
],
credentials: [
{ text: "Over 20 years of practice in German tax law" },
{ text: "Beratungsstellenleiter at b.b.h. Lohnsteuerhilfeverein e.V." },
{ text: "Full service in Polish and German" },
{ text: "Maximizing tax refunds and zero formal stress" }
]
},
team: {
title: "Our Team",
subtitle: "Meet the experts who are committed to your finances. Each of us brings years of experience to guarantee your peace of mind and safety.",
members: [
{ role: "Owner / Beratungsstellenleiter", description: "Long-time tax law practitioner specializing in consulting for Polish workers in Germany." }
]
}
},
de: {
pageHeader: { title: "Über uns", subtitle: "Über 20 Jahre zuverlässige Steuerunterstützung für Polen, die in Deutschland arbeiten." },
bio: {
paragraphs: [
{ text: "Seit über zwei Jahrzehnten helfe ich polnischen Arbeitnehmern und Fachkräften in Deutschland bei der sicheren, stressfreien und völlig legalen Erledigung der Einkommensteuer gegenüber dem Finanzamt." },
{ text: "Mein Ziel ist es, Ihnen die Last der komplizierten deutschen Steuervorschriften von den Schultern zu nehmen. Wir führen den gesamten Prozess auf Polnisch durch — von der ersten Dokumentenanalyse über präzise Berechnungen von Freibeträgen bis hin zur Überprüfung des finalen Steuerbescheids." }
],
credentials: [
{ text: "Über 20 Jahre Praxis im deutschen Steuerrecht" },
{ text: "Beratungsstellenleiter im b.b.h. Lohnsteuerhilfeverein e.V." },
{ text: "Vollständiger Service in Polnisch und Deutsch" },
{ text: "Maximierung der Steuerrückerstattung und null formaler Stress" }
]
},
team: {
title: "Unser Team",
subtitle: "Lernen Sie die Experten kennen, die sich für Ihre Finanzen einsetzen. Jeder von uns bringt jahrelange Erfahrung mit, um Ihre Ruhe und Sicherheit zu garantieren.",
members: [
{ role: "Inhaber / Beratungsstellenleiter", description: "Langjähriger Steuerrechtspraktiker mit Spezialisierung auf die Beratung polnischer Arbeitnehmer in Deutschland." }
]
}
}
}
}
async function run() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', limit: 100, locale: 'pl' })
for (const page of pages.docs) {
if (!translations[page.slug || '']) continue;
for (const loc of ['en', 'de'] as const) {
const dbPage = await payload.findByID({ collection: 'pages', id: page.id, locale: loc })
if (!dbPage || !dbPage.layout) continue;
const tPage = translations[page.slug!][loc];
let modified = false;
const layout = dbPage.layout.map((block: any) => {
// preserve IDs
for (const key of Object.keys(block)) {
if (block[key] && typeof block[key] === 'object' && block[key].id !== undefined) {
block[key] = block[key].id
}
}
if (block.blockType === 'pageHeader' && tPage.pageHeader) {
block.title = tPage.pageHeader.title;
block.subtitle = tPage.pageHeader.subtitle;
modified = true;
}
if (block.blockType === 'servicesDetailed' && tPage.servicesDetailed) {
block.title = tPage.servicesDetailed.title;
block.subtitle = tPage.servicesDetailed.subtitle;
block.services = block.services.map((s: any, i: number) => ({
...s,
title: tPage.servicesDetailed.services[i]?.title || s.title,
description: tPage.servicesDetailed.services[i]?.description || s.description
}))
modified = true;
}
if (block.blockType === 'eligibility' && tPage.eligibility) {
block.title = tPage.eligibility.title;
block.subtitle = tPage.eligibility.subtitle;
block.eligibleTitle = tPage.eligibility.eligibleTitle;
block.limitationsTitle = tPage.eligibility.limitationsTitle;
block.limitationsText = tPage.eligibility.limitationsText;
block.excludedTitle = tPage.eligibility.excludedTitle;
block.legalNotice = tPage.eligibility.legalNotice;
block.eligibleItems = block.eligibleItems.map((s: any, i: number) => ({
...s, text: tPage.eligibility.eligibleItems[i]?.text || s.text
}))
block.excludedItems = block.excludedItems.map((s: any, i: number) => ({
...s, text: tPage.eligibility.excludedItems[i]?.text || s.text
}))
modified = true;
}
if (block.blockType === 'process' && tPage.process) {
block.title = tPage.process.title;
block.subtitle = tPage.process.subtitle;
block.steps = block.steps.map((s: any, i: number) => ({
...s,
title: tPage.process.steps[i]?.title || s.title,
description: tPage.process.steps[i]?.description || s.description
}))
modified = true;
}
if (block.blockType === 'bio' && tPage.bio) {
block.paragraphs = block.paragraphs.map((s: any, i: number) => ({
...s, text: tPage.bio.paragraphs[i]?.text || s.text
}))
block.credentials = block.credentials.map((s: any, i: number) => ({
...s, text: tPage.bio.credentials[i]?.text || s.text
}))
modified = true;
}
if (block.blockType === 'team' && tPage.team) {
block.title = tPage.team.title;
block.subtitle = tPage.team.subtitle;
block.members = block.members.map((s: any, i: number) => ({
...s,
role: tPage.team.members[i]?.role || s.role,
description: tPage.team.members[i]?.description || s.description
}))
modified = true;
}
if (block.blockType === 'contactInfo' && tPage.contactInfo) {
block.hours = tPage.contactInfo.hours;
modified = true;
}
return block;
});
if (modified) {
await payload.update({
collection: 'pages',
id: page.id,
locale: loc,
data: { layout }
})
console.log(`Fully translated ${page.slug} to ${loc}`)
}
}
}
process.exit(0)
}
run().catch(console.error)
+97
View File
@@ -0,0 +1,97 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
// Translation mapping for FinalCTA to EN
const enCta = {
title: "Have questions about your tax return?",
subtitle: "Contact us directly via WhatsApp. We will analyze your situation and help you get the refund you deserve.",
buttonText: "Message on WhatsApp",
}
// Translation mapping for FinalCTA to DE
const deCta = {
title: "Haben Sie Fragen zu Ihrer Steuererklärung?",
subtitle: "Kontaktieren Sie uns direkt über WhatsApp. Wir analysieren Ihre Situation und helfen Ihnen, die Ihnen zustehende Rückerstattung zu erhalten.",
buttonText: "Auf WhatsApp schreiben",
}
const pages = await payload.find({ collection: 'pages', limit: 100, locale: 'pl' })
for (const page of pages.docs) {
if (page.slug === 'home') continue; // already mostly translated
// EN Update
const enPage = await payload.findByID({ collection: 'pages', id: page.id, locale: 'en' })
if (enPage && enPage.layout) {
let modified = false
const layout = enPage.layout.map((block: any) => {
// Strip populated ids
for (const key of Object.keys(block)) {
if (block[key] && typeof block[key] === 'object' && block[key].id !== undefined) {
block[key] = block[key].id
}
}
if (block.blockType === 'finalCta') {
block.title = enCta.title
block.subtitle = enCta.subtitle
block.buttonText = enCta.buttonText
modified = true
}
if (block.blockType === 'pageHeader') {
if (page.slug === 'uslugi') {
block.title = "Our Services"
block.subtitle = "Professional tax and accounting support in Germany."
} else if (page.slug === 'kontakt') {
block.title = "Contact"
block.subtitle = "Do you have questions about tax settlement in Germany? Contact us — we will be happy to help."
}
modified = true
}
return block
})
if (modified) {
await payload.update({ collection: 'pages', id: page.id, locale: 'en', data: { layout } })
console.log(`Updated EN translations for ${page.slug}`)
}
}
// DE Update
const dePage = await payload.findByID({ collection: 'pages', id: page.id, locale: 'de' })
if (dePage && dePage.layout) {
let modified = false
const layout = dePage.layout.map((block: any) => {
for (const key of Object.keys(block)) {
if (block[key] && typeof block[key] === 'object' && block[key].id !== undefined) {
block[key] = block[key].id
}
}
if (block.blockType === 'finalCta') {
block.title = deCta.title
block.subtitle = deCta.subtitle
block.buttonText = deCta.buttonText
modified = true
}
if (block.blockType === 'pageHeader') {
if (page.slug === 'uslugi') {
block.title = "Unsere Dienstleistungen"
block.subtitle = "Professionelle steuerliche und buchhalterische Betreuung in Deutschland."
} else if (page.slug === 'kontakt') {
block.title = "Kontakt"
block.subtitle = "Haben Sie Fragen zur Steuererklärung in Deutschland? Kontaktieren Sie uns — wir helfen Ihnen gerne weiter."
}
modified = true
}
return block
})
if (modified) {
await payload.update({ collection: 'pages', id: page.id, locale: 'de', data: { layout } })
console.log(`Updated DE translations for ${page.slug}`)
}
}
}
process.exit(0)
}
run().catch(console.error)
+54
View File
@@ -0,0 +1,54 @@
import { getPayload } from 'payload'
import configPromise from '../src/payload.config'
async function run() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', limit: 100, locale: 'pl' })
for (const page of pages.docs) {
if (!['faq', 'kontakt', 'stowarzyszenie', 'polityka-prywatnosci', 'polityka-cookies', 'impressum'].includes(page.slug || '')) continue;
for (const loc of ['en', 'de'] as const) {
const dbPage = await payload.findByID({ collection: 'pages', id: page.id, locale: loc })
if (!dbPage || !dbPage.layout) continue;
let modified = false;
const layout = dbPage.layout.map((block: any) => {
// preserve IDs
for (const key of Object.keys(block)) {
if (block[key] && typeof block[key] === 'object' && block[key].id !== undefined) {
block[key] = block[key].id
}
}
if (block.blockType === 'contactInfo') {
block.hours = loc === 'en' ? "Mon-Fri: 9:00 - 17:00\nBy appointment" : "Mo-Fr: 9:00 - 17:00\nNach Vereinbarung"
block.address = "TO BE CONFIRMED BY CLIENT"
modified = true;
}
if (block.blockType === 'TextSection') {
block.text = loc === 'en' ? "Content in preparation" : "Inhalt in Vorbereitung"
modified = true;
}
return block;
});
if (modified) {
await payload.update({
collection: 'pages',
id: page.id,
locale: loc,
data: { layout }
})
console.log(`Fully translated ${page.slug} to ${loc}`)
}
}
}
process.exit(0)
}
run().catch(console.error)
@@ -13,6 +13,44 @@ type FAQFullBlock = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFul
import type { Metadata } from 'next'
import { createPageMetadata } from '@intecion/ipal-kit'
const pageMetadata = createPageMetadata({
config: i18n,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || 'https://www.jkedzierski.de'
})
export const revalidate = 3600
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const pages = await payload.find({ collection: 'pages', limit: 1000 })
const params: { locale: string; slug: string[] }[] = []
for (const page of pages.docs) {
if (page.slug && typeof page.slug === 'object') {
for (const [locale, slug] of Object.entries(page.slug)) {
if (slug && typeof slug === 'string') {
// Assuming index page is just the locale root without slug
params.push({
locale,
slug: slug === 'home' ? [] : slug.split('/')
})
}
}
}
}
// fallback to empty slugs for languages if none matched
if (params.length === 0) {
params.push({ locale: 'pl', slug: [] })
params.push({ locale: 'en', slug: [] })
params.push({ locale: 'de', slug: [] })
}
return params
}
export async function generateMetadata(
props: { params: Promise<{ locale: string; slug?: string[] }> }
): Promise<Metadata> {
@@ -20,46 +58,7 @@ export async function generateMetadata(
const { locale, slug } = params
const payload = await getPayload({ config: configPromise })
const route = await resolveRoute({ payload, locale, segments: slug || [] })
if (!route) {
return {}
}
const doc = await payload.findByID({
collection: ('collection' in route ? route.collection : 'pages') as 'pages',
id: route.doc.id as string,
locale: locale as 'all' | 'pl' | 'de' | 'en',
})
// Retrieve meta object injected by @payloadcms/plugin-seo
const meta = (doc as any).meta
const title = meta?.title || doc.title
const description = meta?.description || ''
let imageUrl = ''
if (meta?.image && typeof meta.image === 'object' && meta.image.url) {
imageUrl = meta.image.url
} else if (meta?.image && typeof meta.image === 'string') {
// If it's just an ID, we might not have the full URL without a separate fetch,
// but usually Payload plugin-seo populates the image relationship.
}
return {
title,
description,
openGraph: {
title,
description,
...(imageUrl && { images: [{ url: imageUrl }] }),
},
twitter: {
card: 'summary_large_image',
title,
description,
...(imageUrl && { images: [imageUrl] }),
},
}
return pageMetadata({ payload, locale, slug })
}
export default async function Page(props: {
+4 -9
View File
@@ -20,10 +20,7 @@ const fraunces = Fraunces({
variable: '--font-heading',
})
export const metadata = {
title: 'Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski',
description: 'Profesjonalne doradztwo podatkowe.',
}
export default async function RootLayout(props: {
children: React.ReactNode,
@@ -61,23 +58,21 @@ export default async function RootLayout(props: {
const schemaOrg = {
"@context": "https://schema.org",
"@type": "AccountingService",
"name": "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
"name": siteData?.siteName || "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
"telephone": companyData?.phone || '',
"email": companyData?.email || '',
"address": companyData?.address || '',
"openingHours": companyData?.hours || '',
"url": "https://www.jkedzierski.de"
"url": process.env.NEXT_PUBLIC_SERVER_URL || "https://www.jkedzierski.de"
};
return (
<html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
<head>
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg) }}
/>
</head>
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
<Providers>
<LanguageSlugsProvider>
<ConsentProvider texts={consentTexts}>
+5
View File
@@ -0,0 +1,5 @@
import { buildRobots } from '@intecion/ipal-kit'
export default function robots() {
return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || '' })
}
+13
View File
@@ -0,0 +1,13 @@
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { buildSitemapEntries } from '@intecion/ipal-kit'
import { i18n } from '../i18n.config'
export default async function sitemap() {
const payload = await getPayload({ config: configPromise })
return buildSitemapEntries({
payload,
config: i18n,
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || '',
})
}
+16 -6
View File
@@ -15,7 +15,17 @@ export const ContactInfoBlock: React.FC<Props> = ({
hours = 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
}) => {
const pathname = usePathname()
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
const localeSegment = pathname?.split('/').filter(Boolean)[0]
const isDe = localeSegment === 'de'
const isEn = localeSegment === 'en'
const t = {
contactInfo: isEn ? 'Contact Information' : (isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'),
address: isEn ? 'Address' : (isDe ? 'Adresse' : 'Adres'),
phone: isEn ? 'Phone' : (isDe ? 'Telefon' : 'Telefon'),
email: isEn ? 'Email' : (isDe ? 'E-Mail' : 'E-mail'),
hours: isEn ? 'Working Hours' : (isDe ? 'Öffnungszeiten' : 'Godziny pracy'),
}
return (
<section className="py-16 md:py-24 bg-background">
@@ -34,7 +44,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
<div className="absolute top-0 right-0 w-12 h-12 bg-background border-l border-b border-border shadow-sm transform translate-x-6 -translate-y-6 rotate-45 pointer-events-none" />
<h2 className="font-heading text-2xl md:text-3xl font-bold text-foreground mb-10 text-center md:text-left">
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
{t.contactInfo}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-12">
@@ -43,7 +53,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<MapPin size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Adresse' : 'Adres'}
{t.address}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
@@ -57,7 +67,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Phone size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Telefon' : 'Telefon'}
{t.phone}
</h3>
</div>
<a
@@ -74,7 +84,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Mail size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'E-Mail' : 'E-mail'}
{t.email}
</h3>
</div>
<a
@@ -91,7 +101,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Clock size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
{t.hours}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
+18 -7
View File
@@ -12,7 +12,18 @@ export const ContactSectionClient: React.FC<any> = ({
formNode,
}) => {
const pathname = usePathname()
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
const localeSegment = pathname?.split('/').filter(Boolean)[0]
const isDe = localeSegment === 'de'
const isEn = localeSegment === 'en'
const t = {
contactInfo: isEn ? 'Contact Information' : (isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'),
address: isEn ? 'Address' : (isDe ? 'Adresse' : 'Adres'),
phone: isEn ? 'Phone' : (isDe ? 'Telefon' : 'Telefon'),
email: isEn ? 'Email' : (isDe ? 'E-Mail' : 'E-mail'),
hours: isEn ? 'Working Hours' : (isDe ? 'Öffnungszeiten' : 'Godziny pracy'),
writeToUs: isEn ? 'Write to us' : (isDe ? 'Schreiben Sie uns' : 'Napisz do nas'),
}
return (
<section className="py-16 md:py-24 bg-background">
@@ -28,7 +39,7 @@ export const ContactSectionClient: React.FC<any> = ({
className="flex flex-col"
>
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
{t.contactInfo}
</h2>
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
@@ -42,7 +53,7 @@ export const ContactSectionClient: React.FC<any> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<MapPin size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Adresse' : 'Adres'}
{t.address}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
@@ -56,7 +67,7 @@ export const ContactSectionClient: React.FC<any> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Phone size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Telefon' : 'Telefon'}
{t.phone}
</h3>
</div>
<a
@@ -73,7 +84,7 @@ export const ContactSectionClient: React.FC<any> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Mail size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'E-Mail' : 'E-mail'}
{t.email}
</h3>
</div>
<a
@@ -90,7 +101,7 @@ export const ContactSectionClient: React.FC<any> = ({
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Clock size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
{t.hours}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
@@ -111,7 +122,7 @@ export const ContactSectionClient: React.FC<any> = ({
className="flex flex-col"
>
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
{isDe ? 'Schreiben Sie uns' : 'Napisz do nas'}
{t.writeToUs}
</h2>
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
style={{ boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)' }}
+28 -5
View File
@@ -2,15 +2,38 @@ import React from 'react'
import type { Page } from '../../payload-types'
import { MessageCircle } from 'lucide-react'
import { MotionStamp } from '../../components/MotionStamp'
import Image from 'next/image'
import type { Media } from '../../payload-types'
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
type Props = BaseProps & { whatsappNumber?: string | null }
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText, whatsappNumber }) => {
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText, image, whatsappNumber }) => {
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
const bgMedia = typeof image === 'object' && image !== null ? (image as Media) : null
const imageUrl = bgMedia?.url
const imageAlt = bgMedia?.alt || 'Tło Call to Action'
return (
<MotionStamp id="final-cta" className="py-20 md:py-32 text-center" style={{ backgroundColor: 'var(--accent-color)', color: 'var(--bg-color)' }}>
<div className="container-premium">
<section id="final-cta" className="relative py-20 md:py-32 text-center overflow-hidden">
{/* Background Image Layer */}
<div className="absolute inset-0 z-0 bg-[var(--accent-color)]">
{imageUrl && (
<>
<Image
src={imageUrl}
alt={imageAlt}
fill
className="object-cover object-center opacity-40 mix-blend-multiply"
/>
{/* Opcjonalny gradient/overlay dla lepszej czytelności */}
<div className="absolute inset-0 bg-black/30" />
</>
)}
</div>
<MotionStamp className="container-premium relative z-10">
<div className="max-w-3xl mx-auto flex flex-col items-center">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight mb-6 text-balance text-[var(--bg-color)]">
{title}
@@ -36,7 +59,7 @@ export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, butto
</div>
)}
</div>
</div>
</MotionStamp>
</MotionStamp>
</section>
)
}
+6
View File
@@ -7,5 +7,11 @@ export const FinalCTA: Block = {
{ name: 'title', type: 'text', required: true, localized: true },
{ name: 'subtitle', type: 'text', localized: true },
{ name: 'buttonText', type: 'text', required: true, localized: true },
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: false,
},
],
}
+1 -35
View File
@@ -40,7 +40,6 @@ export const HeroBlock: React.FC<Props> = ({
primaryCta,
secondaryCta,
image,
floatingStat,
whatsappNumber,
}) => {
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
@@ -49,9 +48,6 @@ export const HeroBlock: React.FC<Props> = ({
const imageUrl = heroMedia?.url
const imageAlt = heroMedia?.alt || 'Tło hero'
const statValue = floatingStat?.value || '20+'
const statLabel = floatingStat?.label || 'Lat doświadczenia'
return (
<motion.section
initial="hidden"
@@ -143,37 +139,7 @@ export const HeroBlock: React.FC<Props> = ({
</motion.div>
</motion.div>
{/* Floating Stat "Seal" (Bottom Right) */}
<motion.div
initial={{ opacity: 0, scale: 0.9, rotate: -5 }}
animate={{ opacity: 1, scale: 1, rotate: 0 }}
transition={{ duration: 0.8, delay: 0.6, ease: [0.34, 1.56, 0.64, 1] }}
className="hidden lg:flex flex-col items-center justify-center bg-card border border-border/50 p-6 lg:p-8 shadow-2xl lg:-translate-y-4 max-w-[200px] aspect-square rounded-sm relative shrink-0"
>
{/* Dekoracyjna krawędź przypominająca stempel */}
<div className="absolute inset-2 border border-border/20 pointer-events-none" />
<span className="font-heading text-4xl lg:text-5xl font-extrabold tracking-tight text-accent mb-2 drop-shadow-sm relative z-10 text-center">
{statValue}
</span>
<span className="text-sm uppercase tracking-widest text-foreground/80 font-medium text-center relative z-10 leading-snug">
{statLabel}
</span>
</motion.div>
{/* Mobile version of the seal - more inline */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6, ease: customEase }}
className="flex lg:hidden items-center gap-4 bg-card/90 backdrop-blur-sm border border-border/50 shadow-md p-4 w-full mt-4"
>
<span className="font-heading text-3xl font-extrabold tracking-tight text-accent drop-shadow-sm">
{statValue}
</span>
<span className="text-xs uppercase tracking-widest text-foreground/80 font-medium leading-snug">
{statLabel}
</span>
</motion.div>
</div>
</div>
+1 -8
View File
@@ -15,14 +15,7 @@ export const Hero: Block = {
{ name: 'url', type: 'text' },
],
},
{
name: 'floatingStat',
type: 'group',
fields: [
{ name: 'value', type: 'text', localized: true, defaultValue: '20+' },
{ name: 'label', type: 'text', localized: true, defaultValue: 'Lat doświadczenia' },
],
},
{ name: 'image', type: 'upload', relationTo: 'media' },
{ name: 'imageSecondary', type: 'upload', relationTo: 'media' },
],
+5 -3
View File
@@ -35,9 +35,11 @@ export const ServicesDetailedBlock: React.FC<Props> = ({
}) => {
const serviceList = services || []
const pathname = usePathname()
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
const contactHref = isDe ? '/de/kontakt' : '/kontakt'
const contactText = isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się'
const localeSegment = pathname?.split('/').filter(Boolean)[0]
const isDe = localeSegment === 'de'
const isEn = localeSegment === 'en'
const contactHref = isEn ? '/en/contact' : (isDe ? '/de/kontakt' : '/kontakt')
const contactText = isEn ? 'Contact us' : (isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się')
return (
<section className="py-16 md:py-24 bg-background text-foreground">
+4 -2
View File
@@ -11,7 +11,9 @@ type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'team' }>
export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, subtitle }) => {
const pathname = usePathname()
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
const localeSegment = pathname?.split('/').filter(Boolean)[0]
const isDe = localeSegment === 'de'
const isEn = localeSegment === 'en'
// MOCK_MEMBERS usunięte na rzecz prawdziwych danych z CMS
const members = originalMembers
@@ -152,7 +154,7 @@ export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, su
</p>
<div className="mt-auto flex items-center text-xs font-bold text-foreground/40 group-hover:text-accent transition-colors uppercase tracking-wider">
<span>{isDe ? 'Mehr lesen' : 'Czytaj więcej'}</span>
<span>{isEn ? 'Read more' : (isDe ? 'Mehr lesen' : 'Czytaj więcej')}</span>
<svg className="w-4 h-4 ml-1.5 transform group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
import * as migration_20260919_104831 from './20260919_104831';
export const migrations = [
{
up: migration_20260919_104831.up,
down: migration_20260919_104831.down,
name: '20260919_104831'
},
];
+2 -10
View File
@@ -333,10 +333,6 @@ export interface Page {
text?: string | null;
url?: string | null;
};
floatingStat?: {
value?: string | null;
label?: string | null;
};
image?: (number | null) | Media;
imageSecondary?: (number | null) | Media;
id?: string | null;
@@ -506,6 +502,7 @@ export interface Page {
title: string;
subtitle?: string | null;
buttonText: string;
image?: (number | null) | Media;
id?: string | null;
blockName?: string | null;
blockType: 'finalCta';
@@ -1030,12 +1027,6 @@ export interface PagesSelect<T extends boolean = true> {
text?: T;
url?: T;
};
floatingStat?:
| T
| {
value?: T;
label?: T;
};
image?: T;
imageSecondary?: T;
id?: T;
@@ -1214,6 +1205,7 @@ export interface PagesSelect<T extends boolean = true> {
title?: T;
subtitle?: T;
buttonText?: T;
image?: T;
id?: T;
blockName?: T;
};
+4 -5
View File
@@ -1,4 +1,5 @@
import { sqliteAdapter } from '@payloadcms/db-sqlite'
import { postgresAdapter } from '@payloadcms/db-postgres'
import { mcpPlugin } from '@payloadcms/plugin-mcp'
import { seoPlugin } from '@payloadcms/plugin-seo'
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
@@ -33,11 +34,9 @@ export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
db: sqliteAdapter({
client: {
url: process.env.DATABASE_URI || '',
},
}),
db: process.env.DATABASE_URI?.startsWith('postgres')
? postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI }, push: false })
: sqliteAdapter({ client: { url: process.env.DATABASE_URI || '' } }),
sharp,
localization: {
locales: i18n.locales.map(l => l.code),