initial commit

This commit is contained in:
Kuba Pyla
2026-05-20 01:38:47 +02:00
commit 9d4f32ecf0
43 changed files with 9188 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import type {Metadata} from 'next';
import {DemoBrowser} from '@/components/DemoBrowser';
import {SectionHeader} from '@/components/SectionHeader';
import {demoContent} from '@/config/content';
export const metadata: Metadata = {
title: 'Dema konfiguratorów 3D | Ultifide',
description: 'Lista interaktywnych dem konfiguratorów 3D Ultifide z podglądem biurka, wnętrz i drzwi.'
};
export default function DemosPage() {
const demos = [demoContent.desk, demoContent.room, demoContent.door];
return (
<main>
<section className="sectionBand sectionBand--light demosPage">
<div className="container demosContainer">
<SectionHeader
eyebrow="Dema konfiguratorów"
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} />
</div>
</section>
</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.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} />;
}
+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} />;
}
+52
View File
@@ -0,0 +1,52 @@
import type {Metadata} from 'next';
import {Poppins} from 'next/font/google';
import {NextIntlClientProvider} from 'next-intl';
import {getMessages} from 'next-intl/server';
import {notFound} from 'next/navigation';
import {Footer} from '@/components/Footer';
import {Header} from '@/components/Header';
import {routing} from '@/i18n/routing';
type LocaleLayoutProps = {
children: React.ReactNode;
params: Promise<{locale: string}>;
};
const poppins = Poppins({
subsets: ['latin', 'latin-ext'],
weight: ['400', '600', '700'],
display: 'swap'
});
export const metadata: Metadata = {
title: {
default: 'Konfiguratory 3D | Ultifide',
template: '%s'
}
};
export function generateStaticParams() {
return routing.locales.map(locale => ({locale}));
}
export default async function LocaleLayout({children, params}: LocaleLayoutProps) {
const {locale} = await params;
if (!routing.locales.includes(locale as never)) {
notFound();
}
const messages = await getMessages();
return (
<html lang={locale}>
<body className={poppins.className}>
<NextIntlClientProvider messages={messages}>
<Header />
{children}
<Footer />
</NextIntlClientProvider>
</body>
</html>
);
}
+161
View File
@@ -0,0 +1,161 @@
import {ArrowRight, Box, CheckCircle2, DoorOpen, Layers3, Plug, Settings2, ShoppingCart, Workflow} from 'lucide-react';
import type {Metadata} from 'next';
import Image from 'next/image';
import Link from 'next/link';
import {ButtonLink} from '@/components/ButtonLink';
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 messages from '../../../messages/pl.json';
export const metadata: Metadata = {
title: messages.metadata.homeTitle,
description: messages.metadata.homeDescription
};
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">
<div className="container hero__grid">
<div className="hero__content">
<p className="eyebrow">{homeContent.hero.eyebrow}</p>
<h1>{homeContent.hero.title}</h1>
<p>{homeContent.hero.description}</p>
<div className="buttonRow">
<ButtonLink href="/pl/dema">{homeContent.hero.primaryCta}</ButtonLink>
<ButtonLink href={contact.url} variant="secondary" isExternal>
{homeContent.hero.secondaryCta}
</ButtonLink>
</div>
</div>
<HeroVisual />
</div>
</section>
<section className="sectionBand statsBand">
<div className="container">
<Stats stats={homeContent.stats} />
</div>
</section>
<section id="dema" className="sectionBand">
<div className="container">
<SectionHeader title={homeContent.demosTitle} description={homeContent.demosDescription} />
<div className="demoCards">
{demos.map(demo => (
<article className="demoCard" key={demo.slug}>
{demo.imageUrl ? (
<Image src={demo.imageUrl} alt="" width={420} height={420} />
) : (
<div className="demoCard__visual" aria-hidden="true">
<DoorOpen size={72} />
</div>
)}
<div>
<p className="eyebrow">{demo.eyebrow}</p>
<h3>{demo.title}</h3>
<p>{demo.intro}</p>
<Link href={`/pl/dema?demo=${demo.slug}`}>
{demo.openLabel}
<ArrowRight size={18} />
</Link>
</div>
</article>
))}
</div>
</div>
</section>
<section id="platforma" className="sectionBand sectionBand--tint">
<div className="container splitSection">
<SectionHeader title={homeContent.benefitsTitle} description={homeContent.benefitsDescription} />
<div className="featureGrid">
{homeContent.benefits.map((item, index) => {
const Icon = platformIcons[index];
return (
<article className="featureItem" key={item.title}>
<Icon size={24} />
<h3>{item.title}</h3>
<p>{item.description}</p>
</article>
);
})}
</div>
</div>
</section>
<section className="sectionBand">
<div className="container">
<SectionHeader title={homeContent.industriesTitle} />
<div className="featureGrid featureGrid--three">
{homeContent.industries.map((item, index) => {
const Icon = industryIcons[index];
return (
<article className="featureItem" key={item.title}>
<Icon size={24} />
<h3>{item.title}</h3>
<p>{item.description}</p>
</article>
);
})}
</div>
</div>
</section>
<section id="funkcje" className="sectionBand sectionBand--dark">
<div className="container splitSection splitSection--dark">
<SectionHeader title={homeContent.featuresTitle} />
<ul className="checkList">
{homeContent.features.map(feature => (
<li key={feature}>
<CheckCircle2 size={20} />
{feature}
</li>
))}
</ul>
</div>
</section>
<section className="sectionBand">
<div className="container">
<SectionHeader title={homeContent.processTitle} />
<div className="processGrid">
{homeContent.process.map((item, index) => (
<article key={item.title}>
<span>{index + 1}</span>
<h3>{item.title}</h3>
<p>{item.description}</p>
</article>
))}
</div>
</div>
</section>
<section id="faq" className="sectionBand sectionBand--tint">
<div className="container narrow">
<SectionHeader title="FAQ" />
<Faq items={marketingFaq} />
</div>
</section>
<section className="sectionBand">
<div className="container finalCta">
<h2>{homeContent.finalCta.title}</h2>
<p>{homeContent.finalCta.description}</p>
<ButtonLink href={contact.url} isExternal>
{homeContent.finalCta.button}
</ButtonLink>
</div>
</section>
</main>
);
}
+1045
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+12
View File
@@ -0,0 +1,12 @@
import type {Metadata} from 'next';
import './globals.scss';
export const metadata: Metadata = {
metadataBase: new URL('https://ultifide.com'),
title: 'Konfiguratory 3D | Ultifide',
description: 'Konfiguratory 3D dla produktów, wnętrz i sprzedaży B2B.'
};
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
return children;
}
+12
View File
@@ -0,0 +1,12 @@
import Link from 'next/link';
export default function NotFound() {
return (
<main className="notFound">
<h1>Nie znaleziono strony</h1>
<Link className="button button--primary" href="/pl">
Wróć do strony głównej
</Link>
</main>
);
}
+5
View File
@@ -0,0 +1,5 @@
import {redirect} from 'next/navigation';
export default function RootPage() {
redirect('/pl');
}
+27
View File
@@ -0,0 +1,27 @@
import Link from 'next/link';
import type {ReactNode} from 'react';
type ButtonLinkProps = {
href: string;
children: ReactNode;
variant?: 'primary' | 'secondary' | 'ghost';
isExternal?: boolean;
};
export function ButtonLink({href, children, variant = 'primary', isExternal = false}: ButtonLinkProps) {
const className = `button button--${variant}`;
if (isExternal) {
return (
<a className={className} href={href} target="_blank" rel="noreferrer">
{children}
</a>
);
}
return (
<Link className={className} href={href}>
{children}
</Link>
);
}
+75
View File
@@ -0,0 +1,75 @@
'use client';
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 {DemoFrame} from './DemoFrame';
import type {DemoContent} from '@/types/content';
type DemoBrowserProps = {
demos: DemoContent[];
};
function isKnownDemo(slug: string | null, demos: DemoContent[]) {
return demos.some(demo => demo.slug === slug);
}
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 activeDemo = useMemo(
() => demos.find(demo => demo.slug === activeSlug) ?? demos[0],
[activeSlug, demos]
);
function selectDemo(slug: DemoContent['slug']) {
setActiveSlug(slug);
router.replace(`/pl/dema?demo=${slug}`, {scroll: false});
}
return (
<div className="demoBrowser">
<aside className="demoBrowser__sidebar" aria-label="Lista dem">
<p className="eyebrow">Wybierz demo</p>
<div className="demoBrowser__list">
{demos.map(demo => (
<button
className={demo.slug === activeDemo.slug ? 'demoBrowser__item isActive' : 'demoBrowser__item'}
key={demo.slug}
type="button"
onClick={() => selectDemo(demo.slug)}
>
<span className="demoBrowser__thumb">
{demo.imageUrl ? <Image src={demo.imageUrl} alt="" width={96} height={96} /> : <DoorOpen size={30} />}
</span>
<span>
<strong>{demo.title}</strong>
<small>{demo.eyebrow}</small>
</span>
</button>
))}
</div>
</aside>
<section className="demoBrowser__preview" aria-live="polite">
<DemoFrame title={activeDemo.title} url={activeDemo.iframeUrl} openLabel={activeDemo.openLabel} />
<div className="demoBrowser__details">
<div>
<p className="eyebrow">{activeDemo.eyebrow}</p>
<h2>{activeDemo.title}</h2>
<p>{activeDemo.intro}</p>
</div>
<Link className="button button--secondary" href={`/pl/${activeDemo.slug}`}>
Poczytaj więcej
<ArrowRight size={18} />
</Link>
</div>
</section>
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import {ExternalLink, Maximize2} from 'lucide-react';
type DemoFrameProps = {
title: string;
url: string;
openLabel: string;
};
export function DemoFrame({title, url, openLabel}: DemoFrameProps) {
return (
<div className="demoFrame">
<div className="demoFrame__bar">
<span>{title}</span>
<div className="demoFrame__actions">
<a href={url} target="_blank" rel="noreferrer" aria-label="Otwórz demo w nowej karcie">
<ExternalLink size={18} />
</a>
<a href={url} target="_blank" rel="noreferrer" aria-label="Pełny ekran">
<Maximize2 size={18} />
</a>
</div>
</div>
<div className="demoFrame__viewport">
<iframe title={title} src={url} loading="lazy" allowFullScreen />
<a className="demoFrame__fallback" href={url} target="_blank" rel="noreferrer">
{openLabel}
</a>
</div>
</div>
);
}
+87
View File
@@ -0,0 +1,87 @@
import {CheckCircle2, DoorOpen} from 'lucide-react';
import Image from 'next/image';
import {ButtonLink} from './ButtonLink';
import {DemoFrame} from './DemoFrame';
import {Faq} from './Faq';
import {SectionHeader} from './SectionHeader';
import {Stats} from './Stats';
import {contact} from '@/config/contact';
import type {DemoContent} from '@/types/content';
type DemoPageProps = {
demo: DemoContent;
};
export function DemoPage({demo}: DemoPageProps) {
return (
<main>
<section className="demoHero sectionBand sectionBand--light">
<div className="container demoHero__grid">
<div>
<p className="eyebrow">{demo.eyebrow}</p>
<h1>{demo.title}</h1>
<p>{demo.intro}</p>
<p>{demo.description}</p>
<div className="buttonRow">
<ButtonLink href="#podglad">Zobacz demo</ButtonLink>
<ButtonLink href={contact.url} variant="secondary" isExternal>
Porozmawiajmy
</ButtonLink>
</div>
</div>
{demo.imageUrl ? (
<Image src={demo.imageUrl} alt="" width={640} height={640} priority />
) : (
<div className="demoHero__visual" aria-hidden="true">
<DoorOpen size={112} />
</div>
)}
</div>
</section>
<section id="podglad" className="sectionBand">
<div className="container">
<DemoFrame title={demo.title} url={demo.iframeUrl} openLabel={demo.openLabel} />
</div>
</section>
<section className="sectionBand sectionBand--tint statsBand">
<div className="container">
<Stats stats={demo.stats} />
</div>
</section>
<section className="sectionBand">
<div className="container">
<SectionHeader title={demo.benefitsIntro} />
<div className="featureGrid featureGrid--three">
{demo.benefits.map(benefit => (
<article className="featureItem" key={benefit.title}>
<CheckCircle2 size={22} />
<h3>{benefit.title}</h3>
<p>{benefit.description}</p>
</article>
))}
</div>
</div>
</section>
<section className="sectionBand sectionBand--dark">
<div className="container finalCta finalCta--dark">
<h2>{demo.cta.title}</h2>
<p>{demo.cta.description}</p>
<ButtonLink href={contact.url} isExternal>
{demo.cta.button}
</ButtonLink>
</div>
</section>
<section className="sectionBand sectionBand--tint">
<div className="container narrow">
<SectionHeader title="FAQ" />
<Faq items={demo.faq} />
</div>
</section>
</main>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type {FaqItem} from '@/types/content';
type FaqProps = {
items: FaqItem[];
};
export function Faq({items}: FaqProps) {
return (
<div className="faqList">
{items.map((item, index) => (
<details key={`${index}-${item.question}`}>
<summary>{item.question}</summary>
<p>{item.answer}</p>
</details>
))}
</div>
);
}
+41
View File
@@ -0,0 +1,41 @@
import Link from 'next/link';
import {contact} from '@/config/contact';
import {Logo} from './Logo';
export function Footer() {
return (
<footer className="footer">
<div className="footer__inner">
<div className="footer__brand">
<Link href="/pl" aria-label="Ultifide konfiguratory 3D">
<Logo isWhite />
</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>
);
}
+108
View File
@@ -0,0 +1,108 @@
'use client';
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: '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}
];
export function Header() {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
const [activeSection, setActiveSection] = useState('platforma');
useEffect(() => {
if (pathname !== '/pl') {
return;
}
const sections = ['platforma', 'dema', 'funkcje', 'faq']
.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];
if (visibleEntry?.target.id) {
setActiveSection(visibleEntry.target.id);
}
},
{rootMargin: '-30% 0px -55% 0px', threshold: [0.1, 0.35, 0.6]}
);
sections.forEach(section => observer.observe(section));
return () => observer.disconnect();
}, [pathname]);
function isActive(itemId: string) {
if (itemId === 'kontakt') {
return false;
}
if (pathname === '/pl') {
return itemId === activeSection;
}
if (itemId === 'dema') {
return pathname === '/pl/dema' || pathname.startsWith('/pl/demo');
}
return false;
}
return (
<header className="siteHeader">
<div className="siteHeader__inner">
<Link className="siteHeader__logo" href="/pl" aria-label="Ultifide konfiguratory 3D">
<Logo />
</Link>
<nav className="siteHeader__nav" aria-label="Nawigacja glowna">
{navItems.map(item => (
<a className={isActive(item.id) ? 'isActive' : undefined} key={item.href} href={item.href}>
{item.label}
</a>
))}
</nav>
<a className="siteHeader__contact" href={contact.url}>
Porozmawiajmy
</a>
<button
className="siteHeader__menu"
type="button"
aria-label={isOpen ? 'Zamknij menu' : 'Otwórz menu'}
aria-expanded={isOpen}
onClick={() => setIsOpen(current => !current)}
>
{isOpen ? <X size={22} /> : <Menu size={22} />}
</button>
</div>
{isOpen ? (
<nav className="siteHeader__mobileNav" aria-label="Nawigacja mobilna">
{navItems.map(item => (
<a
className={isActive(item.id) ? 'isActive' : undefined}
key={item.href}
href={item.href}
onClick={() => setIsOpen(false)}
>
{item.label}
</a>
))}
</nav>
) : null}
</header>
);
}
+53
View File
@@ -0,0 +1,53 @@
import {Box, CheckCircle2, DoorOpen, Layers3, MousePointer2, PanelRight, SlidersHorizontal} from 'lucide-react';
export function HeroVisual() {
return (
<div className="heroVisual" aria-hidden="true">
<div className="heroVisual__topbar">
<span />
<span />
<span />
</div>
<div className="heroVisual__scene">
<div className="heroVisual__product">
<div className="heroVisual__cube heroVisual__cube--main">
<Box size={70} />
</div>
<div className="heroVisual__orbit heroVisual__orbit--one">
<DoorOpen size={28} />
</div>
<div className="heroVisual__orbit heroVisual__orbit--two">
<Layers3 size={28} />
</div>
<div className="heroVisual__cursor">
<MousePointer2 size={22} />
</div>
</div>
<div className="heroVisual__panel">
<div>
<PanelRight size={18} />
<span>Konfiguracja</span>
</div>
<div className="heroVisual__option isActive">
<CheckCircle2 size={16} />
Materiał premium
</div>
<div className="heroVisual__option">
<SlidersHorizontal size={16} />
Reguły produktu
</div>
<div className="heroVisual__swatches">
<span />
<span />
<span />
<span />
</div>
</div>
</div>
<div className="heroVisual__footer">
<span>3D preview</span>
<strong>API-ready</strong>
</div>
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
interface Props {
className?: string;
isWhite?: boolean;
}
const UltifideLogo = ({ className, isWhite }: Props) => (
<svg
className={className}
style={{ maxWidth: '100%', maxHeight: '100%', width: 'auto' }}
xmlns="http://www.w3.org/2000/svg"
width="284"
height="83"
viewBox="0 0 284 83"
fill="none"
>
<path
fill={isWhite ? '#FFFFFF' : '#001C44'}
d="M282.084 52.804c.453 0 .845.185 1.174.555.33.37.494.842.494 1.417 0 1.028-.72 1.973-2.162 2.836a19.642 19.642 0 01-4.698 1.972 19.893 19.893 0 01-4.822.616c-4.656 0-8.344-1.376-11.063-4.13-2.679-2.752-4.019-6.553-4.019-11.402 0-3.082.599-5.794 1.793-8.136 1.195-2.383 2.864-4.232 5.007-5.547 2.184-1.315 4.656-1.973 7.417-1.973 3.914 0 7.026 1.274 9.334 3.822 2.307 2.547 3.461 6 3.461 10.355 0 .822-.165 1.417-.494 1.787-.331.37-.866.555-1.608.555h-19.903c.371 7.068 3.729 10.602 10.075 10.602 1.607 0 2.988-.206 4.142-.617 1.153-.452 2.39-1.048 3.708-1.787 1.071-.617 1.793-.925 2.164-.925zm-10.817-19.909c-2.637 0-4.759.822-6.366 2.466-1.567 1.644-2.493 3.965-2.782 6.965h17.493c-.083-3.04-.845-5.363-2.287-6.965-1.443-1.644-3.462-2.466-6.058-2.466zm-24.439-17.061c.783 0 1.401.226 1.854.678.453.452.681 1.027.681 1.726v39.2c0 .74-.228 1.336-.681 1.788-.453.452-1.071.678-1.854.678-.783 0-1.401-.226-1.855-.678-.411-.452-.617-1.047-.617-1.787V54.11c-.907 1.89-2.246 3.349-4.018 4.376-1.731 1.027-3.77 1.54-6.119 1.54-2.638 0-4.965-.657-6.985-1.972-2.019-1.315-3.585-3.143-4.697-5.485-1.113-2.384-1.67-5.137-1.67-8.26 0-3.082.557-5.794 1.67-8.136 1.112-2.342 2.678-4.15 4.697-5.424 2.02-1.274 4.347-1.91 6.985-1.91 2.349 0 4.388.513 6.119 1.54 1.772 1.027 3.111 2.486 4.018 4.377v-16.52c0-.739.206-1.314.617-1.725.454-.452 1.072-.678 1.855-.678zm-11.621 40.064c2.926 0 5.172-.986 6.738-2.959 1.607-2.013 2.411-4.848 2.411-8.505 0-3.658-.804-6.472-2.411-8.445-1.566-1.972-3.812-2.958-6.738-2.958-2.925 0-5.212.986-6.86 2.958-1.608 1.973-2.411 4.746-2.411 8.321 0 3.657.803 6.513 2.411 8.568 1.648 2.013 3.935 3.02 6.86 3.02zM211.001 29.58c.7 0 1.236.184 1.607.554.371.37.557.863.557 1.48v25.825c0 .78-.248 1.397-.742 1.85-.454.41-1.03.616-1.731.616-.741 0-1.36-.206-1.854-.617-.454-.452-.68-1.068-.68-1.849V33.524h-13.537v23.915c0 .78-.247 1.397-.741 1.85-.453.41-1.03.616-1.731.616-.741 0-1.36-.206-1.854-.617-.454-.452-.68-1.068-.68-1.849V33.524h-4.141c-.701 0-1.257-.165-1.669-.493-.371-.37-.556-.843-.556-1.418 0-.616.185-1.11.556-1.48.412-.369.968-.554 1.669-.554h4.141v-1.048c0-3.698.927-6.595 2.781-8.69 1.855-2.137 4.513-3.35 7.974-3.637l1.484-.123c1.977-.165 2.966.452 2.966 1.849 0 1.191-.7 1.87-2.101 2.034l-1.484.123c-2.184.165-3.832.863-4.945 2.096-1.112 1.191-1.669 3-1.669 5.424v1.972h16.38zm-.309-13.314c.948 0 1.731.287 2.349.863.659.575.989 1.314.989 2.218 0 .945-.309 1.706-.927 2.281-.618.575-1.422.863-2.411.863-.989 0-1.792-.288-2.411-.863-.617-.575-.926-1.335-.926-2.28 0-.904.309-1.644.926-2.22.619-.575 1.422-.862 2.411-.862zm-34.678 43.639c-.741 0-1.36-.206-1.854-.617-.453-.452-.68-1.068-.68-1.849V31.43c0-.781.227-1.377.68-1.788.494-.452 1.113-.678 1.854-.678.742 0 1.34.226 1.793.678.453.41.68 1.007.68 1.787V57.44c0 .822-.227 1.438-.68 1.85-.453.41-1.051.616-1.793.616zm0-37.414c-.989 0-1.792-.288-2.41-.863-.618-.575-.927-1.335-.927-2.28 0-.904.309-1.644.927-2.22.618-.575 1.421-.862 2.41-.862.989 0 1.793.287 2.411.863.618.575.927 1.314.927 2.218 0 .946-.309 1.706-.927 2.281-.618.575-1.422.863-2.411.863zm-9.401 33.469c1.443.123 2.164.78 2.164 1.972 0 .698-.268 1.233-.804 1.602-.494.33-1.257.453-2.287.37l-1.669-.123c-3.296-.246-5.727-1.233-7.293-2.959-1.566-1.725-2.349-4.335-2.349-7.828v-15.47h-4.141c-.701 0-1.257-.165-1.669-.493-.371-.37-.557-.843-.557-1.418 0-.616.186-1.11.557-1.48.412-.37.968-.554 1.669-.554h4.141v-6.965c0-.78.227-1.377.68-1.788.453-.451 1.071-.678 1.854-.678.742 0 1.339.227 1.793.678.453.411.68 1.007.68 1.788v6.965h6.861c.659 0 1.174.185 1.545.555.412.37.618.863.618 1.479 0 .575-.206 1.048-.618 1.418-.371.328-.886.493-1.545.493h-6.861V49.24c0 2.26.453 3.904 1.359 4.931.948.986 2.349 1.541 4.204 1.664l1.668.123zm-25.761 3.944c-.742 0-1.36-.205-1.855-.616-.453-.452-.68-1.068-.68-1.85V18.3c0-.78.227-1.376.68-1.787.495-.452 1.113-.678 1.855-.678.7 0 1.277.226 1.73.678.495.41.742 1.007.742 1.787v39.14c0 .78-.247 1.397-.742 1.849-.453.41-1.03.616-1.73.616z"
/>
<path
fill={isWhite ? '#FFFFFF' : '#001C44'}
stroke="#001C44"
strokeWidth="0.617"
d="M105.338 56.904c2.312 2.127 5.687 3.165 10.073 3.165 4.356 0 7.715-1.038 10.026-3.165 2.346-2.158 3.498-5.277 3.498-9.306V30.811c0-.612-.198-1.127-.616-1.509-.415-.409-.971-.598-1.628-.598-.633 0-1.182.194-1.623.594l.207.228-.207-.228c-.421.382-.62.899-.62 1.513v17.087c0 2.888-.771 5.006-2.263 6.414-1.496 1.411-3.735 2.143-6.774 2.143-3.037 0-5.293-.731-6.821-2.144-1.491-1.408-2.262-3.526-2.262-6.413V30.811c0-.612-.198-1.127-.616-1.509-.412-.406-.95-.598-1.581-.598-.657 0-1.212.19-1.628.598-.417.382-.615.897-.615 1.509v16.787c0 4.055 1.135 7.175 3.45 9.306z"
/>
<g clipPath="url(#a)">
<path
fill={isWhite ? '#FFFFFF' : '#42A6FF'}
d="M.161 47.292a39.51 39.51 0 00.763 4.94 38.687 38.687 0 003.457 9.515A39.121 39.121 0 0024.56 80.023h.01c11.485-5.528 19.928-16.435 22.207-29.396 0-.014-.013-.02-.023-.01a10.276 10.276 0 01-5.24 3.216c-.746.188-1.529.3-2.328.313-5.799.112-10.545-4.776-10.545-10.574V28.56c0-.026.004-.05.004-.076V21.03a2.196 2.196 0 00-2.483-2.17c-1.102.144-1.895 1.14-1.895 2.25v4.43c0 .447-.319.861-.766.9a.844.844 0 01-.921-.841v-8.341c0-1.043-.74-1.905-1.717-2.125-.052-.01-.102-.03-.154-.036a2.114 2.114 0 00-.612-.007c-1.102.145-1.898 1.142-1.898 2.25v4.493a.85.85 0 01-.131.45.834.834 0 01-.895.375c-.395-.088-.658-.467-.658-.871v-9.058c0-.97-.638-1.796-1.516-2.082a2.152 2.152 0 00-.967-.086c-1.102.145-1.895 1.142-1.895 2.25v14.238c0 .447-.318.862-.766.901a.844.844 0 01-.92-.842v-9.85a2.195 2.195 0 00-2.484-2.17c-1.102.144-1.894 1.14-1.894 2.249v5.999a.83.83 0 01-.25.595.834.834 0 01-.655.247c-.454-.03-.79-.447-.79-.905v-2.167c0-1.108-.792-2.105-1.894-2.25A2.198 2.198 0 000 21.026l.007 22.776.154 3.486v.004z"
/>
</g>
<g clipPath="url(#b)">
<path
fill={isWhite ? '#FFFFFF' : '#42A6FF'}
d="M26.713 80.461a38.476 38.476 0 0010.926 1.885h.017c.397 0 .792.016 1.19.016 21.54-.082 38.846-17.869 38.846-39.408V15.613c0-7.703-6.134-14.264-13.77-14.597-.198 0-.415-.016-.63-.016a14.37 14.37 0 00-12.941 8.282 13.898 13.898 0 00-1.29 4.894v28.778c0 .197 0 .414-.016.612v1.273c0 1.338-.066 2.644-.197 3.933a34.57 34.57 0 01-.447 3.24c0 .016-.017.033-.017.05a37.484 37.484 0 01-.908 3.85c-.066.23-.148.48-.213.71-.132.415-.264.826-.415 1.24-3.453 9.937-10.761 18.1-20.135 22.6z"
/>
</g>
<path
fill={isWhite ? '#FFFFFF' : '#42A6FF'}
d="M27.485 65.688c-3.471 1.138 2.255 3.087-1.567 3.186l2.11 1.063c-18.551-12.194-10.155-7.962 2.979 4.248C9.354 74.102 2.1 63.715 0 43.342v-27.66C0 7.89 6.167 1.252 13.844.916c.198 0 .416-.017.631-.017 5.75.033 10.72 3.444 13.01 8.378a14.134 14.134 0 011.296 4.951v29.114c0 .2 0 .419.017.618v1.288c0 1.354.066 2.675.198 3.98.1 1.104.248 2.189.45 3.277 0 .017.016.033.016.05a38.5 38.5 0 00.913 3.896c.066.233.149.486.215.719.132.419.264.835.416 1.254 3.472 10.052-12.943 2.712-3.52 7.264z"
/>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h46.776v69.486H0z" transform="matrix(-1 0 0 1 46.776 10.537)" />
</clipPath>
<clipPath id="b">
<path fill="#fff" d="M0 0h50.979v81.362H0z" transform="matrix(-1 0 0 1 77.692 1)" />
</clipPath>
</defs>
</svg>
);
UltifideLogo.defaultProps = {
isWhite: false,
};
export default UltifideLogo;
export const Logo = UltifideLogo;
+15
View File
@@ -0,0 +1,15 @@
type SectionHeaderProps = {
eyebrow?: string;
title: string;
description?: string;
};
export function SectionHeader({eyebrow, title, description}: SectionHeaderProps) {
return (
<div className="sectionHeader">
{eyebrow ? <p className="eyebrow">{eyebrow}</p> : null}
<h2>{title}</h2>
{description ? <p>{description}</p> : null}
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import type {Stat} from '@/types/content';
type StatsProps = {
stats: Stat[];
};
export function Stats({stats}: StatsProps) {
return (
<div className="statsGrid">
{stats.map(stat => (
<div className="statItem" key={`${stat.value}-${stat.label}`}>
<strong>{stat.value}</strong>
<span>{stat.label}</span>
</div>
))}
</div>
);
}
+8
View File
@@ -0,0 +1,8 @@
export const contact = {
url: 'https://ultifide.com/contact',
email: 'contact@ultifide.com',
phone: '+48 733 226 544',
address: ['Cracow, Poland', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'],
linkedin: 'https://pl.linkedin.com/company/ultifide',
facebook: 'https://facebook.com/ultifide'
} as const;
+29
View File
@@ -0,0 +1,29 @@
import messages from '../../messages/pl.json';
import {demoUrls} from './demoUrls';
import type {DemoContent, FaqItem, FeatureItem, Stat} from '@/types/content';
type HomeMessages = typeof messages.home;
type DemoMessages = Omit<DemoContent, 'iframeUrl' | 'slug'> & {
slug: string;
};
export const homeContent: HomeMessages = messages.home;
function createDemoContent(demo: DemoMessages, iframeUrl: string): DemoContent {
return {
...demo,
iframeUrl,
slug: demo.slug as DemoContent['slug'],
stats: demo.stats as Stat[],
benefits: demo.benefits as FeatureItem[],
faq: demo.faq as FaqItem[]
};
}
export const demoContent = {
desk: createDemoContent(messages.demos.desk, demoUrls.desk),
room: createDemoContent(messages.demos.room, demoUrls.room),
door: createDemoContent(messages.demos.door, demoUrls.door)
} as const;
export const marketingFaq: FaqItem[] = [...demoContent.desk.faq, ...demoContent.room.faq];
+5
View File
@@ -0,0 +1,5 @@
export const demoUrls = {
desk: 'https://3d-desk-demo.ultifide.com/',
room: 'https://3d-demo.ultifide.com/room-editor?h=1',
door: 'https://3d-demo.ultifide.com/product-configurator/door'
} as const;
+13
View File
@@ -0,0 +1,13 @@
import {hasLocale} from 'next-intl';
import {getRequestConfig} from 'next-intl/server';
import {routing} from './routing';
export default getRequestConfig(async ({requestLocale}) => {
const requestedLocale = await requestLocale;
const locale = hasLocale(routing.locales, requestedLocale) ? requestedLocale : routing.defaultLocale;
return {
locale,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
+7
View File
@@ -0,0 +1,7 @@
import {defineRouting} from 'next-intl/routing';
export const routing = defineRouting({
locales: ['pl'],
defaultLocale: 'pl',
localePrefix: 'always'
});
+8
View File
@@ -0,0 +1,8 @@
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|_next|_vercel|.*\\..*).*)'
};
+6
View File
@@ -0,0 +1,6 @@
$primary: #42a6ff;
$secondary: #001c44;
$tertiary: #f9fafe;
$white: #ffffff;
$gray: rgba(157, 158, 180, 1);
$gray-light: rgba(115, 130, 151, 1);
+39
View File
@@ -0,0 +1,39 @@
export type Stat = {
value: string;
label: string;
};
export type LinkItem = {
label: string;
href: string;
};
export type FeatureItem = {
title: string;
description: string;
};
export type FaqItem = {
question: string;
answer: string;
};
export type DemoContent = {
slug: 'demo' | 'demo-room' | 'demo-door';
title: string;
eyebrow: string;
intro: string;
description: string;
imageUrl?: string;
iframeUrl: string;
openLabel: string;
stats: Stat[];
benefitsIntro: string;
benefits: FeatureItem[];
faq: FaqItem[];
cta: {
title: string;
description: string;
button: string;
};
};