Compare commits
9
Commits
f258dfad43
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d9ed69f38 | ||
|
|
9a772dd349 | ||
|
|
09d98a8094 | ||
|
|
65994439d7 | ||
|
|
c8193cf166 | ||
|
|
ab8a148823 | ||
|
|
4299206097 | ||
|
|
3b4be7907f | ||
|
|
f00082a5e9 |
@@ -0,0 +1,12 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
dev.db
|
||||||
|
*.db
|
||||||
|
.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
.vscode
|
||||||
|
.gemini
|
||||||
|
.agents
|
||||||
+18
-1
@@ -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
|
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
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ const __filename = fileURLToPath(import.meta.url)
|
|||||||
const dirname = path.dirname(__filename)
|
const dirname = path.dirname(__filename)
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
async headers() {
|
async headers() {
|
||||||
const { buildSecurityHeaders } = await import('@intecion/ipal-kit')
|
const { buildSecurityHeaders } = await import('@intecion/ipal-kit')
|
||||||
return [
|
return [
|
||||||
@@ -41,6 +42,10 @@ const nextConfig: NextConfig = {
|
|||||||
turbopack: {
|
turbopack: {
|
||||||
root: path.resolve(dirname),
|
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 })
|
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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 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(
|
export async function generateMetadata(
|
||||||
props: { params: Promise<{ locale: string; slug?: string[] }> }
|
props: { params: Promise<{ locale: string; slug?: string[] }> }
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
@@ -20,46 +58,7 @@ export async function generateMetadata(
|
|||||||
const { locale, slug } = params
|
const { locale, slug } = params
|
||||||
const payload = await getPayload({ config: configPromise })
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
const route = await resolveRoute({ payload, locale, segments: slug || [] })
|
return pageMetadata({ payload, locale, 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] }),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ const fraunces = Fraunces({
|
|||||||
variable: '--font-heading',
|
variable: '--font-heading',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski',
|
|
||||||
description: 'Profesjonalne doradztwo podatkowe.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function RootLayout(props: {
|
export default async function RootLayout(props: {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
@@ -61,23 +58,21 @@ export default async function RootLayout(props: {
|
|||||||
const schemaOrg = {
|
const schemaOrg = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "AccountingService",
|
"@type": "AccountingService",
|
||||||
"name": "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
|
"name": siteData?.siteName || "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
|
||||||
"telephone": companyData?.phone || '',
|
"telephone": companyData?.phone || '',
|
||||||
"email": companyData?.email || '',
|
"email": companyData?.email || '',
|
||||||
"address": companyData?.address || '',
|
"address": companyData?.address || '',
|
||||||
"openingHours": companyData?.hours || '',
|
"openingHours": companyData?.hours || '',
|
||||||
"url": "https://www.jkedzierski.de"
|
"url": process.env.NEXT_PUBLIC_SERVER_URL || "https://www.jkedzierski.de"
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
|
<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
|
<script
|
||||||
type="application/ld+json"
|
type="application/ld+json"
|
||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg) }}
|
||||||
/>
|
/>
|
||||||
</head>
|
|
||||||
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
|
|
||||||
<Providers>
|
<Providers>
|
||||||
<LanguageSlugsProvider>
|
<LanguageSlugsProvider>
|
||||||
<ConsentProvider texts={consentTexts}>
|
<ConsentProvider texts={consentTexts}>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { buildRobots } from '@intecion/ipal-kit'
|
||||||
|
|
||||||
|
export default function robots() {
|
||||||
|
return buildRobots({ baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || '' })
|
||||||
|
}
|
||||||
@@ -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 || '',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,17 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
|||||||
hours = 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
|
hours = 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
|
||||||
}) => {
|
}) => {
|
||||||
const pathname = usePathname()
|
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 (
|
return (
|
||||||
<section className="py-16 md:py-24 bg-background">
|
<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" />
|
<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">
|
<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>
|
</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-12">
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<MapPin size={24} strokeWidth={2.5} />
|
<MapPin size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Adresse' : 'Adres'}
|
{t.address}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Phone size={24} strokeWidth={2.5} />
|
<Phone size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Telefon' : 'Telefon'}
|
{t.phone}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Mail size={24} strokeWidth={2.5} />
|
<Mail size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'E-Mail' : 'E-mail'}
|
{t.email}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Clock size={24} strokeWidth={2.5} />
|
<Clock size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
|
{t.hours}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
|
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
|
||||||
|
|||||||
@@ -12,7 +12,18 @@ export const ContactSectionClient: React.FC<any> = ({
|
|||||||
formNode,
|
formNode,
|
||||||
}) => {
|
}) => {
|
||||||
const pathname = usePathname()
|
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 (
|
return (
|
||||||
<section className="py-16 md:py-24 bg-background">
|
<section className="py-16 md:py-24 bg-background">
|
||||||
@@ -28,7 +39,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
|||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
>
|
>
|
||||||
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
|
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
|
||||||
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
|
{t.contactInfo}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<MapPin size={24} strokeWidth={2.5} />
|
<MapPin size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Adresse' : 'Adres'}
|
{t.address}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Phone size={24} strokeWidth={2.5} />
|
<Phone size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Telefon' : 'Telefon'}
|
{t.phone}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Mail size={24} strokeWidth={2.5} />
|
<Mail size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'E-Mail' : 'E-mail'}
|
{t.email}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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">
|
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||||
<Clock size={24} strokeWidth={2.5} />
|
<Clock size={24} strokeWidth={2.5} />
|
||||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||||
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
|
{t.hours}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
|
<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"
|
className="flex flex-col"
|
||||||
>
|
>
|
||||||
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
|
<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>
|
</h2>
|
||||||
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
|
<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)' }}
|
style={{ boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)' }}
|
||||||
|
|||||||
@@ -2,15 +2,38 @@ import React from 'react'
|
|||||||
import type { Page } from '../../payload-types'
|
import type { Page } from '../../payload-types'
|
||||||
import { MessageCircle } from 'lucide-react'
|
import { MessageCircle } from 'lucide-react'
|
||||||
import { MotionStamp } from '../../components/MotionStamp'
|
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 BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
|
||||||
type Props = BaseProps & { whatsappNumber?: string | null }
|
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 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 (
|
return (
|
||||||
<MotionStamp id="final-cta" className="py-20 md:py-32 text-center" style={{ backgroundColor: 'var(--accent-color)', color: 'var(--bg-color)' }}>
|
<section id="final-cta" className="relative py-20 md:py-32 text-center overflow-hidden">
|
||||||
<div className="container-premium">
|
{/* 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">
|
<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)]">
|
<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}
|
{title}
|
||||||
@@ -36,7 +59,7 @@ export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, butto
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MotionStamp>
|
||||||
</MotionStamp>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,11 @@ export const FinalCTA: Block = {
|
|||||||
{ name: 'title', type: 'text', required: true, localized: true },
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
{ name: 'subtitle', type: 'text', localized: true },
|
{ name: 'subtitle', type: 'text', localized: true },
|
||||||
{ name: 'buttonText', type: 'text', required: true, localized: true },
|
{ name: 'buttonText', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
relationTo: 'media',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState } from 'react'
|
import React from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import type { Page, Media } from '../../payload-types'
|
import type { Page, Media } from '../../payload-types'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
@@ -40,19 +40,14 @@ export const HeroBlock: React.FC<Props> = ({
|
|||||||
primaryCta,
|
primaryCta,
|
||||||
secondaryCta,
|
secondaryCta,
|
||||||
image,
|
image,
|
||||||
floatingStat,
|
|
||||||
whatsappNumber,
|
whatsappNumber,
|
||||||
}) => {
|
}) => {
|
||||||
const [imageLoaded, setImageLoaded] = useState(false)
|
|
||||||
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
|
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
|
||||||
|
|
||||||
const heroMedia = typeof image === 'object' && image !== null ? (image as Media) : null
|
const heroMedia = typeof image === 'object' && image !== null ? (image as Media) : null
|
||||||
const imageUrl = heroMedia?.url
|
const imageUrl = heroMedia?.url
|
||||||
const imageAlt = heroMedia?.alt || 'Tło hero'
|
const imageAlt = heroMedia?.alt || 'Tło hero'
|
||||||
|
|
||||||
const statValue = floatingStat?.value || '20+'
|
|
||||||
const statLabel = floatingStat?.label || 'Lat doświadczenia'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
@@ -69,10 +64,7 @@ export const HeroBlock: React.FC<Props> = ({
|
|||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
onLoad={() => setImageLoaded(true)}
|
className="object-cover object-center"
|
||||||
className={`object-cover object-center transition-all duration-1000 ${
|
|
||||||
imageLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
{/* Ciemny gradient pod spodem, by biały tekst był świetnie czytelny, a zdjęcie pozostało nasycone */}
|
{/* Ciemny gradient pod spodem, by biały tekst był świetnie czytelny, a zdjęcie pozostało nasycone */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent lg:bg-gradient-to-r lg:from-black/90 lg:via-black/40 lg:to-transparent" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent lg:bg-gradient-to-r lg:from-black/90 lg:via-black/40 lg:to-transparent" />
|
||||||
@@ -147,37 +139,7 @@ export const HeroBlock: React.FC<Props> = ({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,14 +15,7 @@ export const Hero: Block = {
|
|||||||
{ name: 'url', type: 'text' },
|
{ 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: 'image', type: 'upload', relationTo: 'media' },
|
||||||
{ name: 'imageSecondary', type: 'upload', relationTo: 'media' },
|
{ name: 'imageSecondary', type: 'upload', relationTo: 'media' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ export const ServicesDetailedBlock: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const serviceList = services || []
|
const serviceList = services || []
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
|
const localeSegment = pathname?.split('/').filter(Boolean)[0]
|
||||||
const contactHref = isDe ? '/de/kontakt' : '/kontakt'
|
const isDe = localeSegment === 'de'
|
||||||
const contactText = isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się'
|
const isEn = localeSegment === 'en'
|
||||||
|
const contactHref = isEn ? '/en/contact' : (isDe ? '/de/kontakt' : '/kontakt')
|
||||||
|
const contactText = isEn ? 'Contact us' : (isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-16 md:py-24 bg-background text-foreground">
|
<section className="py-16 md:py-24 bg-background text-foreground">
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'team' }>
|
|||||||
|
|
||||||
export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, subtitle }) => {
|
export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, subtitle }) => {
|
||||||
const pathname = usePathname()
|
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
|
// MOCK_MEMBERS usunięte na rzecz prawdziwych danych z CMS
|
||||||
const members = originalMembers
|
const members = originalMembers
|
||||||
@@ -152,7 +154,7 @@ export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, su
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-auto flex items-center text-xs font-bold text-foreground/40 group-hover:text-accent transition-colors uppercase tracking-wider">
|
<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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { NextResponse } from 'next/server'
|
|
||||||
import type { NextRequest } from 'next/server'
|
|
||||||
import { i18n } from './i18n.config'
|
|
||||||
|
|
||||||
function getLocale(request: NextRequest): string {
|
|
||||||
const acceptLanguage = request.headers.get('accept-language')
|
|
||||||
const defaultLocale = i18n.defaultLocale
|
|
||||||
const supportedLocales = i18n.locales.map((l) => l.code)
|
|
||||||
|
|
||||||
if (!acceptLanguage) {
|
|
||||||
return defaultLocale
|
|
||||||
}
|
|
||||||
|
|
||||||
// Very basic parser for Accept-Language header
|
|
||||||
// Example header: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
|
|
||||||
const languages = acceptLanguage
|
|
||||||
.split(',')
|
|
||||||
.map((lang) => {
|
|
||||||
const [code, qValue] = lang.split(';q=')
|
|
||||||
const quality = qValue ? parseFloat(qValue) : 1.0
|
|
||||||
return { code: code.trim().split('-')[0].toLowerCase(), quality } // Extract primary subtag (e.g., 'en-US' -> 'en')
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.quality - a.quality)
|
|
||||||
|
|
||||||
// Find the first matching supported locale
|
|
||||||
for (const { code } of languages) {
|
|
||||||
if (supportedLocales.includes(code as any)) {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultLocale
|
|
||||||
}
|
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
|
||||||
const { pathname } = request.nextUrl
|
|
||||||
const locales = i18n.locales.map((l) => l.code)
|
|
||||||
|
|
||||||
// Bypass paths that should not be localized
|
|
||||||
const bypassPaths = [
|
|
||||||
'/api',
|
|
||||||
'/_next',
|
|
||||||
'/admin',
|
|
||||||
'/favicon.ico',
|
|
||||||
'/robots.txt',
|
|
||||||
'/sitemap.xml',
|
|
||||||
'/site.webmanifest',
|
|
||||||
'/icon', // standard icons
|
|
||||||
]
|
|
||||||
|
|
||||||
if (bypassPaths.some((path) => pathname.startsWith(path))) {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there is any supported locale in the pathname
|
|
||||||
const pathnameHasLocale = locales.some(
|
|
||||||
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
|
||||||
)
|
|
||||||
|
|
||||||
if (pathnameHasLocale) {
|
|
||||||
return NextResponse.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect if there is no locale
|
|
||||||
const locale = getLocale(request)
|
|
||||||
request.nextUrl.pathname = `/${locale}${pathname}`
|
|
||||||
|
|
||||||
return NextResponse.redirect(request.nextUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
// Matcher ignoring `/_next/` and `/api/`
|
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -333,10 +333,6 @@ export interface Page {
|
|||||||
text?: string | null;
|
text?: string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
floatingStat?: {
|
|
||||||
value?: string | null;
|
|
||||||
label?: string | null;
|
|
||||||
};
|
|
||||||
image?: (number | null) | Media;
|
image?: (number | null) | Media;
|
||||||
imageSecondary?: (number | null) | Media;
|
imageSecondary?: (number | null) | Media;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -506,6 +502,7 @@ export interface Page {
|
|||||||
title: string;
|
title: string;
|
||||||
subtitle?: string | null;
|
subtitle?: string | null;
|
||||||
buttonText: string;
|
buttonText: string;
|
||||||
|
image?: (number | null) | Media;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'finalCta';
|
blockType: 'finalCta';
|
||||||
@@ -1030,12 +1027,6 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
text?: T;
|
text?: T;
|
||||||
url?: T;
|
url?: T;
|
||||||
};
|
};
|
||||||
floatingStat?:
|
|
||||||
| T
|
|
||||||
| {
|
|
||||||
value?: T;
|
|
||||||
label?: T;
|
|
||||||
};
|
|
||||||
image?: T;
|
image?: T;
|
||||||
imageSecondary?: T;
|
imageSecondary?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
@@ -1214,6 +1205,7 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
title?: T;
|
title?: T;
|
||||||
subtitle?: T;
|
subtitle?: T;
|
||||||
buttonText?: T;
|
buttonText?: T;
|
||||||
|
image?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||||
|
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||||
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
||||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||||
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
|
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
|
||||||
@@ -33,11 +34,9 @@ export default buildConfig({
|
|||||||
typescript: {
|
typescript: {
|
||||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
},
|
},
|
||||||
db: sqliteAdapter({
|
db: process.env.DATABASE_URI?.startsWith('postgres')
|
||||||
client: {
|
? postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI }, push: false })
|
||||||
url: process.env.DATABASE_URI || '',
|
: sqliteAdapter({ client: { url: process.env.DATABASE_URI || '' } }),
|
||||||
},
|
|
||||||
}),
|
|
||||||
sharp,
|
sharp,
|
||||||
localization: {
|
localization: {
|
||||||
locales: i18n.locales.map(l => l.code),
|
locales: i18n.locales.map(l => l.code),
|
||||||
|
|||||||
Reference in New Issue
Block a user