initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.idea
|
||||
.next
|
||||
.playwright-mcp
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.DS_Store
|
||||
+26
@@ -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
@@ -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"]
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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ę"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+6
@@ -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.
|
||||
@@ -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);
|
||||
Generated
+6748
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {redirect} from 'next/navigation';
|
||||
|
||||
export default function RootPage() {
|
||||
redirect('/pl');
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import {defineRouting} from 'next-intl/routing';
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['pl'],
|
||||
defaultLocale: 'pl',
|
||||
localePrefix: 'always'
|
||||
});
|
||||
@@ -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|.*\\..*).*)'
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user