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
+8 -8
View File
@@ -8,7 +8,7 @@
}, },
"navigation": { "navigation": {
"platform": "Platforma", "platform": "Platforma",
"demos": "Dema", "demos": "Demo",
"features": "Funkcje", "features": "Funkcje",
"faq": "FAQ", "faq": "FAQ",
"contact": "Kontakt", "contact": "Kontakt",
@@ -79,9 +79,9 @@
}, },
"demos": { "demos": {
"desk": { "desk": {
"slug": "demo", "slug": "demo-biurko",
"eyebrow": "Demo produktowe", "eyebrow": "Demo produktowe",
"title": "Konfigurator 3D, który sprzedaje za Ciebie", "title": "Konfigurator - Biurko",
"intro": "Pozwalaj klientom tworzyć własne biurka regulowane w czasie rzeczywistym.", "intro": "Pozwalaj klientom tworzyć własne biurka regulowane w czasie rzeczywistym.",
"description": "Klient może zobaczyć swoje biurko w naturalnym oświetleniu, obrócić je o 360 stopni, powiększyć detale, zmienić dodatki i sprawdzić jak całość będzie wyglądała w jego przestrzeni.", "description": "Klient może zobaczyć swoje biurko w naturalnym oświetleniu, obrócić je o 360 stopni, powiększyć detale, zmienić dodatki i sprawdzić jak całość będzie wyglądała w jego przestrzeni.",
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/b64b9dddfb838bfafea9da0ee5c04c68.png?width=1000&height=1000", "imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/b64b9dddfb838bfafea9da0ee5c04c68.png?width=1000&height=1000",
@@ -117,9 +117,9 @@
} }
}, },
"room": { "room": {
"slug": "demo-room", "slug": "demo-pokoj",
"eyebrow": "Demo wnętrzarskie", "eyebrow": "Demo wnętrzarskie",
"title": "Konfigurator 3D, który sprzedaje całe wnętrza", "title": "Konfigurator - Wnętrze",
"intro": "Twórz i prezentuj kompletne aranżacje pomieszczeń oraz meble w interaktywnym środowisku 3D.", "intro": "Twórz i prezentuj kompletne aranżacje pomieszczeń oraz meble w interaktywnym środowisku 3D.",
"description": "Klient może zaprojektować pokój od podstaw, ustawić meble, zmienić kolory ścian, materiały, dodatki i zobaczyć wszystko w realistycznym oświetleniu.", "description": "Klient może zaprojektować pokój od podstaw, ustawić meble, zmienić kolory ścian, materiały, dodatki i zobaczyć wszystko w realistycznym oświetleniu.",
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/e78aac1388ff07761422e12272690878.png?width=1000&height=1000", "imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/e78aac1388ff07761422e12272690878.png?width=1000&height=1000",
@@ -155,12 +155,12 @@
} }
}, },
"door": { "door": {
"slug": "demo-door", "slug": "demo-drzwi",
"eyebrow": "Demo produktowe", "eyebrow": "Demo produktowe",
"title": "Konfigurator 3D drzwi dla producentów i sprzedawców", "title": "Konfigurator - Drzwi",
"intro": "Pozwól klientom dobrać model, kolor, przeszklenie, klamkę i detale drzwi w interaktywnym podglądzie 3D.", "intro": "Pozwól klientom dobrać model, kolor, przeszklenie, klamkę i detale drzwi w interaktywnym podglądzie 3D.",
"description": "Konfigurator drzwi ułatwia sprzedaż produktów z wieloma wariantami. Klient widzi efekt wyborów od razu, a konfiguracja może zostać przekazana do koszyka, zapytania ofertowego albo systemu sprzedaży.", "description": "Konfigurator drzwi ułatwia sprzedaż produktów z wieloma wariantami. Klient widzi efekt wyborów od razu, a konfiguracja może zostać przekazana do koszyka, zapytania ofertowego albo systemu sprzedaży.",
"imageUrl": "/demo-door-preview.svg", "imageUrl": "/demo-door-preview.png",
"openLabel": "Otwórz konfigurator drzwi", "openLabel": "Otwórz konfigurator drzwi",
"benefitsIntro": "Dla firm, które sprzedają drzwi, fronty, zabudowy i produkty wymagające precyzyjnego dopasowania wariantów.", "benefitsIntro": "Dla firm, które sprzedają drzwi, fronty, zabudowy i produkty wymagające precyzyjnego dopasowania wariantów.",
"stats": [ "stats": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

-31
View File
@@ -1,31 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 420" fill="none">
<rect width="640" height="420" rx="28" fill="#F9FAFE"/>
<path d="M78 128c0-24 19-43 43-43h398c24 0 43 19 43 43v214H78V128Z" fill="#fff" stroke="#001C44" stroke-width="10"/>
<path d="M78 128c0-24 19-43 43-43h398c24 0 43 19 43 43v34H78v-34Z" fill="#42A6FF" opacity=".72"/>
<circle cx="120" cy="124" r="9" fill="#F9FAFE"/>
<circle cx="151" cy="124" r="9" fill="#F9FAFE"/>
<circle cx="182" cy="124" r="9" fill="#F9FAFE"/>
<rect x="112" y="194" width="118" height="112" rx="14" fill="#E3F3FF"/>
<path d="M145 281V211l53 13v70l-53-13Z" fill="#fff" stroke="#001C44" stroke-width="8" stroke-linejoin="round"/>
<path d="M145 211l38-16 53 13-38 16-53-13Z" fill="#42A6FF" stroke="#001C44" stroke-width="8" stroke-linejoin="round"/>
<path d="M198 224l38-16v70l-38 16v-70Z" fill="#DDF1FF" stroke="#001C44" stroke-width="8" stroke-linejoin="round"/>
<circle cx="186" cy="253" r="5" fill="#001C44"/>
<rect x="266" y="194" width="248" height="36" rx="18" fill="#DDF1FF"/>
<rect x="266" y="251" width="204" height="22" rx="11" fill="#B9DFFF"/>
<rect x="266" y="292" width="162" height="22" rx="11" fill="#B9DFFF"/>
<circle cx="499" cy="292" r="43" fill="#42A6FF"/>
<path d="M499 249a43 43 0 0 1 39 61l-39-18v-43Z" fill="#FF9F2F"/>
<rect x="114" y="332" width="404" height="10" rx="5" fill="#001C44" opacity=".18"/>
<path d="M413 344c0-36 29-65 65-65s65 29 65 65v31H413v-31Z" fill="#42A6FF" opacity=".22"/>
<path d="M491 238c-6-17-1-35 12-45 18-14 45-8 57 14 8 14 8 32 1 46l-16 31-26-25c-13 1-24-7-28-21Z" fill="#001C44"/>
<path d="M456 384v-72c0-16 13-29 29-29h22c16 0 29 13 29 29v72" fill="#2B7BB9"/>
<path d="M455 328l-36 33" stroke="#2B7BB9" stroke-width="18" stroke-linecap="round"/>
<path d="M535 327l34 30" stroke="#2B7BB9" stroke-width="18" stroke-linecap="round"/>
<path d="M456 384h31l-3-62h-28v62Z" fill="#001C44"/>
<path d="M505 384h31v-62h-28l-3 62Z" fill="#001C44"/>
<circle cx="516" cy="230" r="22" fill="#FFCF9D"/>
<rect x="71" y="284" width="104" height="104" rx="18" fill="#fff" stroke="#001C44" stroke-width="8"/>
<path d="M101 349v-47l44 22-44 25Z" fill="#42A6FF" opacity=".28" stroke="#001C44" stroke-width="7" stroke-linejoin="round"/>
<path d="M101 302l44 22 16-28-44-22-16 28Z" fill="#F9FAFE" stroke="#001C44" stroke-width="7" stroke-linejoin="round"/>
<path d="M145 324l16-28v48l-16 28v-48Z" fill="#42A6FF" opacity=".5" stroke="#001C44" stroke-width="7" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

+4 -4
View File
@@ -1,7 +1,8 @@
import type {Metadata} from 'next'; import type {Metadata} from 'next';
import {DemoBrowser} from '@/components/DemoBrowser'; import {DemoBrowser} from '@/components/DemoBrowser';
import {DemoSubnav} from '@/components/DemoSubnav';
import {SectionHeader} from '@/components/SectionHeader'; import {SectionHeader} from '@/components/SectionHeader';
import {demoContent} from '@/config/content'; import {orderedDemos} from '@/config/content';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Dema konfiguratorów 3D | Ultifide', title: 'Dema konfiguratorów 3D | Ultifide',
@@ -9,8 +10,6 @@ export const metadata: Metadata = {
}; };
export default function DemosPage() { export default function DemosPage() {
const demos = [demoContent.desk, demoContent.room, demoContent.door];
return ( return (
<main> <main>
<section className="sectionBand sectionBand--light demosPage"> <section className="sectionBand sectionBand--light demosPage">
@@ -20,7 +19,8 @@ export default function DemosPage() {
title="Sprawdź konfiguratory 3D w jednym miejscu" title="Sprawdź konfiguratory 3D w jednym miejscu"
description="Wybierz demo z listy, przetestuj je od razu w podglądzie, a potem przejdź do strony z opisem zastosowania i korzyści." description="Wybierz demo z listy, przetestuj je od razu w podglądzie, a potem przejdź do strony z opisem zastosowania i korzyści."
/> />
<DemoBrowser demos={demos} /> <DemoSubnav demos={orderedDemos} hrefType="browser" />
<DemoBrowser demos={orderedDemos} />
</div> </div>
</section> </section>
</main> </main>
+14
View File
@@ -0,0 +1,14 @@
import type {Metadata} from 'next';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.deskTitle,
description:
'Poznaj konfigurator 3D, który zwiększa konwersję i pozwala klientom tworzyć własne biurka regulowane w czasie rzeczywistym.'
};
export default function DeskDemoPage() {
return <DemoPage demo={demoContent.desk} />;
}
+2 -11
View File
@@ -1,14 +1,5 @@
import type {Metadata} from 'next'; import {redirect} from 'next/navigation';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.doorTitle,
description:
'Poznaj konfigurator 3D drzwi, który pozwala klientom dobrać model, kolor, przeszklenie, klamkę i detale w czasie rzeczywistym.'
};
export default function DoorDemoPage() { export default function DoorDemoPage() {
return <DemoPage demo={demoContent.door} />; redirect('/pl/demo-drzwi');
} }
+14
View File
@@ -0,0 +1,14 @@
import type {Metadata} from 'next';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.doorTitle,
description:
'Poznaj konfigurator 3D drzwi, który pozwala klientom dobrać model, kolor, przeszklenie, klamkę i detale w czasie rzeczywistym.'
};
export default function DoorDemoPage() {
return <DemoPage demo={demoContent.door} />;
}
+14
View File
@@ -0,0 +1,14 @@
import type {Metadata} from 'next';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.roomTitle,
description:
'Poznaj konfigurator 3D, który pozwala klientom projektować całe pomieszczenia i zestawy mebli w czasie rzeczywistym.'
};
export default function RoomDemoPage() {
return <DemoPage demo={demoContent.room} />;
}
+2 -11
View File
@@ -1,14 +1,5 @@
import type {Metadata} from 'next'; import {redirect} from 'next/navigation';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.roomTitle,
description:
'Poznaj konfigurator 3D, który pozwala klientom projektować całe pomieszczenia i zestawy mebli w czasie rzeczywistym.'
};
export default function RoomDemoPage() { export default function RoomDemoPage() {
return <DemoPage demo={demoContent.room} />; redirect('/pl/demo-pokoj');
} }
+2 -11
View File
@@ -1,14 +1,5 @@
import type {Metadata} from 'next'; import {redirect} from 'next/navigation';
import {DemoPage} from '@/components/DemoPage';
import {demoContent} from '@/config/content';
import messages from '../../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.deskTitle,
description:
'Poznaj konfigurator 3D, który zwiększa konwersję i pozwala klientom tworzyć własne biurka regulowane w czasie rzeczywistym.'
};
export default function DeskDemoPage() { export default function DeskDemoPage() {
return <DemoPage demo={demoContent.desk} />; redirect('/pl/demo-biurko');
} }
+21 -4
View File
@@ -1,8 +1,9 @@
import type {Metadata} from 'next'; import type {Metadata} from 'next';
import {Poppins} from 'next/font/google'; import localFont from 'next/font/local';
import {NextIntlClientProvider} from 'next-intl'; import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server'; import {getMessages} from 'next-intl/server';
import {notFound} from 'next/navigation'; import {notFound} from 'next/navigation';
import {ContactSection} from '@/components/ContactSection';
import {Footer} from '@/components/Footer'; import {Footer} from '@/components/Footer';
import {Header} from '@/components/Header'; import {Header} from '@/components/Header';
import {routing} from '@/i18n/routing'; import {routing} from '@/i18n/routing';
@@ -12,9 +13,24 @@ type LocaleLayoutProps = {
params: Promise<{locale: string}>; params: Promise<{locale: string}>;
}; };
const poppins = Poppins({ const poppins = localFont({
subsets: ['latin', 'latin-ext'], src: [
weight: ['400', '600', '700'], {
path: '../fonts/Poppins-Regular.ttf',
weight: '400',
style: 'normal'
},
{
path: '../fonts/Poppins-SemiBold.ttf',
weight: '600',
style: 'normal'
},
{
path: '../fonts/Poppins-Bold.ttf',
weight: '700',
style: 'normal'
}
],
display: 'swap' display: 'swap'
}); });
@@ -44,6 +60,7 @@ export default async function LocaleLayout({children, params}: LocaleLayoutProps
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
<Header /> <Header />
{children} {children}
<ContactSection />
<Footer /> <Footer />
</NextIntlClientProvider> </NextIntlClientProvider>
</body> </body>
+12 -15
View File
@@ -7,8 +7,7 @@ import {Faq} from '@/components/Faq';
import {HeroVisual} from '@/components/HeroVisual'; import {HeroVisual} from '@/components/HeroVisual';
import {SectionHeader} from '@/components/SectionHeader'; import {SectionHeader} from '@/components/SectionHeader';
import {Stats} from '@/components/Stats'; import {Stats} from '@/components/Stats';
import {contact} from '@/config/contact'; import {homeContent, marketingFaq, orderedDemos} from '@/config/content';
import {demoContent, homeContent, marketingFaq} from '@/config/content';
import messages from '../../../messages/pl.json'; import messages from '../../../messages/pl.json';
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -20,8 +19,6 @@ const platformIcons = [Settings2, Workflow, Plug, ShoppingCart];
const industryIcons = [Box, Layers3, CheckCircle2]; const industryIcons = [Box, Layers3, CheckCircle2];
export default function HomePage() { export default function HomePage() {
const demos = [demoContent.desk, demoContent.room, demoContent.door];
return ( return (
<main> <main>
<section className="hero sectionBand sectionBand--light"> <section className="hero sectionBand sectionBand--light">
@@ -32,7 +29,7 @@ export default function HomePage() {
<p>{homeContent.hero.description}</p> <p>{homeContent.hero.description}</p>
<div className="buttonRow"> <div className="buttonRow">
<ButtonLink href="/pl/dema">{homeContent.hero.primaryCta}</ButtonLink> <ButtonLink href="/pl/dema">{homeContent.hero.primaryCta}</ButtonLink>
<ButtonLink href={contact.url} variant="secondary" isExternal> <ButtonLink href="#kontakt" variant="secondary">
{homeContent.hero.secondaryCta} {homeContent.hero.secondaryCta}
</ButtonLink> </ButtonLink>
</div> </div>
@@ -41,17 +38,11 @@ export default function HomePage() {
</div> </div>
</section> </section>
<section className="sectionBand statsBand"> <section id="dema" className="sectionBand demosBand">
<div className="container">
<Stats stats={homeContent.stats} />
</div>
</section>
<section id="dema" className="sectionBand">
<div className="container"> <div className="container">
<SectionHeader title={homeContent.demosTitle} description={homeContent.demosDescription} /> <SectionHeader title={homeContent.demosTitle} description={homeContent.demosDescription} />
<div className="demoCards"> <div className="demoCards">
{demos.map(demo => ( {orderedDemos.map(demo => (
<article className="demoCard" key={demo.slug}> <article className="demoCard" key={demo.slug}>
{demo.imageUrl ? ( {demo.imageUrl ? (
<Image src={demo.imageUrl} alt="" width={420} height={420} /> <Image src={demo.imageUrl} alt="" width={420} height={420} />
@@ -93,7 +84,13 @@ export default function HomePage() {
</div> </div>
</section> </section>
<section className="sectionBand"> <section className="sectionBand statsBand">
<div className="container">
<Stats stats={homeContent.stats} />
</div>
</section>
<section className="sectionBand industriesBand">
<div className="container"> <div className="container">
<SectionHeader title={homeContent.industriesTitle} /> <SectionHeader title={homeContent.industriesTitle} />
<div className="featureGrid featureGrid--three"> <div className="featureGrid featureGrid--three">
@@ -151,7 +148,7 @@ export default function HomePage() {
<div className="container finalCta"> <div className="container finalCta">
<h2>{homeContent.finalCta.title}</h2> <h2>{homeContent.finalCta.title}</h2>
<p>{homeContent.finalCta.description}</p> <p>{homeContent.finalCta.description}</p>
<ButtonLink href={contact.url} isExternal> <ButtonLink href="#kontakt">
{homeContent.finalCta.button} {homeContent.finalCta.button}
</ButtonLink> </ButtonLink>
</div> </div>
Binary file not shown.
Binary file not shown.
Binary file not shown.
+237 -14
View File
@@ -42,18 +42,21 @@ p {
h1 { h1 {
max-width: 780px; max-width: 780px;
font-size: clamp(1.95rem, 3vw, 2.85rem); font-size: clamp(1.95rem, 3vw, 2.85rem);
font-weight: 600;
line-height: 1.14; line-height: 1.14;
letter-spacing: 0; letter-spacing: 0;
} }
h2 { h2 {
font-size: clamp(2rem, 4vw, 3.35rem); font-size: clamp(2rem, 4vw, 3.35rem);
font-weight: 600;
line-height: 1.12; line-height: 1.12;
letter-spacing: 0; letter-spacing: 0;
} }
h3 { h3 {
font-size: clamp(1.1rem, 2vw, 1.35rem); font-size: clamp(1.1rem, 2vw, 1.35rem);
font-weight: 600;
line-height: 1.28; line-height: 1.28;
} }
@@ -87,6 +90,10 @@ p {
color: $white; color: $white;
} }
.sectionBand--compact {
padding: 24px 0;
}
.siteHeader { .siteHeader {
position: sticky; position: sticky;
top: 0; top: 0;
@@ -160,7 +167,7 @@ p {
border-radius: 6px; border-radius: 6px;
padding: 0 18px; padding: 0 18px;
border: 1px solid transparent; border: 1px solid transparent;
font-weight: 700; font-weight: 600;
line-height: 1.2; line-height: 1.2;
transition: transition:
background 160ms ease, background 160ms ease,
@@ -168,17 +175,27 @@ p {
border-color 160ms ease; border-color 160ms ease;
} }
.siteHeader__contact,
.button--primary { .button--primary {
background: $primary; background: $primary;
color: $white; color: $white;
} }
.siteHeader__contact:hover,
.button--primary:hover { .button--primary:hover {
background: #208de8; background: #208de8;
} }
.siteHeader__contact {
border-color: rgba(0, 28, 68, 0.18);
background: $white;
color: $secondary;
}
.siteHeader__contact:hover,
.siteHeader__contact.isActive {
border-color: $primary;
color: $primary;
}
.button--secondary { .button--secondary {
border-color: rgba(0, 28, 68, 0.18); border-color: rgba(0, 28, 68, 0.18);
background: $white; background: $white;
@@ -243,7 +260,7 @@ p {
.eyebrow { .eyebrow {
color: $primary; color: $primary;
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 700; font-weight: 600;
letter-spacing: 0; letter-spacing: 0;
text-transform: uppercase; text-transform: uppercase;
} }
@@ -370,7 +387,7 @@ p {
.heroVisual__panel > div:first-child { .heroVisual__panel > div:first-child {
color: $secondary; color: $secondary;
font-weight: 700; font-weight: 600;
} }
.heroVisual__option { .heroVisual__option {
@@ -451,7 +468,7 @@ p {
border-bottom: 1px solid rgba(0, 28, 68, 0.1); border-bottom: 1px solid rgba(0, 28, 68, 0.1);
color: $secondary; color: $secondary;
font-size: 0.92rem; font-size: 0.92rem;
font-weight: 700; font-weight: 600;
} }
.demoFrame__bar > span { .demoFrame__bar > span {
@@ -500,7 +517,7 @@ p {
place-items: center; place-items: center;
padding: 24px; padding: 24px;
color: $secondary; color: $secondary;
font-weight: 700; font-weight: 600;
text-align: center; text-align: center;
} }
@@ -511,7 +528,7 @@ p {
} }
.statsBand { .statsBand {
padding: clamp(28px, 4vw, 52px) 0; padding: clamp(34px, 4vw, 58px) 0 clamp(24px, 3vw, 42px);
} }
.statItem { .statItem {
@@ -607,6 +624,7 @@ p {
} }
.demoBrowser__item strong { .demoBrowser__item strong {
font-weight: 600;
font-size: 0.93rem; font-size: 0.93rem;
line-height: 1.32; line-height: 1.32;
} }
@@ -657,6 +675,14 @@ p {
margin-bottom: 34px; margin-bottom: 34px;
} }
.demosBand {
padding-top: clamp(28px, 4vw, 52px);
}
.industriesBand {
padding-top: clamp(34px, 4vw, 56px);
}
.demoCards, .demoCards,
.featureGrid, .featureGrid,
.processGrid { .processGrid {
@@ -710,7 +736,9 @@ p {
.demoCard > div { .demoCard > div {
display: grid; display: grid;
grid-template-rows: auto auto 1fr auto;
gap: 14px; gap: 14px;
height: 100%;
} }
.demoCard p:not(.eyebrow), .demoCard p:not(.eyebrow),
@@ -722,9 +750,98 @@ p {
.demoCard a { .demoCard a {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
align-self: end;
gap: 8px; gap: 8px;
margin-top: 10px;
color: $primary; color: $primary;
font-weight: 700; font-weight: 600;
}
.demoSubnav {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.demoSubnav a {
display: grid;
gap: 5px;
min-height: 78px;
align-content: center;
padding: 14px 16px;
border: 1px solid rgba(0, 28, 68, 0.1);
border-radius: 8px;
background: $white;
color: $secondary;
font-size: 0.94rem;
font-weight: 600;
line-height: 1.3;
}
.demoSubnav a span {
color: $primary;
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
}
.demoSubnav a.isActive {
border-color: rgba(66, 166, 255, 0.7);
background: rgba(66, 166, 255, 0.09);
box-shadow: 0 12px 30px rgba(0, 28, 68, 0.08);
}
.demoSubnavBar {
position: sticky;
top: 78px;
z-index: 19;
border-bottom: 1px solid rgba(0, 28, 68, 0.1);
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(14px);
}
.demoSubnavBar .demoSubnav {
display: flex;
gap: 24px;
align-items: center;
justify-content: center;
margin-bottom: 0;
}
.demoSubnavBar .demoSubnav a {
min-height: 52px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
color: rgba(0, 28, 68, 0.78);
font-size: 0.9rem;
white-space: nowrap;
}
.demoSubnavBar .demoSubnav a span {
display: none;
}
.demoSubnavBar .demoSubnav a.isActive {
position: relative;
border: 0;
background: transparent;
box-shadow: none;
color: $primary;
}
.demoSubnavBar .demoSubnav a.isActive::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 3px;
border-radius: 999px 999px 0 0;
background: $primary;
content: '';
} }
.splitSection { .splitSection {
@@ -808,7 +925,7 @@ p {
border-radius: 6px; border-radius: 6px;
background: $primary; background: $primary;
color: $white; color: $white;
font-weight: 700; font-weight: 600;
} }
.faqList { .faqList {
@@ -825,7 +942,7 @@ p {
.faqList summary { .faqList summary {
cursor: pointer; cursor: pointer;
padding: 20px 22px; padding: 20px 22px;
font-weight: 700; font-weight: 600;
} }
.faqList p { .faqList p {
@@ -857,6 +974,92 @@ p {
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr); grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
} }
.contactSection {
scroll-margin-top: 88px;
background: $tertiary;
}
.contactSection__grid {
display: grid;
gap: clamp(18px, 3vw, 28px);
}
.contactSection__intro {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.5fr);
gap: clamp(28px, 5vw, 72px);
align-items: end;
}
.contactSection__intro > div:first-child {
display: grid;
gap: 16px;
}
.contactSection__grid h2 {
max-width: 680px;
}
.contactSection__grid p:not(.eyebrow) {
max-width: 640px;
color: $gray-light;
}
.contactSection__schedulerWrap {
display: grid;
gap: 14px;
}
.contactSection__scheduler {
width: 100%;
height: 555px;
max-height: 555px;
border: 1px solid rgba(0, 28, 68, 0.1);
border-radius: 8px;
background: $tertiary;
}
.contactSection__details {
display: grid;
align-self: end;
gap: 14px;
padding: clamp(22px, 3vw, 30px);
border: 1px solid rgba(0, 28, 68, 0.1);
border-radius: 8px;
background: $white;
font-style: normal;
}
.contactSection__details > div {
display: grid;
grid-template-columns: 24px 1fr;
gap: 12px;
align-items: start;
}
.contactSection__details svg {
margin-top: 2px;
color: $primary;
}
.contactSection__details span {
display: grid;
gap: 4px;
}
.contactSection__details strong,
.contactSection__details a {
color: $secondary;
font-weight: 600;
line-height: 1.5;
}
.contactSection__fallback {
justify-self: start;
color: $primary;
font-weight: 600;
}
.footer { .footer {
background: $secondary; background: $secondary;
color: $white; color: $white;
@@ -865,14 +1068,13 @@ p {
.footer__inner { .footer__inner {
display: grid; display: grid;
grid-template-columns: 1.1fr 0.8fr 0.9fr auto; grid-template-columns: 1fr auto;
gap: 32px; gap: 32px;
width: min(1180px, calc(100% - 40px)); width: min(1180px, calc(100% - 40px));
margin: 0 auto; margin: 0 auto;
} }
.footer__brand, .footer__brand,
.footer__nav,
.footer__contact, .footer__contact,
.footer__social { .footer__social {
display: grid; display: grid;
@@ -890,6 +1092,12 @@ p {
color: rgba(255, 255, 255, 0.78); color: rgba(255, 255, 255, 0.78);
} }
.footer__contact {
justify-self: end;
font-style: normal;
text-align: right;
}
.footer__social { .footer__social {
grid-auto-flow: column; grid-auto-flow: column;
} }
@@ -937,13 +1145,26 @@ p {
.siteHeader__mobileNav a { .siteHeader__mobileNav a {
padding: 14px 0; padding: 14px 0;
font-weight: 700; font-weight: 600;
}
.demoSubnavBar {
top: 68px;
overflow-x: auto;
}
.demoSubnavBar .demoSubnav {
width: max-content;
min-width: 100%;
justify-content: flex-start;
} }
.hero__grid, .hero__grid,
.demoHero__grid, .demoHero__grid,
.demoBrowser, .demoBrowser,
.splitSection, .splitSection,
.contactSection__grid,
.contactSection__intro,
.footer__inner { .footer__inner {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -958,6 +1179,7 @@ p {
.featureGrid--three, .featureGrid--three,
.demoCards, .demoCards,
.demoSubnav,
.processGrid { .processGrid {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -995,6 +1217,7 @@ p {
.statsGrid, .statsGrid,
.demoCards, .demoCards,
.demoSubnav,
.demoBrowser__list, .demoBrowser__list,
.featureGrid, .featureGrid,
.featureGrid--three, .featureGrid--three,
+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 Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import {useRouter, useSearchParams} from 'next/navigation'; import {useRouter, useSearchParams} from 'next/navigation';
import {useMemo, useState} from 'react'; import {useMemo} from 'react';
import {DemoFrame} from './DemoFrame'; import {DemoFrame} from './DemoFrame';
import type {DemoContent} from '@/types/content'; import type {DemoContent} from '@/types/content';
@@ -12,7 +12,7 @@ type DemoBrowserProps = {
demos: DemoContent[]; 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); return demos.some(demo => demo.slug === slug);
} }
@@ -20,8 +20,7 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const requestedDemo = searchParams.get('demo'); const requestedDemo = searchParams.get('demo');
const initialSlug = isKnownDemo(requestedDemo, demos) ? requestedDemo : demos[0]?.slug; const activeSlug = isKnownDemo(requestedDemo, demos) ? requestedDemo : demos[0].slug;
const [activeSlug, setActiveSlug] = useState(initialSlug);
const activeDemo = useMemo( const activeDemo = useMemo(
() => demos.find(demo => demo.slug === activeSlug) ?? demos[0], () => demos.find(demo => demo.slug === activeSlug) ?? demos[0],
@@ -29,7 +28,6 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
); );
function selectDemo(slug: DemoContent['slug']) { function selectDemo(slug: DemoContent['slug']) {
setActiveSlug(slug);
router.replace(`/pl/dema?demo=${slug}`, {scroll: false}); router.replace(`/pl/dema?demo=${slug}`, {scroll: false});
} }
+12 -3
View File
@@ -2,10 +2,11 @@ import {CheckCircle2, DoorOpen} from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import {ButtonLink} from './ButtonLink'; import {ButtonLink} from './ButtonLink';
import {DemoFrame} from './DemoFrame'; import {DemoFrame} from './DemoFrame';
import {DemoSubnav} from './DemoSubnav';
import {Faq} from './Faq'; import {Faq} from './Faq';
import {SectionHeader} from './SectionHeader'; import {SectionHeader} from './SectionHeader';
import {Stats} from './Stats'; import {Stats} from './Stats';
import {contact} from '@/config/contact'; import {orderedDemos} from '@/config/content';
import type {DemoContent} from '@/types/content'; import type {DemoContent} from '@/types/content';
type DemoPageProps = { type DemoPageProps = {
@@ -14,6 +15,13 @@ type DemoPageProps = {
export function DemoPage({demo}: DemoPageProps) { export function DemoPage({demo}: DemoPageProps) {
return ( return (
<>
<div className="demoSubnavBar">
<div className="container">
<DemoSubnav demos={orderedDemos} activeSlug={demo.slug} />
</div>
</div>
<main> <main>
<section className="demoHero sectionBand sectionBand--light"> <section className="demoHero sectionBand sectionBand--light">
<div className="container demoHero__grid"> <div className="container demoHero__grid">
@@ -24,7 +32,7 @@ export function DemoPage({demo}: DemoPageProps) {
<p>{demo.description}</p> <p>{demo.description}</p>
<div className="buttonRow"> <div className="buttonRow">
<ButtonLink href="#podglad">Zobacz demo</ButtonLink> <ButtonLink href="#podglad">Zobacz demo</ButtonLink>
<ButtonLink href={contact.url} variant="secondary" isExternal> <ButtonLink href="#kontakt" variant="secondary">
Porozmawiajmy Porozmawiajmy
</ButtonLink> </ButtonLink>
</div> </div>
@@ -70,7 +78,7 @@ export function DemoPage({demo}: DemoPageProps) {
<div className="container finalCta finalCta--dark"> <div className="container finalCta finalCta--dark">
<h2>{demo.cta.title}</h2> <h2>{demo.cta.title}</h2>
<p>{demo.cta.description}</p> <p>{demo.cta.description}</p>
<ButtonLink href={contact.url} isExternal> <ButtonLink href="#kontakt">
{demo.cta.button} {demo.cta.button}
</ButtonLink> </ButtonLink>
</div> </div>
@@ -83,5 +91,6 @@ export function DemoPage({demo}: DemoPageProps) {
</div> </div>
</section> </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> </Link>
<p>Copyright © 2026, Ultifide</p> <p>Copyright © 2026, Ultifide</p>
</div> </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"> <address className="footer__contact">
<strong>Contact Us</strong>
{contact.address.map(line => ( {contact.address.map(line => (
<span key={line}>{line}</span> <span key={line}>{line}</span>
))} ))}
<a href={`mailto:${contact.email}`}>{contact.email}</a>
<a href={`tel:${contact.phone.replaceAll(' ', '')}`}>{contact.phone}</a>
</address> </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> </div>
</footer> </footer>
); );
+24 -23
View File
@@ -4,15 +4,13 @@ import {Menu, X} from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {usePathname} from 'next/navigation'; import {usePathname} from 'next/navigation';
import {contact} from '@/config/contact';
import {Logo} from './Logo'; import {Logo} from './Logo';
const navItems = [ const navItems = [
{id: 'dema', label: 'Dema', href: '/pl#dema'},
{id: 'platforma', label: 'Platforma', href: '/pl#platforma'}, {id: 'platforma', label: 'Platforma', href: '/pl#platforma'},
{id: 'dema', label: 'Dema', href: '/pl/dema'},
{id: 'funkcje', label: 'Funkcje', href: '/pl#funkcje'}, {id: 'funkcje', label: 'Funkcje', href: '/pl#funkcje'},
{id: 'faq', label: 'FAQ', href: '/pl#faq'}, {id: 'faq', label: 'FAQ', href: '/pl#faq'}
{id: 'kontakt', label: 'Kontakt', href: contact.url}
]; ];
export function Header() { export function Header() {
@@ -25,33 +23,36 @@ export function Header() {
return; return;
} }
const sections = ['platforma', 'dema', 'funkcje', 'faq'] const sections = ['dema', 'platforma', 'funkcje', 'faq', 'kontakt']
.map(id => document.getElementById(id)) .map(id => document.getElementById(id))
.filter((section): section is HTMLElement => Boolean(section)); .filter((section): section is HTMLElement => Boolean(section));
const observer = new IntersectionObserver( function updateActiveSection() {
entries => { const activationLine = 120;
const visibleEntry = entries const currentSection = sections.reduce<HTMLElement | null>((active, section) => {
.filter(entry => entry.isIntersecting) const sectionTop = section.getBoundingClientRect().top;
.sort((first, second) => second.intersectionRatio - first.intersectionRatio)[0];
if (visibleEntry?.target.id) { if (sectionTop <= activationLine) {
setActiveSection(visibleEntry.target.id); 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]); }, [pathname]);
function isActive(itemId: string) { function isActive(itemId: string) {
if (itemId === 'kontakt') {
return false;
}
if (pathname === '/pl') { if (pathname === '/pl') {
return itemId === activeSection; return itemId === activeSection;
} }
@@ -76,8 +77,8 @@ export function Header() {
</a> </a>
))} ))}
</nav> </nav>
<a className="siteHeader__contact" href={contact.url}> <a className={activeSection === 'kontakt' ? 'siteHeader__contact isActive' : 'siteHeader__contact'} href="#kontakt">
Porozmawiajmy Kontakt
</a> </a>
<button <button
className="siteHeader__menu" className="siteHeader__menu"
-4
View File
@@ -44,10 +44,6 @@ export function HeroVisual() {
</div> </div>
</div> </div>
</div> </div>
<div className="heroVisual__footer">
<span>3D preview</span>
<strong>API-ready</strong>
</div>
</div> </div>
); );
} }
+3 -3
View File
@@ -1,8 +1,8 @@
export const contact = { export const contact = {
url: 'https://ultifide.com/contact', anchor: '#kontakt',
email: 'contact@ultifide.com', email: 'contact@ultifide.com',
phone: '+48 733 226 544', phones: ['+48 733 226 544', '+48 664 565 858'],
address: ['Cracow, Poland', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'], address: ['Kraw, Polska', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'],
linkedin: 'https://pl.linkedin.com/company/ultifide', linkedin: 'https://pl.linkedin.com/company/ultifide',
facebook: 'https://facebook.com/ultifide' facebook: 'https://facebook.com/ultifide'
} as const; } as const;
+66 -3
View File
@@ -21,9 +21,72 @@ function createDemoContent(demo: DemoMessages, iframeUrl: string): DemoContent {
} }
export const demoContent = { export const demoContent = {
desk: createDemoContent(messages.demos.desk, demoUrls.desk),
room: createDemoContent(messages.demos.room, demoUrls.room), room: createDemoContent(messages.demos.room, demoUrls.room),
door: createDemoContent(messages.demos.door, demoUrls.door) door: createDemoContent(messages.demos.door, demoUrls.door),
desk: createDemoContent(messages.demos.desk, demoUrls.desk)
} as const; } as const;
export const marketingFaq: FaqItem[] = [...demoContent.desk.faq, ...demoContent.room.faq]; export const orderedDemos: DemoContent[] = [demoContent.room, demoContent.door, demoContent.desk];
export const marketingFaq: FaqItem[] = [
{
question: 'Czy możemy zacząć od jednego produktu?',
answer:
'Tak. V1 może obejmować jeden produkt, jedną kolekcję albo jedno demo, a potem rosnąć o kolejne warianty, reguły i integracje.'
},
{
question: 'Czy konfigurator może być częścią mojego sklepu?',
answer:
'Tak. Może działać jako widżet, dedykowana podstrona, kreator zestawów, moduł koszyka albo element panelu B2B.'
},
{
question: 'Jak wygląda integracja ze sprzedażą, CRM lub ERP?',
answer:
'Konfiguracja może trafiać do koszyka, formularza zapytania, CRM, ERP albo panelu B2B. Integrujemy przez API, webhooki i popularne platformy e-commerce.'
},
{
question: 'Czy mogę dodać własne modele 3D, materiały i warianty?',
answer:
'Tak. Możemy pracować na dostarczonych modelach albo przygotować i zoptymalizować je pod konfigurator, a potem rozwijać katalog o nowe kolory, materiały, dodatki i akcesoria.'
},
{
question: 'Czy konfigurator obsługuje zależności i reguły produktu?',
answer:
'Tak. Obsługujemy warunki, ograniczenia, wariantowe SKU, zgodność modułów i zależności między modelem, rozmiarem, kolorem, okuciami, dodatkami lub dostępnością.'
},
{
question: 'Czy konfigurator działa na urządzeniach mobilnych?',
answer:
'Tak. Interfejs jest responsywny, a w wybranych wdrożeniach można dodać tryb AR.'
},
{
question: 'Czy wygląd konfiguratora można dopasować do mojej marki?',
answer:
'Tak. UI, kolory, branding, układ opcji, sposób interakcji, modele 3D i środowisko sceny mogą wyglądać jak natywna część Twojego sklepu.'
},
{
question: 'Czy konfigurator sprawdzi się dla mebli modułowych i całych kolekcji?',
answer:
'Tak. System można rozwijać o meble modułowe, zestawy, kolekcje aranżacyjne, nowe produkty i kolejne układy pomieszczeń bez przebudowy całości.'
},
{
question: 'Czy można odwzorować całe pomieszczenia?',
answer:
'Tak. Możemy stworzyć pełne sceny 3D od pustego pokoju po gotowe aranżacje z konfigurowalnymi meblami, kolorami i materiałami.'
},
{
question: 'Czy klient może zmieniać układ mebli i elementów?',
answer:
'Tak. Konfigurator może pozwalać na ustawianie elementów, zmianę układu, dobór dodatków i dopasowanie konfiguracji do przestrzeni.'
},
{
question: 'Jakie są główne korzyści z wdrożenia konfiguratora 3D?',
answer:
'Konfigurator pozwala prezentować produkty w jakości premium, personalizować je w czasie rzeczywistym, ograniczać liczbę zwrotów i pytań oraz skracać proces decyzji zakupowej.'
},
{
question: 'Czy konfigurator nadaje się do produktów takich jak drzwi, fronty lub zabudowy?',
answer:
'Tak. Sprawdza się przy produktach wymagających precyzyjnego dopasowania wariantów, takich jak drzwi, fronty, zabudowy, oświetlenie, wyposażenie i inne produkty custom.'
}
];
+1 -1
View File
@@ -3,4 +3,4 @@ $secondary: #001c44;
$tertiary: #f9fafe; $tertiary: #f9fafe;
$white: #ffffff; $white: #ffffff;
$gray: rgba(157, 158, 180, 1); $gray: rgba(157, 158, 180, 1);
$gray-light: rgba(115, 130, 151, 1); $gray-light: rgba(63, 80, 104, 1);
+1 -1
View File
@@ -19,7 +19,7 @@ export type FaqItem = {
}; };
export type DemoContent = { export type DemoContent = {
slug: 'demo' | 'demo-room' | 'demo-door'; slug: 'demo-pokoj' | 'demo-drzwi' | 'demo-biurko';
title: string; title: string;
eyebrow: string; eyebrow: string;
intro: string; intro: string;