feat: update ipal-kit, integrate Media/SiteSettings and configure PL/DE languages

This commit is contained in:
2026-08-26 14:14:19 +02:00
parent cbb0ac4bfa
commit 59823dc04b
17 changed files with 363 additions and 77 deletions
+2
View File
@@ -40,3 +40,5 @@ dev.db*
# media uploads in local dev if any # media uploads in local dev if any
/public/media /public/media
/media
+1 -1
View File
@@ -28,7 +28,7 @@
"license": "ISC", "license": "ISC",
"packageManager": "[email protected]", "packageManager": "[email protected]",
"dependencies": { "dependencies": {
"@intecion/ipal-kit": "^1.0.17", "@intecion/ipal-kit": "^1.0.18",
"@payloadcms/db-postgres": "^3.88.0", "@payloadcms/db-postgres": "^3.88.0",
"@payloadcms/db-sqlite": "^3.88.0", "@payloadcms/db-sqlite": "^3.88.0",
"@payloadcms/next": "^3.88.0", "@payloadcms/next": "^3.88.0",
+5 -6
View File
@@ -19,8 +19,8 @@ importers:
.: .:
dependencies: dependencies:
'@intecion/ipal-kit': '@intecion/ipal-kit':
specifier: ^1.0.17 specifier: ^1.0.18
version: 1.0.17(e4f897fbda4e3a120e33294a7b805575) version: 1.0.18(e4f897fbda4e3a120e33294a7b805575)
'@payloadcms/db-postgres': '@payloadcms/db-postgres':
specifier: 3.88.0 specifier: 3.88.0
version: 3.88.0(@libsql/[email protected])([email protected]([email protected])([email protected])) version: 3.88.0(@libsql/[email protected])([email protected]([email protected])([email protected]))
@@ -946,9 +946,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@intecion/[email protected]7': '@intecion/[email protected]8':
resolution: {integrity: sha512-huzjbq4ntCFiTk4eYnGHmMADdKN5rzwBfYqmjuHE+tcxGqqrT8ffvzR3f2O0GiqMvJyMHd6Jw0qbu9a7J2tyzQ==, tarball: https://git.intecion.net/api/packages/IntecionSoftware/npm/%40intecion%2Fipal-kit/-/1.0.17/ipal-kit-1.0.17.tgz} resolution: {integrity: sha512-NTQEoWFuGcDBRPlnk1iEeVa75BvNkLmtXChLQE9B3epZ0vcrbTrFQgbXjkhgyc5LFFahZTQ/fEYLPOB/9ZDiuA==, tarball: https://git.intecion.net/api/packages/IntecionSoftware/npm/%40intecion%2Fipal-kit/-/1.0.18/ipal-kit-1.0.18.tgz}
engines: {node: ^18.20.2 || >=20.9.0, pnpm: ^9 || ^10 || ^11}
peerDependencies: peerDependencies:
'@payloadcms/next': 3.88.0 '@payloadcms/next': 3.88.0
'@payloadcms/plugin-form-builder': 3.88.0 '@payloadcms/plugin-form-builder': 3.88.0
@@ -3635,7 +3634,7 @@ snapshots:
'@img/[email protected]': '@img/[email protected]':
optional: true optional: true
'@intecion/[email protected]7(e4f897fbda4e3a120e33294a7b805575)': '@intecion/[email protected]8(e4f897fbda4e3a120e33294a7b805575)':
dependencies: dependencies:
'@payloadcms/next': 3.88.0(@types/[email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]) '@payloadcms/next': 3.88.0(@types/[email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
'@payloadcms/plugin-form-builder': 3.88.0(@types/[email protected])([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected]) '@payloadcms/plugin-form-builder': 3.88.0(@types/[email protected])([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected])([email protected]))([email protected]([email protected]))([email protected])([email protected])
+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 115" fill="none">
<!-- 3 Stars / Sparkles (Cyan) -->
<!-- Top Right Star -->
<path d="M85 6 C85 11 89 15 94 15 C89 15 85 19 85 24 C85 19 81 15 76 15 C81 15 85 11 85 6 Z" fill="#01aeee"/>
<!-- Middle Star -->
<path d="M72 17 C72 22 76 26 81 26 C76 26 72 30 72 35 C72 30 68 26 63 26 C68 26 72 22 72 17 Z" fill="#01aeee"/>
<!-- Far Right Star -->
<path d="M91 27 C91 30.5 94 33.5 97.5 33.5 C94 33.5 91 36.5 91 40 C91 36.5 88 33.5 84.5 33.5 C88 33.5 91 30.5 91 27 Z" fill="#01aeee"/>
<!-- Red Roof -->
<path d="M46 15 C47.5 13 48.5 13 50 15 L78 49 C79.5 50.8 78 54 75 54 L69 54 L69 44 C69 41.5 67 39.5 64.5 39.5 L59 39.5 L59 54 L21 54 C18 54 16.5 50.8 18 49 Z" fill="#ee1d23"/>
<!-- White Chimney Outline / Shape -->
<path d="M60 41 L67 41 L67 53 L60 53 Z" fill="#ffffff"/>
<!-- White Roof Trim / Gutter Separation -->
<path d="M12 55 L48 29 C49.2 28.1 50.8 28.1 52 29 L84 55 L78 61 L50 38 L20 61 Z" fill="#ffffff"/>
<!-- Blue House Base -->
<path d="M22 62 L50 40 L74 62 C79 66.5 80 73 78 79 C75 92 63 103 48 103 C33 103 21 92 18 79 C16 73 17 66.5 22 62 Z" fill="#01aeee"/>
<!-- White Arched Window -->
<path d="M40 73 C40 67.5 43.5 64 48 64 C52.5 64 56 67.5 56 73 L56 87 L40 87 Z" fill="#ffffff"/>
<!-- Window Cross Divider (Cyan) -->
<rect x="47" y="64" width="2" height="23" fill="#01aeee"/>
<rect x="40" y="74" width="16" height="2" fill="#01aeee"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 120" fill="none">
<!-- House Icon (Left) -->
<g transform="translate(10, 5) scale(0.95)">
<!-- 3 Stars / Sparkles (Cyan) -->
<path d="M85 6 C85 11 89 15 94 15 C89 15 85 19 85 24 C85 19 81 15 76 15 C81 15 85 11 85 6 Z" fill="#01aeee"/>
<path d="M72 17 C72 22 76 26 81 26 C76 26 72 30 72 35 C72 30 68 26 63 26 C68 26 72 22 72 17 Z" fill="#01aeee"/>
<path d="M91 27 C91 30.5 94 33.5 97.5 33.5 C94 33.5 91 36.5 91 40 C91 36.5 88 33.5 84.5 33.5 C88 33.5 91 30.5 91 27 Z" fill="#01aeee"/>
<!-- Red Roof -->
<path d="M46 15 C47.5 13 48.5 13 50 15 L78 49 C79.5 50.8 78 54 75 54 L69 54 L69 44 C69 41.5 67 39.5 64.5 39.5 L59 39.5 L59 54 L21 54 C18 54 16.5 50.8 18 49 Z" fill="#ee1d23"/>
<!-- White Chimney Outline / Shape -->
<path d="M60 41 L67 41 L67 53 L60 53 Z" fill="#ffffff"/>
<!-- White Roof Trim / Gutter Separation -->
<path d="M12 55 L48 29 C49.2 28.1 50.8 28.1 52 29 L84 55 L78 61 L50 38 L20 61 Z" fill="#ffffff"/>
<!-- Blue House Base -->
<path d="M22 62 L50 40 L74 62 C79 66.5 80 73 78 79 C75 92 63 103 48 103 C33 103 21 92 18 79 C16 73 17 66.5 22 62 Z" fill="#01aeee"/>
<!-- White Arched Window -->
<path d="M40 73 C40 67.5 43.5 64 48 64 C52.5 64 56 67.5 56 73 L56 87 L40 87 Z" fill="#ffffff"/>
<!-- Window Cross Divider (Cyan) -->
<rect x="47" y="64" width="2" height="23" fill="#01aeee"/>
<rect x="40" y="74" width="16" height="2" fill="#01aeee"/>
</g>
<!-- POI Text -->
<text x="122" y="75" font-family="system-ui, -apple-system, sans-serif" font-weight="900" font-style="italic" font-size="62" letter-spacing="1" fill="#231f20">POI</text>
<!-- CLEAN HOUSE Text -->
<text x="122" y="98" font-family="system-ui, -apple-system, sans-serif" font-weight="800" font-style="italic" font-size="16.5" letter-spacing="4" fill="#231f20">CLEAN HOUSE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 120" fill="none">
<!-- House Icon (Left) -->
<g transform="translate(10, 5) scale(0.95)">
<!-- 3 Stars / Sparkles (Cyan) -->
<path d="M85 6 C85 11 89 15 94 15 C89 15 85 19 85 24 C85 19 81 15 76 15 C81 15 85 11 85 6 Z" fill="#01aeee"/>
<path d="M72 17 C72 22 76 26 81 26 C76 26 72 30 72 35 C72 30 68 26 63 26 C68 26 72 22 72 17 Z" fill="#01aeee"/>
<path d="M91 27 C91 30.5 94 33.5 97.5 33.5 C94 33.5 91 36.5 91 40 C91 36.5 88 33.5 84.5 33.5 C88 33.5 91 30.5 91 27 Z" fill="#01aeee"/>
<!-- Red Roof -->
<path d="M46 15 C47.5 13 48.5 13 50 15 L78 49 C79.5 50.8 78 54 75 54 L69 54 L69 44 C69 41.5 67 39.5 64.5 39.5 L59 39.5 L59 54 L21 54 C18 54 16.5 50.8 18 49 Z" fill="#ee1d23"/>
<!-- White Chimney Outline / Shape -->
<path d="M60 41 L67 41 L67 53 L60 53 Z" fill="#ffffff"/>
<!-- White Roof Trim / Gutter Separation -->
<path d="M12 55 L48 29 C49.2 28.1 50.8 28.1 52 29 L84 55 L78 61 L50 38 L20 61 Z" fill="#ffffff"/>
<!-- Blue House Base -->
<path d="M22 62 L50 40 L74 62 C79 66.5 80 73 78 79 C75 92 63 103 48 103 C33 103 21 92 18 79 C16 73 17 66.5 22 62 Z" fill="#01aeee"/>
<!-- White Arched Window -->
<path d="M40 73 C40 67.5 43.5 64 48 64 C52.5 64 56 67.5 56 73 L56 87 L40 87 Z" fill="#ffffff"/>
<!-- Window Cross Divider (Cyan) -->
<rect x="47" y="64" width="2" height="23" fill="#01aeee"/>
<rect x="40" y="74" width="16" height="2" fill="#01aeee"/>
</g>
<!-- POI Text (White) -->
<text x="122" y="75" font-family="system-ui, -apple-system, sans-serif" font-weight="900" font-style="italic" font-size="62" letter-spacing="1" fill="#ffffff">POI</text>
<!-- CLEAN HOUSE Text (White) -->
<text x="122" y="98" font-family="system-ui, -apple-system, sans-serif" font-weight="800" font-style="italic" font-size="16.5" letter-spacing="4" fill="#ffffff">CLEAN HOUSE</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -5,6 +5,8 @@ import { createPageMetadata } from '@intecion/ipal-kit'
import { i18nConfig } from '@/i18n.config' import { i18nConfig } from '@/i18n.config'
import { blockRegistry } from '@/blocks/registry' import { blockRegistry } from '@/blocks/registry'
import { getCachedPayload, resolveRoute } from '@/lib/content' import { getCachedPayload, resolveRoute } from '@/lib/content'
import { getSettings } from '@/lib/payload'
import type { Media } from '@/payload-types'
interface PageProps { interface PageProps {
params: Promise<{ params: Promise<{
@@ -23,7 +25,31 @@ const pageMetadata = createPageMetadata({
export async function generateMetadata({ params }: PageProps): Promise<Metadata> { export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { locale, slug } = await params const { locale, slug } = await params
return pageMetadata({ payload: await getCachedPayload(), locale, slug: slug ?? [] }) const payload = await getCachedPayload()
const pageMeta = await pageMetadata({ payload, locale, slug: slug ?? [] })
if (!pageMeta.openGraph?.images || pageMeta.openGraph.images.length === 0) {
const settings = await getSettings(locale)
const shareImage =
(typeof settings?.defaultShareImage === 'object' && settings?.defaultShareImage && 'url' in settings.defaultShareImage
? (settings.defaultShareImage as Media)
: null) ||
(typeof settings?.logo === 'object' && settings?.logo && 'url' in settings.logo
? (settings.logo as Media)
: null)
if (shareImage?.url) {
return {
...pageMeta,
openGraph: {
...pageMeta.openGraph,
images: [{ url: shareImage.url }],
},
}
}
}
return pageMeta
} }
export default async function Page({ params, searchParams }: PageProps) { export default async function Page({ params, searchParams }: PageProps) {
+47 -1
View File
@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import type { Metadata } from 'next'
import { import {
getConsentTexts, getConsentTexts,
getAnalyticsConfig, getAnalyticsConfig,
@@ -18,13 +19,51 @@ import { Footer, type FooterNavItem } from '@/components/Footer'
import { CustomCookieBanner } from '@/components/CustomCookieBanner' import { CustomCookieBanner } from '@/components/CustomCookieBanner'
import '../styles.css' import '../styles.css'
import type { Company, Navigation, Page } from '@/payload-types' import type { Company, Media, Navigation, Page } from '@/payload-types'
interface LayoutProps { interface LayoutProps {
children: ReactNode children: ReactNode
params: Promise<{ locale: string }> params: Promise<{ locale: string }>
} }
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params
const settings = await getSettings(locale)
const favicon =
typeof settings?.favicon === 'object' && settings?.favicon && 'url' in settings.favicon
? (settings.favicon as Media)
: null
const faviconUrl = favicon?.url || undefined
const shareImage =
(typeof settings?.defaultShareImage === 'object' && settings?.defaultShareImage && 'url' in settings.defaultShareImage
? (settings.defaultShareImage as Media)
: null) ||
(typeof settings?.logo === 'object' && settings?.logo && 'url' in settings.logo
? (settings.logo as Media)
: null)
const shareImageUrl = shareImage?.url || undefined
return {
...(faviconUrl
? {
icons: {
icon: faviconUrl,
shortcut: faviconUrl,
apple: faviconUrl,
},
}
: {}),
...(shareImageUrl
? {
openGraph: {
images: [{ url: shareImageUrl }],
},
}
: {}),
}
}
function resolveNavItemHref(item: { page?: Page | number | string | null; url?: string | null }): string { function resolveNavItemHref(item: { page?: Page | number | string | null; url?: string | null }): string {
if (item.url) return item.url if (item.url) return item.url
if (item.page && typeof item.page === 'object' && 'slug' in item.page && item.page.slug) { if (item.page && typeof item.page === 'object' && 'slug' in item.page && item.page.slug) {
@@ -40,6 +79,10 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
const payload = await getCachedPayload() const payload = await getCachedPayload()
const settings = await getSettings(locale) const settings = await getSettings(locale)
const logo =
typeof settings?.logo === 'object' && settings?.logo && 'url' in settings.logo
? (settings.logo as Media)
: null
const privacyPage = (settings as { privacyPolicy?: unknown })?.privacyPolicy const privacyPage = (settings as { privacyPolicy?: unknown })?.privacyPolicy
const cookiePage = (settings as { cookiePolicy?: unknown })?.cookiePolicy const cookiePage = (settings as { cookiePolicy?: unknown })?.cookiePolicy
const homePage = (settings as { homepage?: unknown })?.homepage const homePage = (settings as { homepage?: unknown })?.homepage
@@ -154,6 +197,8 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
currentLocale={locale} currentLocale={locale}
contactHref={contactHref} contactHref={contactHref}
contactLabel={contactLabel} contactLabel={contactLabel}
logo={logo}
locales={i18nConfig.locales}
/> />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<Footer <Footer
@@ -179,6 +224,7 @@ export default async function LocaleLayout({ children, params }: LayoutProps) {
legalTitle={(navigation as { legalTitle?: string })?.legalTitle} legalTitle={(navigation as { legalTitle?: string })?.legalTitle}
creditLabel="Powered by NetScale.pl" creditLabel="Powered by NetScale.pl"
creditHref="https://netscale.pl" creditHref="https://netscale.pl"
logo={logo}
/> />
<CustomCookieBanner /> <CustomCookieBanner />
<CookieButton className="fixed bottom-3 left-3 z-40 bg-dark hover:bg-primary text-primary hover:text-dark border-2 border-primary p-2 transition-all shadow-lg cursor-pointer" /> <CookieButton className="fixed bottom-3 left-3 z-40 bg-dark hover:bg-primary text-primary hover:text-dark border-2 border-primary p-2 transition-all shadow-lg cursor-pointer" />
+4 -2
View File
@@ -35,12 +35,13 @@ export function HeroBlockComponent(props: HeroBlockProps) {
const heroImgSrc = const heroImgSrc =
(typeof backgroundImage === 'object' && backgroundImage?.url) || (typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) || (typeof backgroundImage === 'string' && backgroundImage) ||
'/images/paving-hero.webp' null
return ( return (
<div className="w-full"> <div className="w-full">
{/* Hero Banner Section with Next.js Image */} {/* Hero Banner Section with Next.js Image */}
<section className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center overflow-hidden"> <section className="relative w-full min-h-[calc(100vh-8rem)] max-h-[780px] flex items-center overflow-hidden bg-dark">
{heroImgSrc && (
<Image <Image
src={heroImgSrc} src={heroImgSrc}
alt={heading} alt={heading}
@@ -49,6 +50,7 @@ export function HeroBlockComponent(props: HeroBlockProps) {
sizes="100vw" sizes="100vw"
className="object-cover object-center z-0" className="object-cover object-center z-0"
/> />
)}
{/* Dark gradient overlay for text readability */} {/* Dark gradient overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-r from-dark/95 via-dark/70 to-black/30 z-[1]" /> <div className="absolute inset-0 bg-gradient-to-r from-dark/95 via-dark/70 to-black/30 z-[1]" />
+3 -3
View File
@@ -20,12 +20,12 @@ export function ProcessStepsBlockComponent(props: ProcessStepsBlockProps) {
const bgUrl = const bgUrl =
(typeof backgroundImage === 'object' && backgroundImage?.url) || (typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) || (typeof backgroundImage === 'string' && backgroundImage) ||
'/images/process-parallax.webp' null
return ( return (
<section <section
className="relative py-28 border-b-2 border-dark bg-fixed bg-center bg-cover overflow-hidden" className="relative py-28 border-b-2 border-dark bg-dark bg-fixed bg-center bg-cover overflow-hidden"
style={{ backgroundImage: `url('${bgUrl}')` }} style={bgUrl ? { backgroundImage: `url('${bgUrl}')` } : undefined}
> >
{/* Dark overlay for contrast */} {/* Dark overlay for contrast */}
<div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" /> <div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" />
+3 -3
View File
@@ -20,12 +20,12 @@ export function TechEquipmentBlockComponent(props: TechEquipmentBlockProps) {
const bgUrl = const bgUrl =
(typeof backgroundImage === 'object' && backgroundImage?.url) || (typeof backgroundImage === 'object' && backgroundImage?.url) ||
(typeof backgroundImage === 'string' && backgroundImage) || (typeof backgroundImage === 'string' && backgroundImage) ||
'/images/paving-parallax.webp' null
return ( return (
<section <section
className="relative py-28 border-b-2 border-dark bg-fixed bg-center bg-cover overflow-hidden" className="relative py-28 border-b-2 border-dark bg-dark bg-fixed bg-center bg-cover overflow-hidden"
style={{ backgroundImage: `url('${bgUrl}')` }} style={bgUrl ? { backgroundImage: `url('${bgUrl}')` } : undefined}
> >
{/* Dark overlay for contrast */} {/* Dark overlay for contrast */}
<div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" /> <div className="absolute inset-0 bg-dark/85 backdrop-blur-[1px]" />
+9 -1
View File
@@ -2,6 +2,8 @@ import React from 'react'
import { Phone, Mail, ShieldCheck, Cookie, FileText } from 'lucide-react' import { Phone, Mail, ShieldCheck, Cookie, FileText } from 'lucide-react'
import { Logo } from '@/components/Logo' import { Logo } from '@/components/Logo'
import type { Media } from '@/payload-types'
export interface FooterNavItem { export interface FooterNavItem {
id?: string id?: string
label: string label: string
@@ -31,6 +33,7 @@ interface FooterProps {
legalTitle?: string | null legalTitle?: string | null
creditLabel?: string | null creditLabel?: string | null
creditHref?: string | null creditHref?: string | null
logo?: Media | { url?: string | null; alt?: string | null } | null
} }
export function Footer({ export function Footer({
@@ -56,6 +59,7 @@ export function Footer({
legalTitle, legalTitle,
creditLabel, creditLabel,
creditHref, creditHref,
logo,
}: FooterProps) { }: FooterProps) {
return ( return (
<footer className="bg-dark text-[#d0d0d0] pt-14 pb-8 border-t-4 border-primary"> <footer className="bg-dark text-[#d0d0d0] pt-14 pb-8 border-t-4 border-primary">
@@ -64,7 +68,11 @@ export function Footer({
{/* 1. Company Info */} {/* 1. Company Info */}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Logo variant="dark" className="h-12 w-auto" alt={companyName || undefined} /> {logo ? (
<Logo className="h-12 w-auto" alt={companyName || undefined} media={logo} />
) : companyName ? (
<span className="font-black text-xl tracking-tight text-white uppercase">{companyName}</span>
) : null}
</div> </div>
{footerTagline && ( {footerTagline && (
<p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed"> <p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed">
+15 -5
View File
@@ -5,6 +5,8 @@ import { ArrowRight, Globe, ChevronDown } from 'lucide-react'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import { Logo } from '@/components/Logo' import { Logo } from '@/components/Logo'
import type { Media } from '@/payload-types'
export interface NavItem { export interface NavItem {
id?: string id?: string
label: string label: string
@@ -21,6 +23,8 @@ interface HeaderProps {
contactHref?: string | null contactHref?: string | null
contactLabel?: string | null contactLabel?: string | null
phoneLabel?: string | null phoneLabel?: string | null
logo?: Media | { url?: string | null; alt?: string | null } | null
locales?: readonly { code: string; label: string }[] | { code: string; label: string }[]
} }
export function Header({ export function Header({
@@ -33,6 +37,8 @@ export function Header({
contactHref, contactHref,
contactLabel, contactLabel,
phoneLabel, phoneLabel,
logo,
locales,
}: HeaderProps) { }: HeaderProps) {
const pathname = usePathname() || '' const pathname = usePathname() || ''
const [langDropdownOpen, setLangDropdownOpen] = useState(false) const [langDropdownOpen, setLangDropdownOpen] = useState(false)
@@ -63,10 +69,10 @@ export function Header({
(item) => !contactLabel || item.label.trim().toLowerCase() !== contactLabel.trim().toLowerCase() (item) => !contactLabel || item.label.trim().toLowerCase() !== contactLabel.trim().toLowerCase()
) )
const languages = ['pl', 'en'].map((code) => ({ const languages =
code, locales && locales.length > 0
label: code.toUpperCase(), ? locales
})) : [{ code: currentLocale, label: currentLocale.toUpperCase() }]
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0] const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
@@ -106,7 +112,11 @@ export function Header({
{/* Main Navigation Bar */} {/* Main Navigation Bar */}
<div className="container mx-auto flex h-20 items-center justify-between px-4 sm:px-6 lg:px-12 max-w-7xl"> <div className="container mx-auto flex h-20 items-center justify-between px-4 sm:px-6 lg:px-12 max-w-7xl">
<a href={homeHref} className="flex items-center gap-3"> <a href={homeHref} className="flex items-center gap-3">
<Logo className="h-12 w-auto" alt={companyName || undefined} /> {logo ? (
<Logo className="h-12 w-auto" alt={companyName || undefined} media={logo} />
) : companyName ? (
<span className="font-black text-xl tracking-tight text-dark uppercase">{companyName}</span>
) : null}
</a> </a>
{/* Navigation Items */} {/* Navigation Items */}
+29 -6
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import Image from 'next/image' import Image from 'next/image'
import type { Media } from '@/payload-types'
interface LogoProps { interface LogoProps {
className?: string className?: string
@@ -8,13 +9,29 @@ interface LogoProps {
width?: number width?: number
priority?: boolean priority?: boolean
alt?: string alt?: string
media?: Media | { url?: string | null; alt?: string | null } | null
src?: string | null
} }
export function LogoIcon({ className = 'h-11 w-auto', alt = '', ...props }: { className?: string; alt?: string }) { export function LogoIcon({
className = 'h-11 w-auto',
alt = '',
media,
...props
}: {
className?: string
alt?: string
media?: Media | { url?: string | null; alt?: string | null } | null
}) {
const src = (typeof media === 'object' && media?.url) || null
const mediaAlt = (typeof media === 'object' && media?.alt) || alt || ''
if (!src) return null
return ( return (
<Image <Image
src="/images/logo-icon.webp" src={src}
alt={alt} alt={mediaAlt}
width={140} width={140}
height={140} height={140}
priority priority
@@ -26,17 +43,21 @@ export function LogoIcon({ className = 'h-11 w-auto', alt = '', ...props }: { cl
export function Logo({ export function Logo({
className = 'h-12 w-auto', className = 'h-12 w-auto',
variant = 'light',
priority = true, priority = true,
alt = '', alt = '',
media,
src: customSrc,
...props ...props
}: LogoProps) { }: LogoProps) {
const src = variant === 'dark' ? '/images/logo-white.webp' : '/images/logo.webp' const src = customSrc || (typeof media === 'object' && media?.url) || null
const mediaAlt = (typeof media === 'object' && media?.alt) || alt || ''
if (!src) return null
return ( return (
<Image <Image
src={src} src={src}
alt={alt} alt={mediaAlt}
width={240} width={240}
height={84} height={84}
priority={priority} priority={priority}
@@ -45,3 +66,5 @@ export function Logo({
/> />
) )
} }
+2 -1
View File
@@ -4,6 +4,7 @@ export const i18nConfig = {
defaultLocale: 'pl', defaultLocale: 'pl',
locales: [ locales: [
{ code: 'pl', label: 'PL' }, { code: 'pl', label: 'PL' },
{ code: 'en', label: 'UK' }, { code: 'de', label: 'DE' },
], ],
} as const satisfies I18nConfig } as const satisfies I18nConfig
+3 -1
View File
@@ -1,7 +1,9 @@
import { cache } from 'react' import { cache } from 'react'
import { getSiteSettings } from '@intecion/ipal-kit' import { getSiteSettings } from '@intecion/ipal-kit'
import type { SiteSetting } from '@/payload-types'
import { getCachedPayload } from './content' import { getCachedPayload } from './content'
export const getSettings = cache(async (locale: string) => export const getSettings = cache(async (locale: string) =>
getSiteSettings(await getCachedPayload(), { locale: locale as never, depth: 2 }), getSiteSettings<SiteSetting>(await getCachedPayload(), { locale: locale as never, depth: 2 }),
) )
+111 -37
View File
@@ -1,3 +1,5 @@
import path from 'path'
import fs from 'fs'
import type { Payload } from 'payload' import type { Payload } from 'payload'
export async function seed(payload: Payload) { export async function seed(payload: Payload) {
@@ -130,17 +132,94 @@ export async function seed(payload: Payload) {
contactForm = { id: Number(existingForms.docs[0].id) } contactForm = { id: Number(existingForms.docs[0].id) }
} }
// 4. Sprawdź czy strony już istnieją // 4. Seedowanie mediów (logo oraz favicon)
const existingPages = await payload.find({ let logoMediaId: number | null = null
collection: 'pages', let faviconMediaId: number | null = null
const existingLogo = await payload.find({
collection: 'media',
locale: 'pl',
where: {
or: [
{ filename: { contains: 'logo-poi' } },
{ alt: { equals: 'Logo POI Clean House' } },
],
},
limit: 1, limit: 1,
}) })
if (existingPages.totalDocs > 0) { if (existingLogo.totalDocs === 0) {
payload.logger.info('Strony już istnieją w bazie. Pomijam seedowanie stron.') payload.logger.info('Tworzę logo w kolekcji Media...')
return const filePath = path.resolve(process.cwd(), 'public/images/logo-poi.svg')
if (fs.existsSync(filePath)) {
const createdLogo = await payload.create({
collection: 'media',
locale: 'pl',
filePath,
data: {
alt: 'Logo POI Clean House',
},
})
logoMediaId = Number(createdLogo.id)
}
} else {
logoMediaId = Number(existingLogo.docs[0].id)
} }
const existingFavicon = await payload.find({
collection: 'media',
locale: 'pl',
where: {
or: [
{ filename: { contains: 'favicon-poi' } },
{ alt: { equals: 'Favicon icon POI Clean House' } },
],
},
limit: 1,
})
if (existingFavicon.totalDocs === 0) {
payload.logger.info('Tworzę favicon w kolekcji Media...')
const filePath = path.resolve(process.cwd(), 'public/images/favicon-poi.svg')
if (fs.existsSync(filePath)) {
const createdFavicon = await payload.create({
collection: 'media',
locale: 'pl',
filePath,
data: {
alt: 'Favicon icon POI Clean House',
},
})
faviconMediaId = Number(createdFavicon.id)
}
} else {
faviconMediaId = Number(existingFavicon.docs[0].id)
}
// 5. Sprawdź czy strony już istnieją
const existingPages = await payload.find({
collection: 'pages',
limit: 20,
})
let homePageId: number | null = null
let privacyPageId: number | null = null
let cookiePageId: number | null = null
let realizacjePageId: number | null = null
let blogPageId: number | null = null
let kontaktPageId: number | null = null
if (existingPages.totalDocs > 0) {
payload.logger.info('Strony już istnieją w bazie. Odczytuję ID stron...')
for (const page of existingPages.docs) {
if (page.slug === 'home') homePageId = Number(page.id)
if (page.slug === 'polityka-prywatnosci') privacyPageId = Number(page.id)
if (page.slug === 'polityka-cookies') cookiePageId = Number(page.id)
if (page.slug === 'realizacje') realizacjePageId = Number(page.id)
if (page.slug === 'blog') blogPageId = Number(page.id)
if (page.slug === 'kontakt') kontaktPageId = Number(page.id)
}
} else {
payload.logger.info('Tworzę strony serwisu...') payload.logger.info('Tworzę strony serwisu...')
// A. Strona Główna // A. Strona Główna
@@ -578,24 +657,36 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
}, },
}) })
// 4. Przypisanie stron systemowych do globala site-settings homePageId = Number(homePage.id)
payload.logger.info('Konfiguruję strony systemowe w SiteSettings...') privacyPageId = Number(privacyPage.id)
cookiePageId = Number(cookiePage.id)
realizacjePageId = Number(realizacjePage.id)
blogPageId = Number(blogPage.id)
kontaktPageId = Number(kontaktPage.id)
}
// 6. Przypisanie stron systemowych oraz mediów do globala site-settings
payload.logger.info('Konfiguruję SiteSettings (logo, favicon, defaultShareImage, strony systemowe)...')
try { try {
await payload.updateGlobal({ await payload.updateGlobal({
slug: 'site-settings', slug: 'site-settings',
locale: 'pl', locale: 'pl',
data: { data: {
siteName: 'POI Clean House', siteName: 'POI Clean House',
homepage: homePage.id, titleOrder: 'page-first',
privacyPolicy: privacyPage.id, titleSeparator: '|',
cookiePolicy: cookiePage.id, ...(logoMediaId ? { logo: logoMediaId, defaultShareImage: logoMediaId } : {}),
...(faviconMediaId ? { favicon: faviconMediaId } : {}),
...(homePageId ? { homepage: homePageId } : {}),
...(privacyPageId ? { privacyPolicy: privacyPageId } : {}),
...(cookiePageId ? { cookiePolicy: cookiePageId } : {}),
} as never, } as never,
}) })
} catch (err) { } catch (err) {
payload.logger.warn(`Nie udało się automatycznie zaktualizować siteSettings: ${err}`) payload.logger.warn(`Nie udało się automatycznie zaktualizować siteSettings: ${err}`)
} }
// 5. Przypisanie menu do globala navigation // 7. Przypisanie menu do globala navigation
payload.logger.info('Konfiguruję menu w Navigation...') payload.logger.info('Konfiguruję menu w Navigation...')
try { try {
await payload.updateGlobal({ await payload.updateGlobal({
@@ -611,42 +702,24 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
label: 'Cennik', label: 'Cennik',
url: '#cennik', url: '#cennik',
}, },
{ ...(realizacjePageId ? [{ label: 'Realizacje', page: realizacjePageId }] : []),
label: 'Realizacje', ...(blogPageId ? [{ label: 'Blog', page: blogPageId }] : []),
page: realizacjePage.id,
},
{
label: 'Blog',
page: blogPage.id,
},
], ],
ctaButton: { ctaButton: {
label: 'Kontakt', label: 'Kontakt',
page: kontaktPage.id, ...(kontaktPageId ? { page: kontaktPageId } : { url: '/kontakt' }),
}, },
footerTagline: footerTagline:
'Wyspecjalizowane usługi mycia gorącowodnego, usuwania mchu, odplamiania oleju, spoinowania i impregnacji hydrofobowej nawierzchni brukowych.', 'Wyspecjalizowane usługi mycia gorącowodnego, usuwania mchu, odplamiania oleju, spoinowania i impregnacji hydrofobowej nawierzchni brukowych.',
footerItems: [ footerItems: [
{ ...(homePageId ? [{ label: 'Strona główna', page: homePageId }] : []),
label: 'Strona główna',
page: homePage.id,
},
{ {
label: 'Cennik usług', label: 'Cennik usług',
url: '#cennik', url: '#cennik',
}, },
{ ...(realizacjePageId ? [{ label: 'Galeria realizacji', page: realizacjePageId }] : []),
label: 'Galeria realizacji', ...(blogPageId ? [{ label: 'Poradnik i Blog', page: blogPageId }] : []),
page: realizacjePage.id, ...(kontaktPageId ? [{ label: 'Kontakt i wycena', page: kontaktPageId }] : []),
},
{
label: 'Poradnik i Blog',
page: blogPage.id,
},
{
label: 'Kontakt i wycena',
page: kontaktPage.id,
},
], ],
} as never, } as never,
}) })
@@ -656,3 +729,4 @@ W każdej chwili możesz zmienić swoje preferencje dotyczące plików cookies k
payload.logger.info('Seedowanie zakończone pomyślnie!') payload.logger.info('Seedowanie zakończone pomyślnie!')
} }