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
+9
View File
@@ -0,0 +1,9 @@
.git
.idea
.next
.playwright-mcp
node_modules
npm-debug.log*
Dockerfile
docker-compose.yml
.DS_Store
+26
View File
@@ -0,0 +1,26 @@
node_modules/
.next/
out/
build/
dist/
.env
.env*.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
.DS_Store
Thumbs.db
.idea/
.vscode/
.playwright-mcp/
playwright-report/
test-results/
*.tsbuildinfo
+40
View File
@@ -0,0 +1,40 @@
FROM node:24-alpine AS deps
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./
RUN npm ci
FROM node:24-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:24-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
+21
View File
@@ -0,0 +1,21 @@
# Ultifide 3D Configurator Landing
## Docker
Run the production app on port `3000`:
```bash
docker compose up --build -d
```
On a VM, open:
```text
http://<VM_IP>:3000
```
Stop it with:
```bash
docker compose down
```
+15
View File
@@ -0,0 +1,15 @@
services:
configurator-landing:
build:
context: .
dockerfile: Dockerfile
image: ultifide-3d-configurator-landing:latest
container_name: ultifide-3d-configurator-landing
restart: unless-stopped
ports:
- "3000:3000"
environment:
NODE_ENV: production
NEXT_TELEMETRY_DISABLED: "1"
HOSTNAME: "0.0.0.0"
PORT: "3000"
+6
View File
@@ -0,0 +1,6 @@
import nextVitals from 'eslint-config-next/core-web-vitals';
import nextTypescript from 'eslint-config-next/typescript';
const eslintConfig = [...nextVitals, ...nextTypescript];
export default eslintConfig;
+192
View File
@@ -0,0 +1,192 @@
{
"metadata": {
"homeTitle": "Konfiguratory 3D dla e-commerce i B2B | Ultifide",
"homeDescription": "Landing Ultifide dla konfiguratorów 3D: produktowych, wnętrzarskich i modułowych. Zobacz dema i sprawdź, jak możemy wdrożyć konfigurator w Twoim sklepie.",
"deskTitle": "Konfigurator 3D biurka | Ultifide",
"roomTitle": "Konfigurator 3D wnętrz | Ultifide",
"doorTitle": "Konfigurator 3D drzwi | Ultifide"
},
"navigation": {
"platform": "Platforma",
"demos": "Dema",
"features": "Funkcje",
"faq": "FAQ",
"contact": "Kontakt",
"menu": "Menu"
},
"common": {
"seeDemo": "Zobacz demo",
"openDemo": "Otwórz demo",
"contactUs": "Porozmawiajmy",
"learnMore": "Dowiedz się więcej",
"preview": "Podgląd konfiguratora",
"fullscreen": "Pełny ekran"
},
"home": {
"hero": {
"eyebrow": "Konfiguratory produktów i przestrzeni",
"title": "Konfiguratory 3D dla sprzedaży produktów i wnętrz",
"description": "Projektujemy i wdrażamy interaktywne konfiguratory 3D, które pozwalają klientom zobaczyć wariant produktu, zbudować zestaw i szybciej podjąć decyzję zakupową.",
"primaryCta": "Zobacz dema",
"secondaryCta": "Umów rozmowę"
},
"stats": [
{"value": "25%", "label": "wyższa konwersja na stronach produktowych"},
{"value": "35%", "label": "mniej pytań o warianty i specyfikację"},
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
],
"demosTitle": "Aktualne dema konfiguratorów",
"demosDescription": "Trzy gotowe kierunki, które można rozbudować o Twoje produkty, reguły biznesowe i integracje z systemem sprzedaży.",
"benefitsTitle": "Platforma przygotowana pod realną sprzedaż",
"benefitsDescription": "Nie chodzi o efektowny model 3D na stronie. Konfigurator ma skrócić proces decyzji, zmniejszyć liczbę niejasności i dostarczyć kompletne dane do koszyka, CRM lub ERP.",
"benefits": [
{"title": "Personalizacja w czasie rzeczywistym", "description": "Klient zmienia kolory, materiały, dodatki i układ bez oczekiwania na render."},
{"title": "Reguły produktu", "description": "Zależności, ograniczenia, wariantowe SKU i dostępność mogą wynikać z logiki Twojego katalogu."},
{"title": "Integracje i automatyzacja", "description": "API, webhooki i eksport danych pozwalają połączyć konfigurator ze sklepem, CRM, ERP albo panelem B2B."},
{"title": "Dane dla sprzedaży", "description": "Konfiguracja może generować specyfikację, koszyk, zapytanie ofertowe lub dane dla panelu B2B."}
],
"industriesTitle": "Dla produktów, które trzeba zobaczyć przed zakupem",
"industries": [
{"title": "Meble biurowe", "description": "Biurka, krzesła, akcesoria i stanowiska pracy z wieloma wariantami."},
{"title": "Wnętrza i kolekcje", "description": "Pokoje, zestawy mebli, systemy modułowe i gotowe aranżacje."},
{"title": "Produkty custom", "description": "Drzwi, oświetlenie, wyposażenie i inne produkty wymagające dopasowania."}
],
"featuresTitle": "Funkcje, które zwykle są potrzebne w konfiguratorze",
"features": [
"Realistyczne materiały i oświetlenie sceny",
"Widok 360 stopni, zoom i praca na detalach",
"Responsywny interfejs dla desktopu i mobile",
"Opcjonalny tryb AR",
"Integracje API, webhooki i eksport danych",
"Motyw graficzny dopasowany do marki"
],
"processTitle": "Jak pracujemy",
"process": [
{"title": "Analiza katalogu", "description": "Porządkujemy warianty, zależności, dane produktowe i zakres konfiguracji."},
{"title": "Model 3D i UX", "description": "Przygotowujemy modele, materiały oraz interfejs dobrany do sposobu zakupu."},
{"title": "Wdrożenie i integracje", "description": "Osadzamy konfigurator w sklepie lub panelu B2B i spinamy go z procesem sprzedaży."}
],
"faq": [
{"question": "Czy możemy zacząć od jednego produktu?", "answer": "Tak. V1 może obejmować jeden produkt lub jedną kolekcję, 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, osobna podstrona, kreator zestawów lub moduł w panelu B2B."},
{"question": "Czy UI da się dopasować do marki?", "answer": "Tak. Kolory, układ opcji, przyciski i sposób interakcji mogą wyglądać jak natywna część Twojego sklepu."}
],
"finalCta": {
"title": "Zmień sposób, w jaki klienci wybierają produkty",
"description": "Pokaż nam katalog, a zaproponujemy pierwszy zakres konfiguratora 3D i sensowną ścieżkę wdrożenia.",
"button": "Umów rozmowę"
}
},
"demos": {
"desk": {
"slug": "demo",
"eyebrow": "Demo produktowe",
"title": "Konfigurator 3D, który sprzedaje za Ciebie",
"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.",
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/b64b9dddfb838bfafea9da0ee5c04c68.png?width=1000&height=1000",
"openLabel": "Otwórz konfigurator biurka",
"benefitsIntro": "Rozwiązanie dla producentów i sklepów, które sprzedają produkty z wieloma wariantami, dodatkami i zależnościami.",
"stats": [
{"value": "25%", "label": "wyższa konwersja na stronach produktowych"},
{"value": "35%", "label": "mniej zapytań o warianty i specyfikację"},
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
],
"benefits": [
{"title": "Realistyczna prezentacja produktów", "description": "Materiały, tekstury, dynamiczne oświetlenie i wierne odwzorowanie kolorów."},
{"title": "Personalizacja w czasie rzeczywistym", "description": "Zmiana blatu, stelaża i akcesoriów z natychmiastowym podglądem konfiguracji."},
{"title": "Mniej zwrotów i pytań", "description": "Klient widzi dokładnie co zamawia, łącznie z dodatkami i detalami."},
{"title": "Integracja z e-commerce", "description": "Shopify, WooCommerce, Magento, Shopware lub customowy CMS przez API."},
{"title": "Automatyzacja ofertowania", "description": "Specyfikacje PDF, dane do ERP/CRM, wariantowe SKU i eksport konfiguracji do koszyka."},
{"title": "Modułowa rozbudowa", "description": "Dodawanie kolejnych wariantów, akcesoriów i konfiguracji bez przebudowy systemu."}
],
"faq": [
{"question": "Czy mogę dodać własne modele 3D?", "answer": "Tak. Możemy zaimportować gotowe modele albo przygotować i zoptymalizować je pod konfigurator."},
{"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 można zmieniać logikę i zależności produktów?", "answer": "Tak. Obsługujemy warunki, ograniczenia, wariantowe SKU i reguły wynikające z konstrukcji produktu."},
{"question": "Jak wygląda integracja ze sklepem?", "answer": "Konfigurator może działać jako widżet, dedykowana podstrona, moduł koszyka lub panel B2B, z integracją przez API i webhooki."},
{"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 zmniejszać koszty przygotowania wielu wizualizacji."},
{"question": "Czy mogę dodawać nowe warianty produktów i akcesoria?", "answer": "Tak. Konfigurator jest skalowalny i modułowy, więc można rozwijać go o nowe kolory, materiały, dodatki, akcesoria i całe zestawy produktów."},
{"question": "Czy wygląd konfiguratora można dopasować do mojej marki?", "answer": "Tak. Możemy dostosować UI, branding, kolory, układ opcji, modele 3D, materiały, sposób rotacji, zoom i środowisko sceny."},
{"question": "Czy konfigurator nadaje się także do produktów modułowych?", "answer": "Tak. Obsługuje biurka podnoszone, meble modułowe, systemy elementów i akcesoria dodawane w różnych konfiguracjach."}
],
"cta": {
"title": "Zmień sposób, w jaki Twoi klienci kupują meble",
"description": "Interaktywny konfigurator 3D skraca proces sprzedaży i podnosi wartość koszyka.",
"button": "Umów rozmowę"
}
},
"room": {
"slug": "demo-room",
"eyebrow": "Demo wnętrzarskie",
"title": "Konfigurator 3D, który sprzedaje całe wnętrza",
"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.",
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/e78aac1388ff07761422e12272690878.png?width=1000&height=1000",
"openLabel": "Otwórz konfigurator wnętrz",
"benefitsIntro": "Dla producentów mebli i marek wnętrzarskich, które chcą sprzedawać całe doświadczenie przestrzeni.",
"stats": [
{"value": "25%", "label": "wyższa konwersja na stronach produktowych"},
{"value": "35%", "label": "mniej zapytań o warianty i specyfikację"},
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
],
"benefits": [
{"title": "Realistyczne pomieszczenie", "description": "Pełne sceny 3D od pustego pokoju po gotowe aranżacje."},
{"title": "Aranżacja na żywo", "description": "Zmiana układu, kolorów, materiałów i dodatków bez czekania na render."},
{"title": "Lepsze dopasowanie", "description": "Klient sprawdza, jak produkty wspólnie wyglądają w konkretnej przestrzeni."},
{"title": "Mniej nietrafionych zakupów", "description": "Jasny podgląd zestawu ogranicza niepewność przed finalizacją zamówienia."},
{"title": "Integracja ze sprzedażą", "description": "Konfigurator może przekazywać zestawy do koszyka, zapytania lub panelu B2B."},
{"title": "Kolekcje i systemy", "description": "System można rozwijać o nowe meble, style, układy pomieszczeń i kolekcje."}
],
"faq": [
{"question": "Czy mogę odwzorować całe pomieszczenia?", "answer": "Tak. Możemy stworzyć pełne sceny 3D i gotowe aranżacje z konfigurowalnymi meblami, kolorami i materiałami."},
{"question": "Czy klient może zmieniać układ mebli?", "answer": "Tak. Konfigurator może pozwalać na ustawianie elementów, zmianę układu i dopasowanie do przestrzeni."},
{"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 można ustawić zależności między elementami?", "answer": "Tak. Możemy wdrożyć reguły zgodności modułów, ograniczenia przestrzenne i logiczne zestawy produktów."},
{"question": "Jak wygląda integracja z moim sklepem?", "answer": "Konfigurator może działać jako widżet, dedykowana podstrona, kreator zestawów lub panel B2B. Integrujemy przez API, webhooki i popularne platformy e-commerce."},
{"question": "Czy mogę rozwijać konfigurator o nowe meble i kolekcje?", "answer": "Tak. System jest modułowy, więc można dodawać nowe produkty, style, układy pomieszczeń i całe kolekcje bez przebudowy."},
{"question": "Czy wygląd konfiguratora można dopasować do mojej marki?", "answer": "Tak. UI, kolory, styl wizualny, sposób interakcji i całe doświadczenie mogą być dopasowane do Twojego brandu."},
{"question": "Czy to działa dla mebli modułowych i systemów?", "answer": "Tak. Konfigurator świetnie sprawdza się przy systemach modułowych, zestawach i kolekcjach aranżacyjnych."}
],
"cta": {
"title": "Zmień sposób, w jaki Twoi klienci urządzają wnętrza",
"description": "Interaktywny konfigurator 3D pozwala sprzedawać nie tylko produkty, ale całe przestrzenie.",
"button": "Umów rozmowę"
}
},
"door": {
"slug": "demo-door",
"eyebrow": "Demo produktowe",
"title": "Konfigurator 3D drzwi dla producentów i sprzedawców",
"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.",
"imageUrl": "/demo-door-preview.svg",
"openLabel": "Otwórz konfigurator drzwi",
"benefitsIntro": "Dla firm, które sprzedają drzwi, fronty, zabudowy i produkty wymagające precyzyjnego dopasowania wariantów.",
"stats": [
{"value": "25%", "label": "wyższa konwersja na stronach produktowych"},
{"value": "35%", "label": "mniej zapytań o warianty i specyfikację"},
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
],
"benefits": [
{"title": "Warianty bez chaosu", "description": "Modele, kolory, przeszklenia, okucia i akcesoria mogą być prowadzone przez jasne reguły wyboru."},
{"title": "Realistyczny podgląd", "description": "Klient od razu widzi wygląd drzwi, zamiast porównywać osobne próbki i zdjęcia."},
{"title": "Mniej błędów w zamówieniach", "description": "Konfigurator może blokować niedostępne kombinacje i przekazywać komplet danych do sprzedaży."},
{"title": "Integracja z ofertowaniem", "description": "Wybrana konfiguracja może generować zapytanie, specyfikację albo dane do koszyka."},
{"title": "Dopasowanie do marki", "description": "Interfejs, kolory i sposób prezentacji można przygotować pod identyfikację producenta lub sklepu."},
{"title": "Rozbudowa katalogu", "description": "System można rozwijać o kolejne linie produktowe, materiały i reguły biznesowe."}
],
"faq": [
{"question": "Czy konfigurator może obsługiwać zależności między wariantami?", "answer": "Tak. Możemy uwzględnić zależności między modelem, rozmiarem, przeszkleniem, kolorem, okuciami i dostępnością."},
{"question": "Czy dane konfiguracji można wysłać do koszyka lub CRM?", "answer": "Tak. Konfiguracja może trafiać do koszyka, formularza zapytania, CRM, ERP albo panelu B2B."},
{"question": "Czy można dodać własne modele i materiały?", "answer": "Tak. Możemy pracować na dostarczonych modelach albo przygotować i zoptymalizować je pod konfigurator."},
{"question": "Czy konfigurator działa jako osobna podstrona lub widżet?", "answer": "Tak. Możemy wdrożyć go jako dedykowaną stronę, widżet produktowy albo element procesu ofertowego."}
],
"cta": {
"title": "Pokaż klientom drzwi dokładnie w wybranym wariancie",
"description": "Konfigurator 3D porządkuje wybór, skraca rozmowy sprzedażowe i ogranicza ryzyko pomyłek w specyfikacji.",
"button": "Umów rozmowę"
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+20
View File
@@ -0,0 +1,20 @@
import createNextIntlPlugin from 'next-intl/plugin';
import type {NextConfig} from 'next';
const withNextIntl = createNextIntlPlugin();
const nextConfig: NextConfig = {
reactStrictMode: true,
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'backend.ultifide.com',
pathname: '/uploads/**'
}
]
}
};
export default withNextIntl(nextConfig);
+6748
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "3d-configurator-landing",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint ."
},
"dependencies": {
"lucide-react": "latest",
"next": "latest",
"next-intl": "latest",
"react": "latest",
"react-dom": "latest",
"sass": "latest",
"sharp": "^0.34.5"
},
"devDependencies": {
"@eslint/eslintrc": "latest",
"@types/node": "latest",
"@types/react": "latest",
"@types/react-dom": "latest",
"eslint": "latest",
"eslint-config-next": "latest",
"typescript": "latest"
}
}
+31
View File
@@ -0,0 +1,31 @@
<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>

After

Width:  |  Height:  |  Size: 2.5 KiB

+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;
};
};
+42
View File
@@ -0,0 +1,42 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}