feat: complete POI Clean House website with Karcher theme, blog, interactive animations and full CMS configuration

This commit is contained in:
2026-08-26 02:49:44 +02:00
commit 9acdfe05d3
57 changed files with 11415 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
interface AnimateInProps {
children: React.ReactNode
delay?: number // in ms
direction?: 'up' | 'down' | 'left' | 'right' | 'none'
className?: string
threshold?: number
}
export function AnimateIn({
children,
delay = 0,
direction = 'up',
className = '',
threshold = 0.15,
}: AnimateInProps) {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.unobserve(entry.target)
}
},
{
threshold,
rootMargin: '0px 0px -40px 0px',
}
)
const currentRef = ref.current
if (currentRef) {
observer.observe(currentRef)
}
return () => {
if (currentRef) {
observer.unobserve(currentRef)
}
}
}, [threshold])
const getTransform = () => {
if (isVisible) return 'translate3d(0, 0, 0) scale(1)'
switch (direction) {
case 'up':
return 'translate3d(0, 32px, 0)'
case 'down':
return 'translate3d(0, -32px, 0)'
case 'left':
return 'translate3d(32px, 0, 0)'
case 'right':
return 'translate3d(-32px, 0, 0)'
case 'none':
return 'scale(0.96)'
default:
return 'translate3d(0, 32px, 0)'
}
}
return (
<div
ref={ref}
style={{
transitionDuration: '700ms',
transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
transitionDelay: `${delay}ms`,
transform: getTransform(),
opacity: isVisible ? 1 : 0,
}}
className={`transition-all ${className}`}
>
{children}
</div>
)
}
+145
View File
@@ -0,0 +1,145 @@
import React from 'react'
import { MapPin, Phone, Mail, ShieldCheck, Cookie } from 'lucide-react'
export interface FooterNavItem {
id?: string
label: string
href: string
}
interface FooterProps {
companyName?: string | null
footerTagline?: string | null
nip?: string | null
regon?: string | null
address?: string | null
city?: string | null
phone?: string | null
email?: string | null
items?: FooterNavItem[]
privacyHref?: string
privacyLabel?: string
cookieHref?: string
cookieLabel?: string
}
export function Footer({
companyName,
footerTagline,
nip,
regon,
address,
city,
phone,
email,
items,
privacyHref = '/polityka-prywatnosci',
privacyLabel = 'Polityka Prywatności',
cookieHref = '/polityka-cookies',
cookieLabel = 'Polityka Cookies',
}: FooterProps) {
return (
<footer className="bg-[#1f1f1f] text-[#d0d0d0] py-16 border-t-4 border-[#FFED00]">
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl">
<div className="grid grid-cols-1 md:grid-cols-4 gap-12">
{/* Company Info */}
<div className="md:col-span-2 space-y-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center bg-[#FFED00] text-[#1f1f1f] font-black text-xl">
POI
</div>
{companyName && (
<span className="text-xl font-black uppercase tracking-tight text-white">
{companyName}
</span>
)}
</div>
{footerTagline && (
<p className="text-xs sm:text-sm text-[#a0a0a0] leading-relaxed max-w-md">
{footerTagline}
</p>
)}
<div className="text-xs text-[#888888] font-mono space-y-1 pt-2 border-t border-[#333333]">
{nip && <div>NIP: <span className="text-[#e0e0e0] font-semibold">{nip}</span></div>}
{regon && <div>REGON: <span className="text-[#e0e0e0] font-semibold">{regon}</span></div>}
</div>
</div>
{/* Navigation Links */}
{items && items.length > 0 && (
<div className="space-y-4">
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
Nawigacja
</div>
<ul className="space-y-2.5 text-xs font-bold uppercase tracking-wider">
{items.map((item, idx) => (
<li key={item.id || idx}>
<a href={item.href} className="text-[#d0d0d0] hover:text-[#FFED00] transition-colors block py-0.5">
{item.label}
</a>
</li>
))}
</ul>
</div>
)}
{/* Contact Details */}
<div className="space-y-4">
<div className="text-xs font-black uppercase tracking-widest text-[#FFED00] border-b border-[#333333] pb-2">
Kontakt
</div>
<ul className="space-y-3 text-xs">
{phone && (
<li className="flex items-center gap-2.5">
<Phone className="h-4 w-4 text-[#FFED00] shrink-0" />
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="text-white hover:text-[#FFED00] font-mono font-black text-sm">
{phone}
</a>
</li>
)}
{email && (
<li className="flex items-center gap-2.5">
<Mail className="h-4 w-4 text-[#FFED00] shrink-0" />
<a href={`mailto:${email}`} className="text-[#d0d0d0] hover:text-white font-medium">
{email}
</a>
</li>
)}
{(address || city) && (
<li className="flex items-start gap-2.5">
<MapPin className="h-4 w-4 text-[#FFED00] shrink-0 mt-0.5" />
<span className="text-[#a0a0a0]">
{address}
{address && city ? ', ' : ''}
{city}
</span>
</li>
)}
</ul>
</div>
</div>
{/* Bottom Bar with Privacy & Cookie Links */}
<div className="mt-12 pt-8 border-t border-[#333333] flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-[#888888]">
<div>
© {new Date().getFullYear()} {companyName}. Wszelkie prawa zastrzeżone.
</div>
<div className="flex items-center gap-6 text-xs font-bold uppercase tracking-wider">
{privacyHref && (
<a href={privacyHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
<ShieldCheck className="h-3.5 w-3.5 text-[#FFED00]" />
<span>{privacyLabel}</span>
</a>
)}
{cookieHref && (
<a href={cookieHref} className="text-[#a0a0a0] hover:text-[#FFED00] transition-colors flex items-center gap-1.5">
<Cookie className="h-3.5 w-3.5 text-[#FFED00]" />
<span>{cookieLabel}</span>
</a>
)}
</div>
</div>
</div>
</footer>
)
}
+176
View File
@@ -0,0 +1,176 @@
'use client'
import React, { useState, useRef, useEffect } from 'react'
import { ArrowRight, Globe, ChevronDown } from 'lucide-react'
import { usePathname } from 'next/navigation'
export interface NavItem {
id?: string
label: string
href: string
}
interface HeaderProps {
companyName?: string | null
tagline?: string | null
items?: NavItem[]
phone?: string | null
homeHref?: string
currentLocale?: string
contactHref?: string
contactLabel?: string
}
export function Header({
companyName,
tagline,
items,
phone,
homeHref = '/',
currentLocale = 'pl',
contactHref = '/kontakt',
contactLabel = 'KONTAKT',
}: HeaderProps) {
const pathname = usePathname() || ''
const [langDropdownOpen, setLangDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
// Close dropdown on outside click
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setLangDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
// Function to switch locale in URL
const getSwitchedLocalePath = (targetLocale: string) => {
if (currentLocale === targetLocale) return pathname
if (pathname.startsWith(`/${currentLocale}`)) {
return pathname.replace(`/${currentLocale}`, `/${targetLocale}`)
}
return `/${targetLocale}${pathname}`
}
// Filter out duplicated "Kontakt" from menu items since it has a dedicated right CTA button
const filteredNavItems = items?.filter(
(item) => !item.label.toLowerCase().includes('kontakt')
)
const languages = [
{ code: 'pl', label: 'PL - Polski' },
{ code: 'en', label: 'UK - English' },
]
const activeLang = languages.find((l) => l.code === currentLocale) || languages[0]
return (
<header className="sticky top-0 z-50 w-full bg-white border-b-2 border-[#1f1f1f] shadow-xs">
{/* Top Black Utility Bar */}
<div className="bg-[#1f1f1f] text-white text-xs py-2 hidden md:block">
<div className="container mx-auto px-4 sm:px-6 lg:px-12 max-w-7xl flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="inline-block w-2.5 h-2.5 bg-[#FFED00]" />
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
{tagline || 'Profesjonalne Mycie i Renowacja Nawierzchni Brukowych'}
</span>
</div>
{phone && (
<div className="flex items-center gap-2">
<span className="font-bold uppercase tracking-wider text-[11px] text-[#e0e0e0]">
Infolinia / Bezpłatna wycena:
</span>
<a
href={`tel:${phone.replace(/\s+/g, '')}`}
className="font-black uppercase tracking-wider text-[11px] text-[#FFED00] hover:underline font-mono"
>
{phone}
</a>
</div>
)}
</div>
</div>
{/* 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">
<a href={homeHref} className="flex items-center gap-3.5">
<div className="flex h-11 w-11 items-center justify-center bg-[#FFED00] text-[#1f1f1f] font-black text-xl border-2 border-[#1f1f1f] shadow-xs">
POI
</div>
<div>
{companyName && (
<span className="text-xl font-black uppercase tracking-tight text-[#1f1f1f] block leading-none">
{companyName}
</span>
)}
<span className="text-[10px] uppercase tracking-widest text-[#666666] font-bold block mt-1">
Czyszczenie Kostki Brukowej
</span>
</div>
</a>
{/* Navigation Items (Without duplicated Kontakt) */}
{filteredNavItems && filteredNavItems.length > 0 && (
<nav className="hidden lg:flex items-center gap-8 text-xs font-black uppercase tracking-wider text-[#1f1f1f]">
{filteredNavItems.map((item, idx) => (
<a
key={item.id || idx}
href={item.href}
className="hover:text-[#1f1f1f] hover:border-b-2 hover:border-[#FFED00] transition-all py-2"
>
{item.label}
</a>
))}
</nav>
)}
<div className="flex items-center gap-4">
{/* Wysuwany przełącznik języków (Dropdown Language Switcher) */}
<div className="relative" ref={dropdownRef}>
<button
type="button"
onClick={() => setLangDropdownOpen(!langDropdownOpen)}
className="flex items-center gap-2 border-2 border-[#1f1f1f] bg-white px-3 py-2 text-xs font-black text-[#1f1f1f] hover:bg-[#f4f4f4] transition-colors cursor-pointer"
>
<Globe className="h-3.5 w-3.5 text-[#1f1f1f]" />
<span className="uppercase">{activeLang.code.toUpperCase()}</span>
<ChevronDown className={`h-3.5 w-3.5 transition-transform duration-200 ${langDropdownOpen ? 'rotate-180' : ''}`} />
</button>
{langDropdownOpen && (
<div className="absolute right-0 mt-1 w-36 bg-white border-2 border-[#1f1f1f] shadow-xl z-50 py-1">
{languages.map((lang) => (
<a
key={lang.code}
href={getSwitchedLocalePath(lang.code)}
onClick={() => setLangDropdownOpen(false)}
className={`flex items-center justify-between px-3 py-2 text-xs font-bold transition-colors ${
currentLocale === lang.code
? 'bg-[#1f1f1f] text-[#FFED00]'
: 'text-[#1f1f1f] hover:bg-[#FFED00]/20'
}`}
>
<span>{lang.label}</span>
{currentLocale === lang.code && <span className="w-1.5 h-1.5 bg-[#FFED00]" />}
</a>
))}
</div>
)}
</div>
{/* Right CTA Button: KONTAKT */}
<a
href={contactHref}
className="inline-flex items-center gap-2 bg-[#FFED00] hover:bg-[#fae100] text-[#1f1f1f] border-2 border-[#1f1f1f] px-6 py-3 text-xs font-black uppercase tracking-widest transition-colors shadow-xs"
>
<span>{contactLabel}</span>
<ArrowRight className="h-4 w-4" />
</a>
</div>
</div>
</header>
)
}