rework components

This commit is contained in:
Kuba Pyla
2026-05-20 02:58:48 +02:00
parent 9d4f32ecf0
commit 1aa0a96e95
27 changed files with 527 additions and 172 deletions
+52
View File
@@ -0,0 +1,52 @@
import {Mail, Phone} from 'lucide-react';
import {contact} from '@/config/contact';
const schedulerUrl = 'https://42min.us/kubapyla';
function phoneHref(phone: string) {
return `tel:${phone.replaceAll(' ', '')}`;
}
export function ContactSection() {
return (
<section id="kontakt" className="contactSection sectionBand">
<div className="container contactSection__grid">
<div className="contactSection__intro">
<div>
<p className="eyebrow">Kontakt</p>
<h2>Porozmawiajmy o konfiguratorze 3D</h2>
<p>Opisz katalog produktów, zakres wariantów albo pierwszy pomysł na demo. Wrócimy z konkretną ścieżką wdrożenia.</p>
</div>
<address className="contactSection__details">
<div>
<Mail size={22} />
<a href={`mailto:${contact.email}`}>{contact.email}</a>
</div>
<div>
<Phone size={22} />
<span>
{contact.phones.map(phone => (
<a href={phoneHref(phone)} key={phone}>
{phone}
</a>
))}
</span>
</div>
</address>
</div>
<div className="contactSection__schedulerWrap">
<iframe
className="contactSection__scheduler"
src={schedulerUrl}
title="Umów rozmowę z Ultifide"
loading="lazy"
referrerPolicy="strict-origin-when-cross-origin"
/>
<a className="contactSection__fallback" href={schedulerUrl} target="_blank" rel="noreferrer">
Umów rozmowę online w kalendarzu
</a>
</div>
</div>
</section>
);
}
+3 -5
View File
@@ -4,7 +4,7 @@ import {ArrowRight, DoorOpen} from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import {useRouter, useSearchParams} from 'next/navigation';
import {useMemo, useState} from 'react';
import {useMemo} from 'react';
import {DemoFrame} from './DemoFrame';
import type {DemoContent} from '@/types/content';
@@ -12,7 +12,7 @@ type DemoBrowserProps = {
demos: DemoContent[];
};
function isKnownDemo(slug: string | null, demos: DemoContent[]) {
function isKnownDemo(slug: string | null, demos: DemoContent[]): slug is DemoContent['slug'] {
return demos.some(demo => demo.slug === slug);
}
@@ -20,8 +20,7 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
const router = useRouter();
const searchParams = useSearchParams();
const requestedDemo = searchParams.get('demo');
const initialSlug = isKnownDemo(requestedDemo, demos) ? requestedDemo : demos[0]?.slug;
const [activeSlug, setActiveSlug] = useState(initialSlug);
const activeSlug = isKnownDemo(requestedDemo, demos) ? requestedDemo : demos[0].slug;
const activeDemo = useMemo(
() => demos.find(demo => demo.slug === activeSlug) ?? demos[0],
@@ -29,7 +28,6 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
);
function selectDemo(slug: DemoContent['slug']) {
setActiveSlug(slug);
router.replace(`/pl/dema?demo=${slug}`, {scroll: false});
}
+14 -5
View File
@@ -2,10 +2,11 @@ import {CheckCircle2, DoorOpen} from 'lucide-react';
import Image from 'next/image';
import {ButtonLink} from './ButtonLink';
import {DemoFrame} from './DemoFrame';
import {DemoSubnav} from './DemoSubnav';
import {Faq} from './Faq';
import {SectionHeader} from './SectionHeader';
import {Stats} from './Stats';
import {contact} from '@/config/contact';
import {orderedDemos} from '@/config/content';
import type {DemoContent} from '@/types/content';
type DemoPageProps = {
@@ -14,7 +15,14 @@ type DemoPageProps = {
export function DemoPage({demo}: DemoPageProps) {
return (
<main>
<>
<div className="demoSubnavBar">
<div className="container">
<DemoSubnav demos={orderedDemos} activeSlug={demo.slug} />
</div>
</div>
<main>
<section className="demoHero sectionBand sectionBand--light">
<div className="container demoHero__grid">
<div>
@@ -24,7 +32,7 @@ export function DemoPage({demo}: DemoPageProps) {
<p>{demo.description}</p>
<div className="buttonRow">
<ButtonLink href="#podglad">Zobacz demo</ButtonLink>
<ButtonLink href={contact.url} variant="secondary" isExternal>
<ButtonLink href="#kontakt" variant="secondary">
Porozmawiajmy
</ButtonLink>
</div>
@@ -70,7 +78,7 @@ export function DemoPage({demo}: DemoPageProps) {
<div className="container finalCta finalCta--dark">
<h2>{demo.cta.title}</h2>
<p>{demo.cta.description}</p>
<ButtonLink href={contact.url} isExternal>
<ButtonLink href="#kontakt">
{demo.cta.button}
</ButtonLink>
</div>
@@ -82,6 +90,7 @@ export function DemoPage({demo}: DemoPageProps) {
<Faq items={demo.faq} />
</div>
</section>
</main>
</main>
</>
);
}
+33
View File
@@ -0,0 +1,33 @@
'use client';
import Link from 'next/link';
import {usePathname, useSearchParams} from 'next/navigation';
import type {DemoContent} from '@/types/content';
type DemoSubnavProps = {
demos: DemoContent[];
activeSlug?: DemoContent['slug'];
hrefType?: 'page' | 'browser';
};
export function DemoSubnav({demos, activeSlug, hrefType = 'page'}: DemoSubnavProps) {
const pathname = usePathname();
const searchParams = useSearchParams();
const selectedSlug = activeSlug ?? searchParams.get('demo') ?? demos[0]?.slug;
return (
<nav className="demoSubnav" aria-label="Dema konfiguratorów">
{demos.map(demo => {
const isActive = selectedSlug === demo.slug || pathname === `/pl/${demo.slug}`;
const href = hrefType === 'browser' ? `/pl/dema?demo=${demo.slug}` : `/pl/${demo.slug}`;
return (
<Link className={isActive ? 'isActive' : undefined} href={href} key={demo.slug}>
<span>{demo.eyebrow}</span>
{demo.title}
</Link>
);
})}
</nav>
);
}
-18
View File
@@ -12,29 +12,11 @@ export function Footer() {
</Link>
<p>Copyright © 2026, Ultifide</p>
</div>
<nav className="footer__nav" aria-label="Nawigacja stopki">
<Link href="/pl">Platforma</Link>
<Link href="/pl/demo">Demo biurka</Link>
<Link href="/pl/demo-room">Demo wnętrz</Link>
<Link href="/pl/demo-door">Demo drzwi</Link>
<a href={contact.url}>Kontakt</a>
</nav>
<address className="footer__contact">
<strong>Contact Us</strong>
{contact.address.map(line => (
<span key={line}>{line}</span>
))}
<a href={`mailto:${contact.email}`}>{contact.email}</a>
<a href={`tel:${contact.phone.replaceAll(' ', '')}`}>{contact.phone}</a>
</address>
<div className="footer__social">
<a href={contact.linkedin} aria-label="LinkedIn Ultifide">
in
</a>
<a href={contact.facebook} aria-label="Facebook Ultifide">
f
</a>
</div>
</div>
</footer>
);
+24 -23
View File
@@ -4,15 +4,13 @@ import {Menu, X} from 'lucide-react';
import Link from 'next/link';
import {useEffect, useState} from 'react';
import {usePathname} from 'next/navigation';
import {contact} from '@/config/contact';
import {Logo} from './Logo';
const navItems = [
{id: 'dema', label: 'Dema', href: '/pl#dema'},
{id: 'platforma', label: 'Platforma', href: '/pl#platforma'},
{id: 'dema', label: 'Dema', href: '/pl/dema'},
{id: 'funkcje', label: 'Funkcje', href: '/pl#funkcje'},
{id: 'faq', label: 'FAQ', href: '/pl#faq'},
{id: 'kontakt', label: 'Kontakt', href: contact.url}
{id: 'faq', label: 'FAQ', href: '/pl#faq'}
];
export function Header() {
@@ -25,33 +23,36 @@ export function Header() {
return;
}
const sections = ['platforma', 'dema', 'funkcje', 'faq']
const sections = ['dema', 'platforma', 'funkcje', 'faq', 'kontakt']
.map(id => document.getElementById(id))
.filter((section): section is HTMLElement => Boolean(section));
const observer = new IntersectionObserver(
entries => {
const visibleEntry = entries
.filter(entry => entry.isIntersecting)
.sort((first, second) => second.intersectionRatio - first.intersectionRatio)[0];
function updateActiveSection() {
const activationLine = 120;
const currentSection = sections.reduce<HTMLElement | null>((active, section) => {
const sectionTop = section.getBoundingClientRect().top;
if (visibleEntry?.target.id) {
setActiveSection(visibleEntry.target.id);
if (sectionTop <= activationLine) {
return section;
}
},
{rootMargin: '-30% 0px -55% 0px', threshold: [0.1, 0.35, 0.6]}
);
sections.forEach(section => observer.observe(section));
return active;
}, null);
return () => observer.disconnect();
setActiveSection(currentSection?.id ?? 'platforma');
}
updateActiveSection();
window.addEventListener('scroll', updateActiveSection, {passive: true});
window.addEventListener('resize', updateActiveSection);
return () => {
window.removeEventListener('scroll', updateActiveSection);
window.removeEventListener('resize', updateActiveSection);
};
}, [pathname]);
function isActive(itemId: string) {
if (itemId === 'kontakt') {
return false;
}
if (pathname === '/pl') {
return itemId === activeSection;
}
@@ -76,8 +77,8 @@ export function Header() {
</a>
))}
</nav>
<a className="siteHeader__contact" href={contact.url}>
Porozmawiajmy
<a className={activeSection === 'kontakt' ? 'siteHeader__contact isActive' : 'siteHeader__contact'} href="#kontakt">
Kontakt
</a>
<button
className="siteHeader__menu"
-4
View File
@@ -44,10 +44,6 @@ export function HeroVisual() {
</div>
</div>
</div>
<div className="heroVisual__footer">
<span>3D preview</span>
<strong>API-ready</strong>
</div>
</div>
);
}