Compare commits
7
Commits
3b4be7907f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d9ed69f38 | ||
|
|
9a772dd349 | ||
|
|
09d98a8094 | ||
|
|
65994439d7 | ||
|
|
c8193cf166 | ||
|
|
ab8a148823 | ||
|
|
4299206097 |
@@ -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
|
||||
|
||||
# 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 nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
async headers() {
|
||||
const { buildSecurityHeaders } = await import('@intecion/ipal-kit')
|
||||
return [
|
||||
@@ -41,6 +42,10 @@ const nextConfig: NextConfig = {
|
||||
turbopack: {
|
||||
root: path.resolve(dirname),
|
||||
},
|
||||
experimental: {
|
||||
// @ts-expect-error - Undocumented Next.js feature required for SEO streaming
|
||||
htmlLimitedBots: /Googlebot|bingbot|yandex|baiduspider/i,
|
||||
},
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
|
||||
@@ -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 { createPageMetadata } from '@intecion/ipal-kit'
|
||||
|
||||
const pageMetadata = createPageMetadata({
|
||||
config: i18n,
|
||||
baseUrl: process.env.NEXT_PUBLIC_SERVER_URL || 'https://www.jkedzierski.de'
|
||||
})
|
||||
|
||||
export const revalidate = 3600
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
const pages = await payload.find({ collection: 'pages', limit: 1000 })
|
||||
const params: { locale: string; slug: string[] }[] = []
|
||||
|
||||
for (const page of pages.docs) {
|
||||
if (page.slug && typeof page.slug === 'object') {
|
||||
for (const [locale, slug] of Object.entries(page.slug)) {
|
||||
if (slug && typeof slug === 'string') {
|
||||
// Assuming index page is just the locale root without slug
|
||||
params.push({
|
||||
locale,
|
||||
slug: slug === 'home' ? [] : slug.split('/')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to empty slugs for languages if none matched
|
||||
if (params.length === 0) {
|
||||
params.push({ locale: 'pl', slug: [] })
|
||||
params.push({ locale: 'en', slug: [] })
|
||||
params.push({ locale: 'de', slug: [] })
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ locale: string; slug?: string[] }> }
|
||||
): Promise<Metadata> {
|
||||
@@ -20,46 +58,7 @@ export async function generateMetadata(
|
||||
const { locale, slug } = params
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const route = await resolveRoute({ payload, locale, segments: slug || [] })
|
||||
if (!route) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const doc = await payload.findByID({
|
||||
collection: ('collection' in route ? route.collection : 'pages') as 'pages',
|
||||
id: route.doc.id as string,
|
||||
locale: locale as 'all' | 'pl' | 'de' | 'en',
|
||||
})
|
||||
|
||||
// Retrieve meta object injected by @payloadcms/plugin-seo
|
||||
const meta = (doc as any).meta
|
||||
|
||||
const title = meta?.title || doc.title
|
||||
const description = meta?.description || ''
|
||||
|
||||
let imageUrl = ''
|
||||
if (meta?.image && typeof meta.image === 'object' && meta.image.url) {
|
||||
imageUrl = meta.image.url
|
||||
} else if (meta?.image && typeof meta.image === 'string') {
|
||||
// If it's just an ID, we might not have the full URL without a separate fetch,
|
||||
// but usually Payload plugin-seo populates the image relationship.
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
...(imageUrl && { images: [{ url: imageUrl }] }),
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title,
|
||||
description,
|
||||
...(imageUrl && { images: [imageUrl] }),
|
||||
},
|
||||
}
|
||||
return pageMetadata({ payload, locale, slug })
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
|
||||
@@ -20,10 +20,7 @@ const fraunces = Fraunces({
|
||||
variable: '--font-heading',
|
||||
})
|
||||
|
||||
export const metadata = {
|
||||
title: 'Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski',
|
||||
description: 'Profesjonalne doradztwo podatkowe.',
|
||||
}
|
||||
|
||||
|
||||
export default async function RootLayout(props: {
|
||||
children: React.ReactNode,
|
||||
@@ -61,23 +58,21 @@ export default async function RootLayout(props: {
|
||||
const schemaOrg = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AccountingService",
|
||||
"name": "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
|
||||
"name": siteData?.siteName || "Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski",
|
||||
"telephone": companyData?.phone || '',
|
||||
"email": companyData?.email || '',
|
||||
"address": companyData?.address || '',
|
||||
"openingHours": companyData?.hours || '',
|
||||
"url": "https://www.jkedzierski.de"
|
||||
"url": process.env.NEXT_PUBLIC_SERVER_URL || "https://www.jkedzierski.de"
|
||||
};
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
|
||||
<head>
|
||||
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaOrg) }}
|
||||
/>
|
||||
</head>
|
||||
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
|
||||
<Providers>
|
||||
<LanguageSlugsProvider>
|
||||
<ConsentProvider texts={consentTexts}>
|
||||
|
||||
@@ -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',
|
||||
}) => {
|
||||
const pathname = usePathname()
|
||||
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
|
||||
const localeSegment = pathname?.split('/').filter(Boolean)[0]
|
||||
const isDe = localeSegment === 'de'
|
||||
const isEn = localeSegment === 'en'
|
||||
|
||||
const t = {
|
||||
contactInfo: isEn ? 'Contact Information' : (isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'),
|
||||
address: isEn ? 'Address' : (isDe ? 'Adresse' : 'Adres'),
|
||||
phone: isEn ? 'Phone' : (isDe ? 'Telefon' : 'Telefon'),
|
||||
email: isEn ? 'Email' : (isDe ? 'E-Mail' : 'E-mail'),
|
||||
hours: isEn ? 'Working Hours' : (isDe ? 'Öffnungszeiten' : 'Godziny pracy'),
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-background">
|
||||
@@ -34,7 +44,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
||||
<div className="absolute top-0 right-0 w-12 h-12 bg-background border-l border-b border-border shadow-sm transform translate-x-6 -translate-y-6 rotate-45 pointer-events-none" />
|
||||
|
||||
<h2 className="font-heading text-2xl md:text-3xl font-bold text-foreground mb-10 text-center md:text-left">
|
||||
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
|
||||
{t.contactInfo}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-12">
|
||||
@@ -43,7 +53,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<MapPin size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Adresse' : 'Adres'}
|
||||
{t.address}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
|
||||
@@ -57,7 +67,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Phone size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Telefon' : 'Telefon'}
|
||||
{t.phone}
|
||||
</h3>
|
||||
</div>
|
||||
<a
|
||||
@@ -74,7 +84,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Mail size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'E-Mail' : 'E-mail'}
|
||||
{t.email}
|
||||
</h3>
|
||||
</div>
|
||||
<a
|
||||
@@ -91,7 +101,7 @@ export const ContactInfoBlock: React.FC<Props> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Clock size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
|
||||
{t.hours}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[65ch]">
|
||||
|
||||
@@ -12,7 +12,18 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
formNode,
|
||||
}) => {
|
||||
const pathname = usePathname()
|
||||
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
|
||||
const localeSegment = pathname?.split('/').filter(Boolean)[0]
|
||||
const isDe = localeSegment === 'de'
|
||||
const isEn = localeSegment === 'en'
|
||||
|
||||
const t = {
|
||||
contactInfo: isEn ? 'Contact Information' : (isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'),
|
||||
address: isEn ? 'Address' : (isDe ? 'Adresse' : 'Adres'),
|
||||
phone: isEn ? 'Phone' : (isDe ? 'Telefon' : 'Telefon'),
|
||||
email: isEn ? 'Email' : (isDe ? 'E-Mail' : 'E-mail'),
|
||||
hours: isEn ? 'Working Hours' : (isDe ? 'Öffnungszeiten' : 'Godziny pracy'),
|
||||
writeToUs: isEn ? 'Write to us' : (isDe ? 'Schreiben Sie uns' : 'Napisz do nas'),
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-background">
|
||||
@@ -28,7 +39,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
className="flex flex-col"
|
||||
>
|
||||
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
|
||||
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
|
||||
{t.contactInfo}
|
||||
</h2>
|
||||
|
||||
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
|
||||
@@ -42,7 +53,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<MapPin size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Adresse' : 'Adres'}
|
||||
{t.address}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
|
||||
@@ -56,7 +67,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Phone size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Telefon' : 'Telefon'}
|
||||
{t.phone}
|
||||
</h3>
|
||||
</div>
|
||||
<a
|
||||
@@ -73,7 +84,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Mail size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'E-Mail' : 'E-mail'}
|
||||
{t.email}
|
||||
</h3>
|
||||
</div>
|
||||
<a
|
||||
@@ -90,7 +101,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
|
||||
<Clock size={24} strokeWidth={2.5} />
|
||||
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
|
||||
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
|
||||
{t.hours}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base">
|
||||
@@ -111,7 +122,7 @@ export const ContactSectionClient: React.FC<any> = ({
|
||||
className="flex flex-col"
|
||||
>
|
||||
<h2 className="font-heading text-3xl md:text-4xl font-extrabold text-foreground mb-8 text-balance">
|
||||
{isDe ? 'Schreiben Sie uns' : 'Napisz do nas'}
|
||||
{t.writeToUs}
|
||||
</h2>
|
||||
<div className="bg-card border border-border p-8 relative overflow-hidden flex-grow"
|
||||
style={{ boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)' }}
|
||||
|
||||
@@ -2,15 +2,38 @@ import React from 'react'
|
||||
import type { Page } from '../../payload-types'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import { MotionStamp } from '../../components/MotionStamp'
|
||||
import Image from 'next/image'
|
||||
import type { Media } from '../../payload-types'
|
||||
|
||||
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
|
||||
type Props = BaseProps & { whatsappNumber?: string | null }
|
||||
|
||||
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText, whatsappNumber }) => {
|
||||
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText, image, whatsappNumber }) => {
|
||||
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
|
||||
|
||||
const bgMedia = typeof image === 'object' && image !== null ? (image as Media) : null
|
||||
const imageUrl = bgMedia?.url
|
||||
const imageAlt = bgMedia?.alt || 'Tło Call to Action'
|
||||
|
||||
return (
|
||||
<MotionStamp id="final-cta" className="py-20 md:py-32 text-center" style={{ backgroundColor: 'var(--accent-color)', color: 'var(--bg-color)' }}>
|
||||
<div className="container-premium">
|
||||
<section id="final-cta" className="relative py-20 md:py-32 text-center overflow-hidden">
|
||||
{/* Background Image Layer */}
|
||||
<div className="absolute inset-0 z-0 bg-[var(--accent-color)]">
|
||||
{imageUrl && (
|
||||
<>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
className="object-cover object-center opacity-40 mix-blend-multiply"
|
||||
/>
|
||||
{/* Opcjonalny gradient/overlay dla lepszej czytelności */}
|
||||
<div className="absolute inset-0 bg-black/30" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<MotionStamp className="container-premium relative z-10">
|
||||
<div className="max-w-3xl mx-auto flex flex-col items-center">
|
||||
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight mb-6 text-balance text-[var(--bg-color)]">
|
||||
{title}
|
||||
@@ -36,7 +59,7 @@ export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, butto
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</MotionStamp>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,5 +7,11 @@ export const FinalCTA: Block = {
|
||||
{ name: 'title', type: 'text', required: true, localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'buttonText', type: 'text', required: true, localized: true },
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
image,
|
||||
floatingStat,
|
||||
whatsappNumber,
|
||||
}) => {
|
||||
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
|
||||
@@ -49,9 +48,6 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
const imageUrl = heroMedia?.url
|
||||
const imageAlt = heroMedia?.alt || 'Tło hero'
|
||||
|
||||
const statValue = floatingStat?.value || '20+'
|
||||
const statLabel = floatingStat?.label || 'Lat doświadczenia'
|
||||
|
||||
return (
|
||||
<motion.section
|
||||
initial="hidden"
|
||||
@@ -143,37 +139,7 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Floating Stat "Seal" (Bottom Right) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, rotate: -5 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6, ease: [0.34, 1.56, 0.64, 1] }}
|
||||
className="hidden lg:flex flex-col items-center justify-center bg-card border border-border/50 p-6 lg:p-8 shadow-2xl lg:-translate-y-4 max-w-[200px] aspect-square rounded-sm relative shrink-0"
|
||||
>
|
||||
{/* Dekoracyjna krawędź przypominająca stempel */}
|
||||
<div className="absolute inset-2 border border-border/20 pointer-events-none" />
|
||||
<span className="font-heading text-4xl lg:text-5xl font-extrabold tracking-tight text-accent mb-2 drop-shadow-sm relative z-10 text-center">
|
||||
{statValue}
|
||||
</span>
|
||||
<span className="text-sm uppercase tracking-widest text-foreground/80 font-medium text-center relative z-10 leading-snug">
|
||||
{statLabel}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Mobile version of the seal - more inline */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.6, ease: customEase }}
|
||||
className="flex lg:hidden items-center gap-4 bg-card/90 backdrop-blur-sm border border-border/50 shadow-md p-4 w-full mt-4"
|
||||
>
|
||||
<span className="font-heading text-3xl font-extrabold tracking-tight text-accent drop-shadow-sm">
|
||||
{statValue}
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-widest text-foreground/80 font-medium leading-snug">
|
||||
{statLabel}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,14 +15,7 @@ export const Hero: Block = {
|
||||
{ name: 'url', type: 'text' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'floatingStat',
|
||||
type: 'group',
|
||||
fields: [
|
||||
{ name: 'value', type: 'text', localized: true, defaultValue: '20+' },
|
||||
{ name: 'label', type: 'text', localized: true, defaultValue: 'Lat doświadczenia' },
|
||||
],
|
||||
},
|
||||
|
||||
{ name: 'image', type: 'upload', relationTo: 'media' },
|
||||
{ name: 'imageSecondary', type: 'upload', relationTo: 'media' },
|
||||
],
|
||||
|
||||
@@ -35,9 +35,11 @@ export const ServicesDetailedBlock: React.FC<Props> = ({
|
||||
}) => {
|
||||
const serviceList = services || []
|
||||
const pathname = usePathname()
|
||||
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
|
||||
const contactHref = isDe ? '/de/kontakt' : '/kontakt'
|
||||
const contactText = isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się'
|
||||
const localeSegment = pathname?.split('/').filter(Boolean)[0]
|
||||
const isDe = localeSegment === 'de'
|
||||
const isEn = localeSegment === 'en'
|
||||
const contactHref = isEn ? '/en/contact' : (isDe ? '/de/kontakt' : '/kontakt')
|
||||
const contactText = isEn ? 'Contact us' : (isDe ? 'Kontakt aufnehmen' : 'Skontaktuj się')
|
||||
|
||||
return (
|
||||
<section className="py-16 md:py-24 bg-background text-foreground">
|
||||
|
||||
@@ -11,7 +11,9 @@ type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'team' }>
|
||||
|
||||
export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, subtitle }) => {
|
||||
const pathname = usePathname()
|
||||
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
|
||||
const localeSegment = pathname?.split('/').filter(Boolean)[0]
|
||||
const isDe = localeSegment === 'de'
|
||||
const isEn = localeSegment === 'en'
|
||||
|
||||
// MOCK_MEMBERS usunięte na rzecz prawdziwych danych z CMS
|
||||
const members = originalMembers
|
||||
@@ -152,7 +154,7 @@ export const TeamBlock: React.FC<Props> = ({ members: originalMembers, title, su
|
||||
</p>
|
||||
|
||||
<div className="mt-auto flex items-center text-xs font-bold text-foreground/40 group-hover:text-accent transition-colors uppercase tracking-wider">
|
||||
<span>{isDe ? 'Mehr lesen' : 'Czytaj więcej'}</span>
|
||||
<span>{isEn ? 'Read more' : (isDe ? 'Mehr lesen' : 'Czytaj więcej')}</span>
|
||||
<svg className="w-4 h-4 ml-1.5 transform group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
url?: string | null;
|
||||
};
|
||||
floatingStat?: {
|
||||
value?: string | null;
|
||||
label?: string | null;
|
||||
};
|
||||
image?: (number | null) | Media;
|
||||
imageSecondary?: (number | null) | Media;
|
||||
id?: string | null;
|
||||
@@ -506,6 +502,7 @@ export interface Page {
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
buttonText: string;
|
||||
image?: (number | null) | Media;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'finalCta';
|
||||
@@ -1030,12 +1027,6 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
text?: T;
|
||||
url?: T;
|
||||
};
|
||||
floatingStat?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
};
|
||||
image?: T;
|
||||
imageSecondary?: T;
|
||||
id?: T;
|
||||
@@ -1214,6 +1205,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
subtitle?: T;
|
||||
buttonText?: T;
|
||||
image?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sqliteAdapter } from '@payloadcms/db-sqlite'
|
||||
import { postgresAdapter } from '@payloadcms/db-postgres'
|
||||
import { mcpPlugin } from '@payloadcms/plugin-mcp'
|
||||
import { seoPlugin } from '@payloadcms/plugin-seo'
|
||||
import { ipalKit, panelSmtpAdapter } from '@intecion/ipal-kit'
|
||||
@@ -33,11 +34,9 @@ export default buildConfig({
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
db: sqliteAdapter({
|
||||
client: {
|
||||
url: process.env.DATABASE_URI || '',
|
||||
},
|
||||
}),
|
||||
db: process.env.DATABASE_URI?.startsWith('postgres')
|
||||
? postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI }, push: false })
|
||||
: sqliteAdapter({ client: { url: process.env.DATABASE_URI || '' } }),
|
||||
sharp,
|
||||
localization: {
|
||||
locales: i18n.locales.map(l => l.code),
|
||||
|
||||
Reference in New Issue
Block a user