rework components
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import type {Metadata} from 'next';
|
||||
import {DemoBrowser} from '@/components/DemoBrowser';
|
||||
import {DemoSubnav} from '@/components/DemoSubnav';
|
||||
import {SectionHeader} from '@/components/SectionHeader';
|
||||
import {demoContent} from '@/config/content';
|
||||
import {orderedDemos} from '@/config/content';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dema konfiguratorów 3D | Ultifide',
|
||||
@@ -9,8 +10,6 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default function DemosPage() {
|
||||
const demos = [demoContent.desk, demoContent.room, demoContent.door];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="sectionBand sectionBand--light demosPage">
|
||||
@@ -20,7 +19,8 @@ export default function DemosPage() {
|
||||
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."
|
||||
/>
|
||||
<DemoBrowser demos={demos} />
|
||||
<DemoSubnav demos={orderedDemos} hrefType="browser" />
|
||||
<DemoBrowser demos={orderedDemos} />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
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.'
|
||||
};
|
||||
import {redirect} from 'next/navigation';
|
||||
|
||||
export default function DoorDemoPage() {
|
||||
return <DemoPage demo={demoContent.door} />;
|
||||
redirect('/pl/demo-drzwi');
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
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.'
|
||||
};
|
||||
import {redirect} from 'next/navigation';
|
||||
|
||||
export default function RoomDemoPage() {
|
||||
return <DemoPage demo={demoContent.room} />;
|
||||
redirect('/pl/demo-pokoj');
|
||||
}
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
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.'
|
||||
};
|
||||
import {redirect} from 'next/navigation';
|
||||
|
||||
export default function DeskDemoPage() {
|
||||
return <DemoPage demo={demoContent.desk} />;
|
||||
redirect('/pl/demo-biurko');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type {Metadata} from 'next';
|
||||
import {Poppins} from 'next/font/google';
|
||||
import localFont from 'next/font/local';
|
||||
import {NextIntlClientProvider} from 'next-intl';
|
||||
import {getMessages} from 'next-intl/server';
|
||||
import {notFound} from 'next/navigation';
|
||||
import {ContactSection} from '@/components/ContactSection';
|
||||
import {Footer} from '@/components/Footer';
|
||||
import {Header} from '@/components/Header';
|
||||
import {routing} from '@/i18n/routing';
|
||||
@@ -12,9 +13,24 @@ type LocaleLayoutProps = {
|
||||
params: Promise<{locale: string}>;
|
||||
};
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ['latin', 'latin-ext'],
|
||||
weight: ['400', '600', '700'],
|
||||
const poppins = localFont({
|
||||
src: [
|
||||
{
|
||||
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'
|
||||
});
|
||||
|
||||
@@ -44,6 +60,7 @@ export default async function LocaleLayout({children, params}: LocaleLayoutProps
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<Header />
|
||||
{children}
|
||||
<ContactSection />
|
||||
<Footer />
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
|
||||
+12
-15
@@ -7,8 +7,7 @@ import {Faq} from '@/components/Faq';
|
||||
import {HeroVisual} from '@/components/HeroVisual';
|
||||
import {SectionHeader} from '@/components/SectionHeader';
|
||||
import {Stats} from '@/components/Stats';
|
||||
import {contact} from '@/config/contact';
|
||||
import {demoContent, homeContent, marketingFaq} from '@/config/content';
|
||||
import {homeContent, marketingFaq, orderedDemos} from '@/config/content';
|
||||
import messages from '../../../messages/pl.json';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -20,8 +19,6 @@ const platformIcons = [Settings2, Workflow, Plug, ShoppingCart];
|
||||
const industryIcons = [Box, Layers3, CheckCircle2];
|
||||
|
||||
export default function HomePage() {
|
||||
const demos = [demoContent.desk, demoContent.room, demoContent.door];
|
||||
|
||||
return (
|
||||
<main>
|
||||
<section className="hero sectionBand sectionBand--light">
|
||||
@@ -32,7 +29,7 @@ export default function HomePage() {
|
||||
<p>{homeContent.hero.description}</p>
|
||||
<div className="buttonRow">
|
||||
<ButtonLink href="/pl/dema">{homeContent.hero.primaryCta}</ButtonLink>
|
||||
<ButtonLink href={contact.url} variant="secondary" isExternal>
|
||||
<ButtonLink href="#kontakt" variant="secondary">
|
||||
{homeContent.hero.secondaryCta}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
@@ -41,17 +38,11 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="sectionBand statsBand">
|
||||
<div className="container">
|
||||
<Stats stats={homeContent.stats} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="dema" className="sectionBand">
|
||||
<section id="dema" className="sectionBand demosBand">
|
||||
<div className="container">
|
||||
<SectionHeader title={homeContent.demosTitle} description={homeContent.demosDescription} />
|
||||
<div className="demoCards">
|
||||
{demos.map(demo => (
|
||||
{orderedDemos.map(demo => (
|
||||
<article className="demoCard" key={demo.slug}>
|
||||
{demo.imageUrl ? (
|
||||
<Image src={demo.imageUrl} alt="" width={420} height={420} />
|
||||
@@ -93,7 +84,13 @@ export default function HomePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="sectionBand">
|
||||
<section className="sectionBand statsBand">
|
||||
<div className="container">
|
||||
<Stats stats={homeContent.stats} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="sectionBand industriesBand">
|
||||
<div className="container">
|
||||
<SectionHeader title={homeContent.industriesTitle} />
|
||||
<div className="featureGrid featureGrid--three">
|
||||
@@ -151,7 +148,7 @@ export default function HomePage() {
|
||||
<div className="container finalCta">
|
||||
<h2>{homeContent.finalCta.title}</h2>
|
||||
<p>{homeContent.finalCta.description}</p>
|
||||
<ButtonLink href={contact.url} isExternal>
|
||||
<ButtonLink href="#kontakt">
|
||||
{homeContent.finalCta.button}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+237
-14
@@ -42,18 +42,21 @@ p {
|
||||
h1 {
|
||||
max-width: 780px;
|
||||
font-size: clamp(1.95rem, 3vw, 2.85rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.14;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(2rem, 4vw, 3.35rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.12;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.1rem, 2vw, 1.35rem);
|
||||
font-weight: 600;
|
||||
line-height: 1.28;
|
||||
}
|
||||
|
||||
@@ -87,6 +90,10 @@ p {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.sectionBand--compact {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.siteHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@@ -160,7 +167,7 @@ p {
|
||||
border-radius: 6px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid transparent;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
transition:
|
||||
background 160ms ease,
|
||||
@@ -168,17 +175,27 @@ p {
|
||||
border-color 160ms ease;
|
||||
}
|
||||
|
||||
.siteHeader__contact,
|
||||
.button--primary {
|
||||
background: $primary;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.siteHeader__contact:hover,
|
||||
.button--primary:hover {
|
||||
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 {
|
||||
border-color: rgba(0, 28, 68, 0.18);
|
||||
background: $white;
|
||||
@@ -243,7 +260,7 @@ p {
|
||||
.eyebrow {
|
||||
color: $primary;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -370,7 +387,7 @@ p {
|
||||
|
||||
.heroVisual__panel > div:first-child {
|
||||
color: $secondary;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.heroVisual__option {
|
||||
@@ -451,7 +468,7 @@ p {
|
||||
border-bottom: 1px solid rgba(0, 28, 68, 0.1);
|
||||
color: $secondary;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.demoFrame__bar > span {
|
||||
@@ -500,7 +517,7 @@ p {
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
color: $secondary;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -511,7 +528,7 @@ p {
|
||||
}
|
||||
|
||||
.statsBand {
|
||||
padding: clamp(28px, 4vw, 52px) 0;
|
||||
padding: clamp(34px, 4vw, 58px) 0 clamp(24px, 3vw, 42px);
|
||||
}
|
||||
|
||||
.statItem {
|
||||
@@ -607,6 +624,7 @@ p {
|
||||
}
|
||||
|
||||
.demoBrowser__item strong {
|
||||
font-weight: 600;
|
||||
font-size: 0.93rem;
|
||||
line-height: 1.32;
|
||||
}
|
||||
@@ -657,6 +675,14 @@ p {
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
|
||||
.demosBand {
|
||||
padding-top: clamp(28px, 4vw, 52px);
|
||||
}
|
||||
|
||||
.industriesBand {
|
||||
padding-top: clamp(34px, 4vw, 56px);
|
||||
}
|
||||
|
||||
.demoCards,
|
||||
.featureGrid,
|
||||
.processGrid {
|
||||
@@ -710,7 +736,9 @@ p {
|
||||
|
||||
.demoCard > div {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.demoCard p:not(.eyebrow),
|
||||
@@ -722,9 +750,98 @@ p {
|
||||
.demoCard a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: end;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
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 {
|
||||
@@ -808,7 +925,7 @@ p {
|
||||
border-radius: 6px;
|
||||
background: $primary;
|
||||
color: $white;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.faqList {
|
||||
@@ -825,7 +942,7 @@ p {
|
||||
.faqList summary {
|
||||
cursor: pointer;
|
||||
padding: 20px 22px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.faqList p {
|
||||
@@ -857,6 +974,92 @@ p {
|
||||
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 {
|
||||
background: $secondary;
|
||||
color: $white;
|
||||
@@ -865,14 +1068,13 @@ p {
|
||||
|
||||
.footer__inner {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 0.8fr 0.9fr auto;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 32px;
|
||||
width: min(1180px, calc(100% - 40px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer__brand,
|
||||
.footer__nav,
|
||||
.footer__contact,
|
||||
.footer__social {
|
||||
display: grid;
|
||||
@@ -890,6 +1092,12 @@ p {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.footer__contact {
|
||||
justify-self: end;
|
||||
font-style: normal;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer__social {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
@@ -937,13 +1145,26 @@ p {
|
||||
|
||||
.siteHeader__mobileNav a {
|
||||
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,
|
||||
.demoHero__grid,
|
||||
.demoBrowser,
|
||||
.splitSection,
|
||||
.contactSection__grid,
|
||||
.contactSection__intro,
|
||||
.footer__inner {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -958,6 +1179,7 @@ p {
|
||||
|
||||
.featureGrid--three,
|
||||
.demoCards,
|
||||
.demoSubnav,
|
||||
.processGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -995,6 +1217,7 @@ p {
|
||||
|
||||
.statsGrid,
|
||||
.demoCards,
|
||||
.demoSubnav,
|
||||
.demoBrowser__list,
|
||||
.featureGrid,
|
||||
.featureGrid--three,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -44,10 +44,6 @@ export function HeroVisual() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="heroVisual__footer">
|
||||
<span>3D preview</span>
|
||||
<strong>API-ready</strong>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const contact = {
|
||||
url: 'https://ultifide.com/contact',
|
||||
anchor: '#kontakt',
|
||||
email: 'contact@ultifide.com',
|
||||
phone: '+48 733 226 544',
|
||||
address: ['Cracow, Poland', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'],
|
||||
phones: ['+48 733 226 544', '+48 664 565 858'],
|
||||
address: ['Kraków, Polska', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'],
|
||||
linkedin: 'https://pl.linkedin.com/company/ultifide',
|
||||
facebook: 'https://facebook.com/ultifide'
|
||||
} as const;
|
||||
|
||||
+66
-3
@@ -21,9 +21,72 @@ function createDemoContent(demo: DemoMessages, iframeUrl: string): DemoContent {
|
||||
}
|
||||
|
||||
export const demoContent = {
|
||||
desk: createDemoContent(messages.demos.desk, demoUrls.desk),
|
||||
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;
|
||||
|
||||
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.'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -3,4 +3,4 @@ $secondary: #001c44;
|
||||
$tertiary: #f9fafe;
|
||||
$white: #ffffff;
|
||||
$gray: rgba(157, 158, 180, 1);
|
||||
$gray-light: rgba(115, 130, 151, 1);
|
||||
$gray-light: rgba(63, 80, 104, 1);
|
||||
|
||||
@@ -19,7 +19,7 @@ export type FaqItem = {
|
||||
};
|
||||
|
||||
export type DemoContent = {
|
||||
slug: 'demo' | 'demo-room' | 'demo-door';
|
||||
slug: 'demo-pokoj' | 'demo-drzwi' | 'demo-biurko';
|
||||
title: string;
|
||||
eyebrow: string;
|
||||
intro: string;
|
||||
|
||||
Reference in New Issue
Block a user