rework components v2
This commit is contained in:
+82
-14
@@ -1,30 +1,97 @@
|
|||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
"defaultTitle": "Konfiguratory 3D | Ultifide",
|
||||||
|
"defaultDescription": "Konfiguratory 3D dla produktów, wnętrz i sprzedaży B2B.",
|
||||||
"homeTitle": "Konfiguratory 3D dla e-commerce i B2B | Ultifide",
|
"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.",
|
"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.",
|
||||||
|
"demosPageTitle": "Dema konfiguratorów 3D | Ultifide",
|
||||||
|
"demosPageDescription": "Lista interaktywnych dem konfiguratorów 3D Ultifide z podglądem biurka, wnętrz i drzwi.",
|
||||||
"deskTitle": "Konfigurator 3D biurka | Ultifide",
|
"deskTitle": "Konfigurator 3D biurka | Ultifide",
|
||||||
|
"deskDescription": "Poznaj konfigurator 3D, który zwiększa konwersję i pozwala klientom tworzyć własne biurka regulowane w czasie rzeczywistym.",
|
||||||
"roomTitle": "Konfigurator 3D wnętrz | Ultifide",
|
"roomTitle": "Konfigurator 3D wnętrz | Ultifide",
|
||||||
"doorTitle": "Konfigurator 3D drzwi | Ultifide"
|
"roomDescription": "Poznaj konfigurator 3D, który pozwala klientom projektować całe pomieszczenia i zestawy mebli w czasie rzeczywistym.",
|
||||||
|
"doorTitle": "Konfigurator 3D drzwi | Ultifide",
|
||||||
|
"doorDescription": "Poznaj konfigurator 3D drzwi, który pozwala klientom dobrać model, kolor, przeszklenie, klamkę i detale w czasie rzeczywistym."
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
|
"dema": "Dema",
|
||||||
"platform": "Platforma",
|
"platform": "Platforma",
|
||||||
"demos": "Demo",
|
"demos": "Demo",
|
||||||
"features": "Funkcje",
|
"features": "Funkcje",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"menu": "Menu"
|
"menu": "Menu",
|
||||||
|
"mainNav": "Nawigacja główna",
|
||||||
|
"mobileNav": "Nawigacja mobilna",
|
||||||
|
"openMenu": "Otwórz menu",
|
||||||
|
"closeMenu": "Zamknij menu",
|
||||||
|
"logoAriaLabel": "Ultifide konfiguratory 3D"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"seeDemo": "Zobacz demo",
|
"seeDemo": "Zobacz demo",
|
||||||
|
"seeMore": "Zobacz więcej",
|
||||||
"openDemo": "Otwórz demo",
|
"openDemo": "Otwórz demo",
|
||||||
"contactUs": "Porozmawiajmy",
|
"contactUs": "Porozmawiajmy",
|
||||||
"learnMore": "Dowiedz się więcej",
|
"learnMore": "Dowiedz się więcej",
|
||||||
"preview": "Podgląd konfiguratora",
|
"preview": "Podgląd konfiguratora",
|
||||||
"fullscreen": "Pełny ekran"
|
"fullscreen": "Pełny ekran",
|
||||||
|
"openInNewTab": "Otwórz demo w nowej karcie",
|
||||||
|
"close": "Zamknij",
|
||||||
|
"backToList": "< Wróć do listy"
|
||||||
},
|
},
|
||||||
|
"contact": {
|
||||||
|
"eyebrow": "Kontakt",
|
||||||
|
"title": "Porozmawiajmy!",
|
||||||
|
"description": "Opisz katalog produktów, zakres wariantów albo pierwszy pomysł na demo. Wrócimy z konkretną ścieżką wdrożenia.",
|
||||||
|
"schedulerTitle": "Umów rozmowę z Ultifide",
|
||||||
|
"schedulerFallback": "Umów rozmowę online w kalendarzu",
|
||||||
|
"schedulerUrl": "https://42min.us/kubapyla",
|
||||||
|
"email": "contact@ultifide.com",
|
||||||
|
"phones": ["+48 733 226 544", "+48 664 565 858"],
|
||||||
|
"address": ["Kraków, Polska", "ul. św. Wawrzyńca 19/2", "31-060 Kraków"]
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"copyright": "Copyright © 2026, Ultifide"
|
||||||
|
},
|
||||||
|
"notFound": {
|
||||||
|
"title": "Nie znaleziono strony",
|
||||||
|
"backHome": "Wróć do strony głównej"
|
||||||
|
},
|
||||||
|
"demosPage": {
|
||||||
|
"title": "Nasze konfiguratory 3D",
|
||||||
|
"description": "Wybierz demo z listy, przetestuj je od razu w podglądzie, lub przejdź do podstrony z opisem zastosowania.",
|
||||||
|
"selectDemo": "Wybierz demo",
|
||||||
|
"demoListAriaLabel": "Lista dem"
|
||||||
|
},
|
||||||
|
"demoSubnav": {
|
||||||
|
"ariaLabel": "Dema konfiguratorów",
|
||||||
|
"labels": {
|
||||||
|
"demo-pokoj": "Wnętrza",
|
||||||
|
"demo-drzwi": "Drzwi",
|
||||||
|
"demo-biurko": "Biurko"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heroVisual": {
|
||||||
|
"configuration": "Konfiguracja",
|
||||||
|
"premiumMaterial": "Materiał premium",
|
||||||
|
"productRules": "Reguły produktu"
|
||||||
|
},
|
||||||
|
"marketingFaq": [
|
||||||
|
{"question": "Czy możemy zacząć od jednego produktu?", "answer": "Tak. V1 może obejmować jeden produkt, jedną kolekcję albo jedno demo, a potem rosnąć o kolejne warianty, reguły i integracje."},
|
||||||
|
{"question": "Czy konfigurator może być częścią mojego sklepu?", "answer": "Tak. Może działać jako widżet, dedykowana podstrona, kreator zestawów, moduł koszyka albo element panelu B2B."},
|
||||||
|
{"question": "Jak wygląda integracja ze sprzedażą, CRM lub ERP?", "answer": "Konfiguracja może trafiać do koszyka, formularza zapytania, CRM, ERP albo panelu B2B. Integrujemy przez API, webhooki i popularne platformy e-commerce."},
|
||||||
|
{"question": "Czy mogę dodać własne modele 3D, materiały i warianty?", "answer": "Tak. Możemy pracować na dostarczonych modelach albo przygotować i zoptymalizować je pod konfigurator, a potem rozwijać katalog o nowe kolory, materiały, dodatki i akcesoria."},
|
||||||
|
{"question": "Czy konfigurator obsługuje zależności i reguły produktu?", "answer": "Tak. Obsługujemy warunki, ograniczenia, wariantowe SKU, zgodność modułów i zależności między modelem, rozmiarem, kolorem, okuciami, dodatkami lub dostępnością."},
|
||||||
|
{"question": "Czy konfigurator działa na urządzeniach mobilnych?", "answer": "Tak. Interfejs jest responsywny, a w wybranych wdrożeniach można dodać tryb AR."},
|
||||||
|
{"question": "Czy wygląd konfiguratora można dopasować do mojej marki?", "answer": "Tak. UI, kolory, branding, układ opcji, sposób interakcji, modele 3D i środowisko sceny mogą wyglądać jak natywna część Twojego sklepu."},
|
||||||
|
{"question": "Czy konfigurator sprawdzi się dla mebli modułowych i całych kolekcji?", "answer": "Tak. System można rozwijać o meble modułowe, zestawy, kolekcje aranżacyjne, nowe produkty i kolejne układy pomieszczeń bez przebudowy całości."},
|
||||||
|
{"question": "Czy można odwzorować całe pomieszczenia?", "answer": "Tak. Możemy stworzyć pełne sceny 3D od pustego pokoju po gotowe aranżacje z konfigurowalnymi meblami, kolorami i materiałami."},
|
||||||
|
{"question": "Czy klient może zmieniać układ mebli i elementów?", "answer": "Tak. Konfigurator może pozwalać na ustawianie elementów, zmianę układu, dobór dodatków i dopasowanie konfiguracji do przestrzeni."},
|
||||||
|
{"question": "Jakie są główne korzyści z wdrożenia konfiguratora 3D?", "answer": "Konfigurator pozwala prezentować produkty w jakości premium, personalizować je w czasie rzeczywistym, ograniczać liczbę zwrotów i pytań oraz skracać proces decyzji zakupowej."},
|
||||||
|
{"question": "Czy konfigurator nadaje się do produktów takich jak drzwi, fronty lub zabudowy?", "answer": "Tak. Sprawdza się przy produktach wymagających precyzyjnego dopasowania wariantów, takich jak drzwi, fronty, zabudowy, oświetlenie, wyposażenie i inne produkty custom."}
|
||||||
|
],
|
||||||
"home": {
|
"home": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"eyebrow": "Konfiguratory produktów i przestrzeni",
|
|
||||||
"title": "Konfiguratory 3D dla sprzedaży produktów i wnętrz",
|
"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ą.",
|
"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",
|
"primaryCta": "Zobacz dema",
|
||||||
@@ -35,8 +102,8 @@
|
|||||||
{"value": "35%", "label": "mniej pytań o warianty i specyfikację"},
|
{"value": "35%", "label": "mniej pytań o warianty i specyfikację"},
|
||||||
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
|
{"value": "3x", "label": "szybsze podejmowanie decyzji zakupowej"}
|
||||||
],
|
],
|
||||||
"demosTitle": "Aktualne dema konfiguratorów",
|
"demosTitle": "Dema konfiguratorów",
|
||||||
"demosDescription": "Trzy gotowe kierunki, które można rozbudować o Twoje produkty, reguły biznesowe i integracje z systemem sprzedaży.",
|
"demosDescription": "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ż",
|
"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.",
|
"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": [
|
"benefits": [
|
||||||
@@ -51,7 +118,7 @@
|
|||||||
{"title": "Wnętrza i kolekcje", "description": "Pokoje, zestawy mebli, systemy modułowe i gotowe aranżacje."},
|
{"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."}
|
{"title": "Produkty custom", "description": "Drzwi, oświetlenie, wyposażenie i inne produkty wymagające dopasowania."}
|
||||||
],
|
],
|
||||||
"featuresTitle": "Funkcje, które zwykle są potrzebne w konfiguratorze",
|
"featuresTitle": "Funkcje niezbędne w konfiguratorze idealnym!",
|
||||||
"features": [
|
"features": [
|
||||||
"Realistyczne materiały i oświetlenie sceny",
|
"Realistyczne materiały i oświetlenie sceny",
|
||||||
"Widok 360 stopni, zoom i praca na detalach",
|
"Widok 360 stopni, zoom i praca na detalach",
|
||||||
@@ -60,7 +127,8 @@
|
|||||||
"Integracje API, webhooki i eksport danych",
|
"Integracje API, webhooki i eksport danych",
|
||||||
"Motyw graficzny dopasowany do marki"
|
"Motyw graficzny dopasowany do marki"
|
||||||
],
|
],
|
||||||
"processTitle": "Jak pracujemy",
|
"faqTitle": "FAQ - często zadawane pytania",
|
||||||
|
"processTitle": "Jak pracujemy?",
|
||||||
"process": [
|
"process": [
|
||||||
{"title": "Analiza katalogu", "description": "Porządkujemy warianty, zależności, dane produktowe i zakres konfiguracji."},
|
{"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": "Model 3D i UX", "description": "Przygotowujemy modele, materiały oraz interfejs dobrany do sposobu zakupu."},
|
||||||
@@ -80,11 +148,11 @@
|
|||||||
"demos": {
|
"demos": {
|
||||||
"desk": {
|
"desk": {
|
||||||
"slug": "demo-biurko",
|
"slug": "demo-biurko",
|
||||||
"eyebrow": "Demo produktowe",
|
"eyebrow": "",
|
||||||
"title": "Konfigurator - Biurko",
|
"title": "Konfigurator - Biurko",
|
||||||
"intro": "Pozwalaj klientom tworzyć własne biurka regulowane w czasie rzeczywistym.",
|
"intro": "Pozwalaj klientom tworzyć własne biurka regulowane w czasie rzeczywistym.",
|
||||||
"description": "Klient może zobaczyć swoje biurko w naturalnym oświetleniu, obrócić je o 360 stopni, powiększyć detale, zmienić dodatki i sprawdzić jak całość będzie wyglądała w jego przestrzeni.",
|
"description": "Klient może zobaczyć swoje biurko w naturalnym oświetleniu, obrócić je o 360 stopni, powiększyć detale, zmienić dodatki i sprawdzić jak całość będzie wyglądała w jego przestrzeni.",
|
||||||
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/b64b9dddfb838bfafea9da0ee5c04c68.png?width=1000&height=1000",
|
"imageUrl": "/demo-desk-preview.png",
|
||||||
"openLabel": "Otwórz konfigurator biurka",
|
"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.",
|
"benefitsIntro": "Rozwiązanie dla producentów i sklepów, które sprzedają produkty z wieloma wariantami, dodatkami i zależnościami.",
|
||||||
"stats": [
|
"stats": [
|
||||||
@@ -118,11 +186,11 @@
|
|||||||
},
|
},
|
||||||
"room": {
|
"room": {
|
||||||
"slug": "demo-pokoj",
|
"slug": "demo-pokoj",
|
||||||
"eyebrow": "Demo wnętrzarskie",
|
"eyebrow": "",
|
||||||
"title": "Konfigurator - Wnętrze",
|
"title": "Konfigurator - Wnętrza",
|
||||||
"intro": "Twórz i prezentuj kompletne aranżacje pomieszczeń oraz meble w interaktywnym środowisku 3D.",
|
"intro": "Twórz i prezentuj kompletne aranżacje pomieszczeń oraz meble w interaktywnym środowisku 3D.",
|
||||||
"description": "Klient może zaprojektować pokój od podstaw, ustawić meble, zmienić kolory ścian, materiały, dodatki i zobaczyć wszystko w realistycznym oświetleniu.",
|
"description": "Klient może zaprojektować pokój od podstaw, ustawić meble, zmienić kolory ścian, materiały, dodatki i zobaczyć wszystko w realistycznym oświetleniu.",
|
||||||
"imageUrl": "https://backend.ultifide.com/uploads/offer/page-header-element/optimized/e78aac1388ff07761422e12272690878.png?width=1000&height=1000",
|
"imageUrl": "/demo-room-preview.png",
|
||||||
"openLabel": "Otwórz konfigurator wnętrz",
|
"openLabel": "Otwórz konfigurator wnętrz",
|
||||||
"benefitsIntro": "Dla producentów mebli i marek wnętrzarskich, które chcą sprzedawać całe doświadczenie przestrzeni.",
|
"benefitsIntro": "Dla producentów mebli i marek wnętrzarskich, które chcą sprzedawać całe doświadczenie przestrzeni.",
|
||||||
"stats": [
|
"stats": [
|
||||||
@@ -156,7 +224,7 @@
|
|||||||
},
|
},
|
||||||
"door": {
|
"door": {
|
||||||
"slug": "demo-drzwi",
|
"slug": "demo-drzwi",
|
||||||
"eyebrow": "Demo produktowe",
|
"eyebrow": "",
|
||||||
"title": "Konfigurator - Drzwi",
|
"title": "Konfigurator - Drzwi",
|
||||||
"intro": "Pozwól klientom dobrać model, kolor, przeszklenie, klamkę i detale drzwi w interaktywnym podglądzie 3D.",
|
"intro": "Pozwól klientom dobrać model, kolor, przeszklenie, klamkę i detale drzwi w interaktywnym podglądzie 3D.",
|
||||||
"description": "Konfigurator drzwi ułatwia sprzedaż produktów z wieloma wariantami. Klient widzi efekt wyborów od razu, a konfiguracja może zostać przekazana do koszyka, zapytania ofertowego albo systemu sprzedaży.",
|
"description": "Konfigurator drzwi ułatwia sprzedaż produktów z wieloma wariantami. Klient widzi efekt wyborów od razu, a konfiguracja może zostać przekazana do koszyka, zapytania ofertowego albo systemu sprzedaży.",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 751 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 574 KiB |
@@ -1,25 +1,26 @@
|
|||||||
import type {Metadata} from 'next';
|
import type {Metadata} from 'next';
|
||||||
|
import {getTranslations} from 'next-intl/server';
|
||||||
import {DemoBrowser} from '@/components/DemoBrowser';
|
import {DemoBrowser} from '@/components/DemoBrowser';
|
||||||
import {DemoSubnav} from '@/components/DemoSubnav';
|
|
||||||
import {SectionHeader} from '@/components/SectionHeader';
|
import {SectionHeader} from '@/components/SectionHeader';
|
||||||
import {orderedDemos} from '@/config/content';
|
import {orderedDemos} from '@/config/content';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: 'Dema konfiguratorów 3D | Ultifide',
|
const t = await getTranslations('metadata');
|
||||||
description: 'Lista interaktywnych dem konfiguratorów 3D Ultifide z podglądem biurka, wnętrz i drzwi.'
|
|
||||||
};
|
return {
|
||||||
|
title: t('demosPageTitle'),
|
||||||
|
description: t('demosPageDescription')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function DemosPage() {
|
||||||
|
const t = await getTranslations('demosPage');
|
||||||
|
|
||||||
export default function DemosPage() {
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<section className="sectionBand sectionBand--light demosPage">
|
<section className="sectionBand sectionBand--light demosPage">
|
||||||
<div className="container demosContainer">
|
<div className="container demosContainer">
|
||||||
<SectionHeader
|
<SectionHeader title={t('title')} description={t('description')} />
|
||||||
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."
|
|
||||||
/>
|
|
||||||
<DemoSubnav demos={orderedDemos} hrefType="browser" />
|
|
||||||
<DemoBrowser demos={orderedDemos} />
|
<DemoBrowser demos={orderedDemos} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import messages from '../../../../messages/pl.json';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: messages.metadata.deskTitle,
|
title: messages.metadata.deskTitle,
|
||||||
description:
|
description: messages.metadata.deskDescription
|
||||||
'Poznaj konfigurator 3D, który zwiększa konwersję i pozwala klientom tworzyć własne biurka regulowane w czasie rzeczywistym.'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeskDemoPage() {
|
export default function DeskDemoPage() {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import messages from '../../../../messages/pl.json';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: messages.metadata.doorTitle,
|
title: messages.metadata.doorTitle,
|
||||||
description:
|
description: messages.metadata.doorDescription
|
||||||
'Poznaj konfigurator 3D drzwi, który pozwala klientom dobrać model, kolor, przeszklenie, klamkę i detale w czasie rzeczywistym.'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DoorDemoPage() {
|
export default function DoorDemoPage() {
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import messages from '../../../../messages/pl.json';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: messages.metadata.roomTitle,
|
title: messages.metadata.roomTitle,
|
||||||
description:
|
description: messages.metadata.roomDescription
|
||||||
'Poznaj konfigurator 3D, który pozwala klientom projektować całe pomieszczenia i zestawy mebli w czasie rzeczywistym.'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RoomDemoPage() {
|
export default function RoomDemoPage() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {Metadata} from 'next';
|
import type {Metadata} from 'next';
|
||||||
import localFont from 'next/font/local';
|
import localFont from 'next/font/local';
|
||||||
|
import messages from '../../../messages/pl.json';
|
||||||
import {NextIntlClientProvider} from 'next-intl';
|
import {NextIntlClientProvider} from 'next-intl';
|
||||||
import {getMessages} from 'next-intl/server';
|
import {getMessages} from 'next-intl/server';
|
||||||
import {notFound} from 'next/navigation';
|
import {notFound} from 'next/navigation';
|
||||||
@@ -36,7 +37,7 @@ const poppins = localFont({
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: 'Konfiguratory 3D | Ultifide',
|
default: messages.metadata.defaultTitle,
|
||||||
template: '%s'
|
template: '%s'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default function HomePage() {
|
|||||||
<section className="hero sectionBand sectionBand--light">
|
<section className="hero sectionBand sectionBand--light">
|
||||||
<div className="container hero__grid">
|
<div className="container hero__grid">
|
||||||
<div className="hero__content">
|
<div className="hero__content">
|
||||||
<p className="eyebrow">{homeContent.hero.eyebrow}</p>
|
|
||||||
<h1>{homeContent.hero.title}</h1>
|
<h1>{homeContent.hero.title}</h1>
|
||||||
<p>{homeContent.hero.description}</p>
|
<p>{homeContent.hero.description}</p>
|
||||||
<div className="buttonRow">
|
<div className="buttonRow">
|
||||||
@@ -139,7 +138,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
<section id="faq" className="sectionBand sectionBand--tint">
|
<section id="faq" className="sectionBand sectionBand--tint">
|
||||||
<div className="container narrow">
|
<div className="container narrow">
|
||||||
<SectionHeader title="FAQ" />
|
<SectionHeader title={homeContent.faqTitle} />
|
||||||
<Faq items={marketingFaq} />
|
<Faq items={marketingFaq} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+208
-23
@@ -95,12 +95,12 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader {
|
.siteHeader {
|
||||||
|
--site-header-height: 78px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
border-bottom: 1px solid rgba(157, 158, 180, 0.22);
|
border-bottom: 1px solid rgba(157, 158, 180, 0.22);
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: $white;
|
||||||
backdrop-filter: blur(14px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader__inner {
|
.siteHeader__inner {
|
||||||
@@ -118,6 +118,12 @@ p {
|
|||||||
width: 154px;
|
width: 154px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.siteHeader__logo svg {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
.logoMark {
|
.logoMark {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -240,13 +246,14 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero__content,
|
.hero__content,
|
||||||
.demoHero__grid > div {
|
.demoHero__content {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero__content > p:not(.eyebrow),
|
.hero__content > p:not(.eyebrow),
|
||||||
.demoHero__grid > div > p:not(.eyebrow),
|
.demoHero__content > p:not(.eyebrow),
|
||||||
.sectionHeader > p {
|
.sectionHeader > p {
|
||||||
max-width: 680px;
|
max-width: 680px;
|
||||||
color: $gray-light;
|
color: $gray-light;
|
||||||
@@ -449,7 +456,6 @@ p {
|
|||||||
border: 1px solid rgba(0, 28, 68, 0.12);
|
border: 1px solid rgba(0, 28, 68, 0.12);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: $white;
|
background: $white;
|
||||||
box-shadow: 0 22px 60px rgba(0, 28, 68, 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
@@ -483,7 +489,8 @@ p {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoFrame__actions a {
|
.demoFrame__actions a,
|
||||||
|
.demoFrame__actions button {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
@@ -491,6 +498,42 @@ p {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 1px solid rgba(0, 28, 68, 0.12);
|
border: 1px solid rgba(0, 28, 68, 0.12);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
background: $white;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoFrameOverlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: grid;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(0, 28, 68, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoFrameOverlay__panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoFrameOverlay__viewport {
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
background: #edf5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoFrameOverlay__viewport iframe {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoFrame__viewport {
|
.demoFrame__viewport {
|
||||||
@@ -731,7 +774,8 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoHero__visual {
|
.demoHero__visual {
|
||||||
min-height: 420px;
|
width: 100%;
|
||||||
|
min-height: min(420px, 52vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoCard > div {
|
.demoCard > div {
|
||||||
@@ -793,19 +837,28 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoSubnavBar {
|
.demoSubnavBar {
|
||||||
|
--demo-subnav-height: 53px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 78px;
|
top: var(--site-header-height, 78px);
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
|
min-height: var(--demo-subnav-height);
|
||||||
border-bottom: 1px solid rgba(0, 28, 68, 0.1);
|
border-bottom: 1px solid rgba(0, 28, 68, 0.1);
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: $white;
|
||||||
backdrop-filter: blur(14px);
|
}
|
||||||
|
|
||||||
|
.demoSubnavBar .container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: min(1180px, calc(100% - 40px));
|
||||||
|
min-height: var(--demo-subnav-height);
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoSubnavBar .demoSubnav {
|
.demoSubnavBar .demoSubnav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,11 +1020,76 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.demoHero {
|
.demoHero {
|
||||||
padding-top: clamp(56px, 8vw, 92px);
|
padding: clamp(20px, 2.5vw, 32px) 0 clamp(20px, 3vw, 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoHero__grid {
|
.demoHero__grid {
|
||||||
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
|
grid-template-columns: minmax(0, 1.1fr) minmax(300px, 0.9fr);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoHero__media {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: center;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoHero__media > span {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoHero__media img {
|
||||||
|
width: 100%;
|
||||||
|
max-height: min(420px, 52vh);
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoHero__media .demoHero__visual {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPreview {
|
||||||
|
--hero-content-width: min(1180px, calc(100% - 40px));
|
||||||
|
--demo-frame-width: calc((var(--hero-content-width) + 100vw) / 2);
|
||||||
|
--demo-frame-bar-height: 52px;
|
||||||
|
--demo-editor-chrome: calc(var(--site-header-height, 78px) + var(--demo-subnav-height, 53px) + var(--demo-frame-bar-height) + 40px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
scroll-margin-top: calc(var(--site-header-height, 78px) + var(--demo-subnav-height, 53px));
|
||||||
|
padding: 0 0 clamp(12px, 1.5vw, 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPage .sectionHeader h2,
|
||||||
|
.demoPage .finalCta h2 {
|
||||||
|
max-width: 820px;
|
||||||
|
font-size: clamp(1.45rem, 2.4vw, 2.1rem);
|
||||||
|
line-height: 1.22;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPage .sectionHeader {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPage .demoPreview + .sectionBand {
|
||||||
|
padding-top: clamp(24px, 3vw, 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPreview .demoFrame {
|
||||||
|
width: var(--demo-frame-width);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPreview .demoFrame__viewport {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(120svh - var(--demo-editor-chrome));
|
||||||
|
min-height: min(90svh, 900px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contactSection {
|
.contactSection {
|
||||||
@@ -1011,9 +1129,10 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contactSection__scheduler {
|
.contactSection__scheduler {
|
||||||
|
--contact-scheduler-height: 555px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 555px;
|
height: var(--contact-scheduler-height);
|
||||||
max-height: 555px;
|
max-height: var(--contact-scheduler-height);
|
||||||
border: 1px solid rgba(0, 28, 68, 0.1);
|
border: 1px solid rgba(0, 28, 68, 0.1);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: $tertiary;
|
background: $tertiary;
|
||||||
@@ -1122,7 +1241,13 @@ p {
|
|||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.siteHeader__inner {
|
.siteHeader__inner {
|
||||||
grid-template-columns: 150px 1fr auto;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
padding-inline: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader__nav,
|
.siteHeader__nav,
|
||||||
@@ -1132,13 +1257,13 @@ p {
|
|||||||
|
|
||||||
.siteHeader__menu {
|
.siteHeader__menu {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-self: end;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.siteHeader__mobileNav {
|
.siteHeader__mobileNav {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 10px 20px 20px;
|
padding: 10px 14px 20px;
|
||||||
border-top: 1px solid rgba(0, 28, 68, 0.08);
|
border-top: 1px solid rgba(0, 28, 68, 0.08);
|
||||||
background: $white;
|
background: $white;
|
||||||
}
|
}
|
||||||
@@ -1148,14 +1273,16 @@ p {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoSubnavBar {
|
.demoSubnavBar .container {
|
||||||
top: 68px;
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
padding-inline: 14px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demoSubnavBar .demoSubnav {
|
.demoSubnavBar .demoSubnav {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: 100%;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1164,11 +1291,40 @@ p {
|
|||||||
.demoBrowser,
|
.demoBrowser,
|
||||||
.splitSection,
|
.splitSection,
|
||||||
.contactSection__grid,
|
.contactSection__grid,
|
||||||
.contactSection__intro,
|
.contactSection__intro {
|
||||||
.footer__inner {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contactSection__schedulerWrap,
|
||||||
|
.contactSection__scheduler {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__brand {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__brand a {
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__contact {
|
||||||
|
order: 2;
|
||||||
|
justify-self: start;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer__brand p {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
.demoBrowser__sidebar {
|
.demoBrowser__sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
@@ -1202,11 +1358,14 @@ p {
|
|||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.container,
|
.container,
|
||||||
.narrow,
|
.narrow,
|
||||||
.siteHeader__inner,
|
|
||||||
.footer__inner {
|
.footer__inner {
|
||||||
width: min(100% - 28px, 1180px);
|
width: min(100% - 28px, 1180px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.siteHeader {
|
||||||
|
--site-header-height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
.siteHeader__inner {
|
.siteHeader__inner {
|
||||||
min-height: 68px;
|
min-height: 68px;
|
||||||
}
|
}
|
||||||
@@ -1215,6 +1374,22 @@ p {
|
|||||||
width: 138px;
|
width: 138px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.siteHeader__logo svg {
|
||||||
|
max-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactSection__scheduler {
|
||||||
|
--contact-scheduler-height: 370px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactSection.sectionBand {
|
||||||
|
padding-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contactSection__schedulerWrap {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.statsGrid,
|
.statsGrid,
|
||||||
.demoCards,
|
.demoCards,
|
||||||
.demoSubnav,
|
.demoSubnav,
|
||||||
@@ -1240,6 +1415,16 @@ p {
|
|||||||
min-height: 480px;
|
min-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.demoPreview {
|
||||||
|
--hero-content-width: min(1180px, calc(100% - 28px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoPreview .demoFrame__viewport {
|
||||||
|
aspect-ratio: unset;
|
||||||
|
height: calc(120svh - var(--demo-editor-chrome));
|
||||||
|
min-height: min(85svh, 800px);
|
||||||
|
}
|
||||||
|
|
||||||
.heroVisual__scene {
|
.heroVisual__scene {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
min-height: auto;
|
min-height: auto;
|
||||||
|
|||||||
+3
-2
@@ -1,10 +1,11 @@
|
|||||||
import type {Metadata} from 'next';
|
import type {Metadata} from 'next';
|
||||||
|
import messages from '../../messages/pl.json';
|
||||||
import './globals.scss';
|
import './globals.scss';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL('https://ultifide.com'),
|
metadataBase: new URL('https://ultifide.com'),
|
||||||
title: 'Konfiguratory 3D | Ultifide',
|
title: messages.metadata.defaultTitle,
|
||||||
description: 'Konfiguratory 3D dla produktów, wnętrz i sprzedaży B2B.'
|
description: messages.metadata.defaultDescription
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
export default function RootLayout({children}: Readonly<{children: React.ReactNode}>) {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import messages from '../../messages/pl.json';
|
||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<main className="notFound">
|
<main className="notFound">
|
||||||
<h1>Nie znaleziono strony</h1>
|
<h1>{messages.notFound.title}</h1>
|
||||||
<Link className="button button--primary" href="/pl">
|
<Link className="button button--primary" href="/pl">
|
||||||
Wróć do strony głównej
|
{messages.notFound.backHome}
|
||||||
</Link>
|
</Link>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import {Mail, Phone} from 'lucide-react';
|
import {Mail, Phone} from 'lucide-react';
|
||||||
|
import {getTranslations} from 'next-intl/server';
|
||||||
import {contact} from '@/config/contact';
|
import {contact} from '@/config/contact';
|
||||||
|
|
||||||
const schedulerUrl = 'https://42min.us/kubapyla';
|
|
||||||
|
|
||||||
function phoneHref(phone: string) {
|
function phoneHref(phone: string) {
|
||||||
return `tel:${phone.replaceAll(' ', '')}`;
|
return `tel:${phone.replaceAll(' ', '')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContactSection() {
|
export async function ContactSection() {
|
||||||
|
const t = await getTranslations('contact');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="kontakt" className="contactSection sectionBand">
|
<section id="kontakt" className="contactSection sectionBand">
|
||||||
<div className="container contactSection__grid">
|
<div className="container contactSection__grid">
|
||||||
<div className="contactSection__intro">
|
<div className="contactSection__intro">
|
||||||
<div>
|
<div>
|
||||||
<p className="eyebrow">Kontakt</p>
|
<p className="eyebrow">{t('eyebrow')}</p>
|
||||||
<h2>Porozmawiajmy o konfiguratorze 3D</h2>
|
<h2>{t('title')}</h2>
|
||||||
<p>Opisz katalog produktów, zakres wariantów albo pierwszy pomysł na demo. Wrócimy z konkretną ścieżką wdrożenia.</p>
|
<p>{t('description')}</p>
|
||||||
</div>
|
</div>
|
||||||
<address className="contactSection__details">
|
<address className="contactSection__details">
|
||||||
<div>
|
<div>
|
||||||
@@ -37,13 +38,13 @@ export function ContactSection() {
|
|||||||
<div className="contactSection__schedulerWrap">
|
<div className="contactSection__schedulerWrap">
|
||||||
<iframe
|
<iframe
|
||||||
className="contactSection__scheduler"
|
className="contactSection__scheduler"
|
||||||
src={schedulerUrl}
|
src={contact.schedulerUrl}
|
||||||
title="Umów rozmowę z Ultifide"
|
title={t('schedulerTitle')}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
referrerPolicy="strict-origin-when-cross-origin"
|
referrerPolicy="strict-origin-when-cross-origin"
|
||||||
/>
|
/>
|
||||||
<a className="contactSection__fallback" href={schedulerUrl} target="_blank" rel="noreferrer">
|
<a className="contactSection__fallback" href={contact.schedulerUrl} target="_blank" rel="noreferrer">
|
||||||
Umów rozmowę online w kalendarzu
|
{t('schedulerFallback')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import {ArrowRight, DoorOpen} from 'lucide-react';
|
import {ArrowRight, DoorOpen} from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
import {useRouter, useSearchParams} from 'next/navigation';
|
import {useRouter, useSearchParams} from 'next/navigation';
|
||||||
import {useMemo} from 'react';
|
import {useMemo} from 'react';
|
||||||
import {DemoFrame} from './DemoFrame';
|
import {DemoFrame} from './DemoFrame';
|
||||||
@@ -17,6 +18,9 @@ function isKnownDemo(slug: string | null, demos: DemoContent[]): slug is DemoCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function DemoBrowser({demos}: DemoBrowserProps) {
|
export function DemoBrowser({demos}: DemoBrowserProps) {
|
||||||
|
const t = useTranslations('demosPage');
|
||||||
|
const tSubnav = useTranslations('demoSubnav');
|
||||||
|
const tCommon = useTranslations('common');
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const requestedDemo = searchParams.get('demo');
|
const requestedDemo = searchParams.get('demo');
|
||||||
@@ -33,8 +37,8 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="demoBrowser">
|
<div className="demoBrowser">
|
||||||
<aside className="demoBrowser__sidebar" aria-label="Lista dem">
|
<aside className="demoBrowser__sidebar" aria-label={t('demoListAriaLabel')}>
|
||||||
<p className="eyebrow">Wybierz demo</p>
|
<p className="eyebrow">{t('selectDemo')}</p>
|
||||||
<div className="demoBrowser__list">
|
<div className="demoBrowser__list">
|
||||||
{demos.map(demo => (
|
{demos.map(demo => (
|
||||||
<button
|
<button
|
||||||
@@ -47,8 +51,7 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
|
|||||||
{demo.imageUrl ? <Image src={demo.imageUrl} alt="" width={96} height={96} /> : <DoorOpen size={30} />}
|
{demo.imageUrl ? <Image src={demo.imageUrl} alt="" width={96} height={96} /> : <DoorOpen size={30} />}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{demo.title}</strong>
|
<strong>{tSubnav(`labels.${demo.slug}`)}</strong>
|
||||||
<small>{demo.eyebrow}</small>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -62,8 +65,8 @@ export function DemoBrowser({demos}: DemoBrowserProps) {
|
|||||||
<h2>{activeDemo.title}</h2>
|
<h2>{activeDemo.title}</h2>
|
||||||
<p>{activeDemo.intro}</p>
|
<p>{activeDemo.intro}</p>
|
||||||
</div>
|
</div>
|
||||||
<Link className="button button--secondary" href={`/pl/${activeDemo.slug}`}>
|
<Link className="button button--secondary" href={`/pl/${activeDemo.slug}`} scroll>
|
||||||
Poczytaj więcej
|
{tCommon('seeMore')}
|
||||||
<ArrowRight size={18} />
|
<ArrowRight size={18} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import {ExternalLink, Maximize2} from 'lucide-react';
|
'use client';
|
||||||
|
|
||||||
|
import {ExternalLink, Maximize2, X} from 'lucide-react';
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import {createPortal} from 'react-dom';
|
||||||
|
|
||||||
type DemoFrameProps = {
|
type DemoFrameProps = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -7,25 +12,88 @@ type DemoFrameProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DemoFrame({title, url, openLabel}: DemoFrameProps) {
|
export function DemoFrame({title, url, openLabel}: DemoFrameProps) {
|
||||||
|
const t = useTranslations('common');
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFullscreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsFullscreen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
window.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = previousOverflow;
|
||||||
|
window.removeEventListener('keydown', onKeyDown);
|
||||||
|
};
|
||||||
|
}, [isFullscreen]);
|
||||||
|
|
||||||
|
const overlay =
|
||||||
|
isFullscreen && isMounted
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className="demoFrameOverlay"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={title}
|
||||||
|
onClick={() => setIsFullscreen(false)}
|
||||||
|
>
|
||||||
|
<div className="demoFrameOverlay__panel demoFrame" onClick={event => event.stopPropagation()}>
|
||||||
|
<div className="demoFrame__bar">
|
||||||
|
<span>{title}</span>
|
||||||
|
<div className="demoFrame__actions">
|
||||||
|
<a href={url} target="_blank" rel="noreferrer" aria-label={t('openInNewTab')}>
|
||||||
|
<ExternalLink size={18} />
|
||||||
|
</a>
|
||||||
|
<button type="button" aria-label={t('close')} onClick={() => setIsFullscreen(false)}>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="demoFrameOverlay__viewport">
|
||||||
|
<iframe title={title} src={url} allowFullScreen />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="demoFrame">
|
<>
|
||||||
<div className="demoFrame__bar">
|
<div className="demoFrame">
|
||||||
<span>{title}</span>
|
<div className="demoFrame__bar">
|
||||||
<div className="demoFrame__actions">
|
<span>{title}</span>
|
||||||
<a href={url} target="_blank" rel="noreferrer" aria-label="Otwórz demo w nowej karcie">
|
<div className="demoFrame__actions">
|
||||||
<ExternalLink size={18} />
|
<a href={url} target="_blank" rel="noreferrer" aria-label={t('openInNewTab')}>
|
||||||
</a>
|
<ExternalLink size={18} />
|
||||||
<a href={url} target="_blank" rel="noreferrer" aria-label="Pełny ekran">
|
</a>
|
||||||
<Maximize2 size={18} />
|
<button type="button" aria-label={t('fullscreen')} onClick={() => setIsFullscreen(true)}>
|
||||||
|
<Maximize2 size={18} />
|
||||||
|
</button>
|
||||||
|
</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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="demoFrame__viewport">
|
{overlay}
|
||||||
<iframe title={title} src={url} loading="lazy" allowFullScreen />
|
</>
|
||||||
<a className="demoFrame__fallback" href={url} target="_blank" rel="noreferrer">
|
|
||||||
{openLabel}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-33
@@ -1,11 +1,12 @@
|
|||||||
import {CheckCircle2, DoorOpen} from 'lucide-react';
|
import {CheckCircle2, DoorOpen} from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import {getTranslations} from 'next-intl/server';
|
||||||
import {ButtonLink} from './ButtonLink';
|
import {ButtonLink} from './ButtonLink';
|
||||||
import {DemoFrame} from './DemoFrame';
|
import {DemoFrame} from './DemoFrame';
|
||||||
import {DemoSubnav} from './DemoSubnav';
|
import {DemoSubnav} from './DemoSubnav';
|
||||||
import {Faq} from './Faq';
|
import {DemoSubnavBar} from './DemoSubnavBar';
|
||||||
|
import {ScrollToTopOnMount} from './ScrollToTopOnMount';
|
||||||
import {SectionHeader} from './SectionHeader';
|
import {SectionHeader} from './SectionHeader';
|
||||||
import {Stats} from './Stats';
|
|
||||||
import {orderedDemos} from '@/config/content';
|
import {orderedDemos} from '@/config/content';
|
||||||
import type {DemoContent} from '@/types/content';
|
import type {DemoContent} from '@/types/content';
|
||||||
|
|
||||||
@@ -13,50 +14,47 @@ type DemoPageProps = {
|
|||||||
demo: DemoContent;
|
demo: DemoContent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DemoPage({demo}: DemoPageProps) {
|
export async function DemoPage({demo}: DemoPageProps) {
|
||||||
|
const t = await getTranslations('common');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="demoSubnavBar">
|
<ScrollToTopOnMount />
|
||||||
|
<DemoSubnavBar>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<DemoSubnav demos={orderedDemos} activeSlug={demo.slug} />
|
<DemoSubnav demos={orderedDemos} activeSlug={demo.slug} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DemoSubnavBar>
|
||||||
|
|
||||||
<main>
|
<main className="demoPage">
|
||||||
<section className="demoHero sectionBand sectionBand--light">
|
<section className="demoHero sectionBand sectionBand--light">
|
||||||
<div className="container demoHero__grid">
|
<div className="container demoHero__grid">
|
||||||
<div>
|
<div className="demoHero__content">
|
||||||
<p className="eyebrow">{demo.eyebrow}</p>
|
<p className="eyebrow">{demo.eyebrow}</p>
|
||||||
<h1>{demo.title}</h1>
|
<h1>{demo.title}</h1>
|
||||||
<p>{demo.intro}</p>
|
<p>{demo.intro}</p>
|
||||||
<p>{demo.description}</p>
|
<p>{demo.description}</p>
|
||||||
<div className="buttonRow">
|
<div className="buttonRow">
|
||||||
<ButtonLink href="#podglad">Zobacz demo</ButtonLink>
|
<ButtonLink href="#podglad">{t('seeDemo')}</ButtonLink>
|
||||||
<ButtonLink href="#kontakt" variant="secondary">
|
<ButtonLink href="#kontakt" variant="secondary">
|
||||||
Porozmawiajmy
|
{t('contactUs')}
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{demo.imageUrl ? (
|
<div className="demoHero__media">
|
||||||
<Image src={demo.imageUrl} alt="" width={640} height={640} priority />
|
{demo.imageUrl ? (
|
||||||
) : (
|
<Image src={demo.imageUrl} alt="" width={640} height={640} priority />
|
||||||
<div className="demoHero__visual" aria-hidden="true">
|
) : (
|
||||||
<DoorOpen size={112} />
|
<div className="demoHero__visual" aria-hidden="true">
|
||||||
</div>
|
<DoorOpen size={112} />
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="podglad" className="sectionBand">
|
<section id="podglad" className="demoPreview">
|
||||||
<div className="container">
|
<DemoFrame title={demo.title} url={demo.iframeUrl} openLabel={demo.openLabel} />
|
||||||
<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>
|
||||||
|
|
||||||
<section className="sectionBand">
|
<section className="sectionBand">
|
||||||
@@ -83,13 +81,6 @@ export function DemoPage({demo}: DemoPageProps) {
|
|||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="sectionBand sectionBand--tint">
|
|
||||||
<div className="container narrow">
|
|
||||||
<SectionHeader title="FAQ" />
|
|
||||||
<Faq items={demo.faq} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
import {usePathname, useSearchParams} from 'next/navigation';
|
import {usePathname, useSearchParams} from 'next/navigation';
|
||||||
import type {DemoContent} from '@/types/content';
|
import type {DemoContent} from '@/types/content';
|
||||||
|
|
||||||
@@ -11,20 +12,26 @@ type DemoSubnavProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DemoSubnav({demos, activeSlug, hrefType = 'page'}: DemoSubnavProps) {
|
export function DemoSubnav({demos, activeSlug, hrefType = 'page'}: DemoSubnavProps) {
|
||||||
|
const t = useTranslations('common');
|
||||||
|
const tSubnav = useTranslations('demoSubnav');
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const selectedSlug = activeSlug ?? searchParams.get('demo') ?? demos[0]?.slug;
|
const selectedSlug = activeSlug ?? searchParams.get('demo') ?? demos[0]?.slug;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="demoSubnav" aria-label="Dema konfiguratorów">
|
<nav className="demoSubnav" aria-label={tSubnav('ariaLabel')}>
|
||||||
|
{hrefType === 'page' ? (
|
||||||
|
<Link className={pathname === '/pl/dema' ? 'isActive' : undefined} href="/pl/dema">
|
||||||
|
{t('backToList')}
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
{demos.map(demo => {
|
{demos.map(demo => {
|
||||||
const isActive = selectedSlug === demo.slug || pathname === `/pl/${demo.slug}`;
|
const isActive = selectedSlug === demo.slug || pathname === `/pl/${demo.slug}`;
|
||||||
const href = hrefType === 'browser' ? `/pl/dema?demo=${demo.slug}` : `/pl/${demo.slug}`;
|
const href = hrefType === 'browser' ? `/pl/dema?demo=${demo.slug}` : `/pl/${demo.slug}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link className={isActive ? 'isActive' : undefined} href={href} key={demo.slug}>
|
<Link className={isActive ? 'isActive' : undefined} href={href} key={demo.slug}>
|
||||||
<span>{demo.eyebrow}</span>
|
{tSubnav(`labels.${demo.slug}`)}
|
||||||
{demo.title}
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {useEffect, type ReactNode} from 'react';
|
||||||
|
|
||||||
|
type DemoSubnavBarProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DemoSubnavBar({children}: DemoSubnavBarProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
const header = document.querySelector<HTMLElement>('.siteHeader');
|
||||||
|
if (!header) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncHeaderHeight() {
|
||||||
|
if (!header) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.style.setProperty('--site-header-height', `${header.offsetHeight}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
syncHeaderHeight();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(syncHeaderHeight);
|
||||||
|
observer.observe(header);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div className="demoSubnavBar">{children}</div>;
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import {getTranslations} from 'next-intl/server';
|
||||||
import {contact} from '@/config/contact';
|
import {contact} from '@/config/contact';
|
||||||
import {Logo} from './Logo';
|
import {Logo} from './Logo';
|
||||||
|
|
||||||
export function Footer() {
|
export async function Footer() {
|
||||||
|
const t = await getTranslations('footer');
|
||||||
|
const tNavigation = await getTranslations('navigation');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="footer">
|
<footer className="footer">
|
||||||
<div className="footer__inner">
|
<div className="footer__inner">
|
||||||
<div className="footer__brand">
|
<div className="footer__brand">
|
||||||
<Link href="/pl" aria-label="Ultifide konfiguratory 3D">
|
<Link href="/pl" aria-label={tNavigation('logoAriaLabel')}>
|
||||||
<Logo isWhite />
|
<Logo isWhite />
|
||||||
</Link>
|
</Link>
|
||||||
<p>Copyright © 2026, Ultifide</p>
|
<p>{t('copyright')}</p>
|
||||||
</div>
|
</div>
|
||||||
<address className="footer__contact">
|
<address className="footer__contact">
|
||||||
{contact.address.map(line => (
|
{contact.address.map(line => (
|
||||||
|
|||||||
+21
-19
@@ -2,28 +2,30 @@
|
|||||||
|
|
||||||
import {Menu, X} from 'lucide-react';
|
import {Menu, X} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import {useTranslations} from 'next-intl';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {usePathname} from 'next/navigation';
|
import {usePathname} from 'next/navigation';
|
||||||
import {Logo} from './Logo';
|
import {Logo} from './Logo';
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{id: 'dema', label: 'Dema', href: '/pl#dema'},
|
{id: 'dema', labelKey: 'dema', href: '/pl/dema'},
|
||||||
{id: 'platforma', label: 'Platforma', href: '/pl#platforma'},
|
{id: 'platforma', labelKey: 'platform', href: '/pl#platforma'},
|
||||||
{id: 'funkcje', label: 'Funkcje', href: '/pl#funkcje'},
|
{id: 'funkcje', labelKey: 'features', href: '/pl#funkcje'},
|
||||||
{id: 'faq', label: 'FAQ', href: '/pl#faq'}
|
{id: 'faq', labelKey: 'faq', href: '/pl#faq'}
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
|
const t = useTranslations('navigation');
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [activeSection, setActiveSection] = useState('platforma');
|
const [activeSection, setActiveSection] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pathname !== '/pl') {
|
if (pathname !== '/pl') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sections = ['dema', 'platforma', 'funkcje', 'faq', 'kontakt']
|
const sections = ['platforma', 'funkcje', 'faq', 'kontakt']
|
||||||
.map(id => document.getElementById(id))
|
.map(id => document.getElementById(id))
|
||||||
.filter((section): section is HTMLElement => Boolean(section));
|
.filter((section): section is HTMLElement => Boolean(section));
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ export function Header() {
|
|||||||
return active;
|
return active;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
setActiveSection(currentSection?.id ?? 'platforma');
|
setActiveSection(currentSection?.id ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateActiveSection();
|
updateActiveSection();
|
||||||
@@ -53,37 +55,37 @@ export function Header() {
|
|||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
function isActive(itemId: string) {
|
function isActive(itemId: string) {
|
||||||
if (pathname === '/pl') {
|
|
||||||
return itemId === activeSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemId === 'dema') {
|
if (itemId === 'dema') {
|
||||||
return pathname === '/pl/dema' || pathname.startsWith('/pl/demo');
|
return pathname === '/pl/dema' || pathname.startsWith('/pl/demo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pathname === '/pl') {
|
||||||
|
return itemId === activeSection;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="siteHeader">
|
<header className="siteHeader">
|
||||||
<div className="siteHeader__inner">
|
<div className="siteHeader__inner">
|
||||||
<Link className="siteHeader__logo" href="/pl" aria-label="Ultifide konfiguratory 3D">
|
<Link className="siteHeader__logo" href="/pl" aria-label={t('logoAriaLabel')}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="siteHeader__nav" aria-label="Nawigacja glowna">
|
<nav className="siteHeader__nav" aria-label={t('mainNav')}>
|
||||||
{navItems.map(item => (
|
{navItems.map(item => (
|
||||||
<a className={isActive(item.id) ? 'isActive' : undefined} key={item.href} href={item.href}>
|
<a className={isActive(item.id) ? 'isActive' : undefined} key={item.href} href={item.href}>
|
||||||
{item.label}
|
{t(item.labelKey)}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
<a className={activeSection === 'kontakt' ? 'siteHeader__contact isActive' : 'siteHeader__contact'} href="#kontakt">
|
<a className={activeSection === 'kontakt' ? 'siteHeader__contact isActive' : 'siteHeader__contact'} href="#kontakt">
|
||||||
Kontakt
|
{t('contact')}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
className="siteHeader__menu"
|
className="siteHeader__menu"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={isOpen ? 'Zamknij menu' : 'Otwórz menu'}
|
aria-label={isOpen ? t('closeMenu') : t('openMenu')}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
onClick={() => setIsOpen(current => !current)}
|
onClick={() => setIsOpen(current => !current)}
|
||||||
>
|
>
|
||||||
@@ -91,7 +93,7 @@ export function Header() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<nav className="siteHeader__mobileNav" aria-label="Nawigacja mobilna">
|
<nav className="siteHeader__mobileNav" aria-label={t('mobileNav')}>
|
||||||
{navItems.map(item => (
|
{navItems.map(item => (
|
||||||
<a
|
<a
|
||||||
className={isActive(item.id) ? 'isActive' : undefined}
|
className={isActive(item.id) ? 'isActive' : undefined}
|
||||||
@@ -99,7 +101,7 @@ export function Header() {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.labelKey)}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import {Box, CheckCircle2, DoorOpen, Layers3, MousePointer2, PanelRight, SlidersHorizontal} from 'lucide-react';
|
import {Box, CheckCircle2, DoorOpen, Layers3, MousePointer2, PanelRight, SlidersHorizontal} from 'lucide-react';
|
||||||
|
import {getTranslations} from 'next-intl/server';
|
||||||
|
|
||||||
|
export async function HeroVisual() {
|
||||||
|
const t = await getTranslations('heroVisual');
|
||||||
|
|
||||||
export function HeroVisual() {
|
|
||||||
return (
|
return (
|
||||||
<div className="heroVisual" aria-hidden="true">
|
<div className="heroVisual" aria-hidden="true">
|
||||||
<div className="heroVisual__topbar">
|
<div className="heroVisual__topbar">
|
||||||
@@ -26,15 +29,15 @@ export function HeroVisual() {
|
|||||||
<div className="heroVisual__panel">
|
<div className="heroVisual__panel">
|
||||||
<div>
|
<div>
|
||||||
<PanelRight size={18} />
|
<PanelRight size={18} />
|
||||||
<span>Konfiguracja</span>
|
<span>{t('configuration')}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="heroVisual__option isActive">
|
<div className="heroVisual__option isActive">
|
||||||
<CheckCircle2 size={16} />
|
<CheckCircle2 size={16} />
|
||||||
Materiał premium
|
{t('premiumMaterial')}
|
||||||
</div>
|
</div>
|
||||||
<div className="heroVisual__option">
|
<div className="heroVisual__option">
|
||||||
<SlidersHorizontal size={16} />
|
<SlidersHorizontal size={16} />
|
||||||
Reguły produktu
|
{t('productRules')}
|
||||||
</div>
|
</div>
|
||||||
<div className="heroVisual__swatches">
|
<div className="heroVisual__swatches">
|
||||||
<span />
|
<span />
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {useEffect} from 'react';
|
||||||
|
|
||||||
|
export function ScrollToTopOnMount() {
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.location.hash) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.scrollTo({top: 0, left: 0});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import messages from '../../messages/pl.json';
|
||||||
|
|
||||||
|
const contactMessages = messages.contact;
|
||||||
|
|
||||||
export const contact = {
|
export const contact = {
|
||||||
anchor: '#kontakt',
|
anchor: '#kontakt',
|
||||||
email: 'contact@ultifide.com',
|
email: contactMessages.email,
|
||||||
phones: ['+48 733 226 544', '+48 664 565 858'],
|
phones: contactMessages.phones,
|
||||||
address: ['Kraków, Polska', 'ul. św. Wawrzyńca 19/2', '31-060 Kraków'],
|
address: contactMessages.address,
|
||||||
|
schedulerUrl: contactMessages.schedulerUrl,
|
||||||
linkedin: 'https://pl.linkedin.com/company/ultifide',
|
linkedin: 'https://pl.linkedin.com/company/ultifide',
|
||||||
facebook: 'https://facebook.com/ultifide'
|
facebook: 'https://facebook.com/ultifide'
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+1
-62
@@ -28,65 +28,4 @@ export const demoContent = {
|
|||||||
|
|
||||||
export const orderedDemos: DemoContent[] = [demoContent.room, demoContent.door, demoContent.desk];
|
export const orderedDemos: DemoContent[] = [demoContent.room, demoContent.door, demoContent.desk];
|
||||||
|
|
||||||
export const marketingFaq: FaqItem[] = [
|
export const marketingFaq: FaqItem[] = messages.marketingFaq;
|
||||||
{
|
|
||||||
question: 'Czy możemy zacząć od jednego produktu?',
|
|
||||||
answer:
|
|
||||||
'Tak. V1 może obejmować jeden produkt, jedną kolekcję albo jedno demo, a potem rosnąć o kolejne warianty, reguły i integracje.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy konfigurator może być częścią mojego sklepu?',
|
|
||||||
answer:
|
|
||||||
'Tak. Może działać jako widżet, dedykowana podstrona, kreator zestawów, moduł koszyka albo element panelu B2B.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Jak wygląda integracja ze sprzedażą, CRM lub ERP?',
|
|
||||||
answer:
|
|
||||||
'Konfiguracja może trafiać do koszyka, formularza zapytania, CRM, ERP albo panelu B2B. Integrujemy przez API, webhooki i popularne platformy e-commerce.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy mogę dodać własne modele 3D, materiały i warianty?',
|
|
||||||
answer:
|
|
||||||
'Tak. Możemy pracować na dostarczonych modelach albo przygotować i zoptymalizować je pod konfigurator, a potem rozwijać katalog o nowe kolory, materiały, dodatki i akcesoria.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy konfigurator obsługuje zależności i reguły produktu?',
|
|
||||||
answer:
|
|
||||||
'Tak. Obsługujemy warunki, ograniczenia, wariantowe SKU, zgodność modułów i zależności między modelem, rozmiarem, kolorem, okuciami, dodatkami lub dostępnością.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy konfigurator działa na urządzeniach mobilnych?',
|
|
||||||
answer:
|
|
||||||
'Tak. Interfejs jest responsywny, a w wybranych wdrożeniach można dodać tryb AR.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy wygląd konfiguratora można dopasować do mojej marki?',
|
|
||||||
answer:
|
|
||||||
'Tak. UI, kolory, branding, układ opcji, sposób interakcji, modele 3D i środowisko sceny mogą wyglądać jak natywna część Twojego sklepu.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy konfigurator sprawdzi się dla mebli modułowych i całych kolekcji?',
|
|
||||||
answer:
|
|
||||||
'Tak. System można rozwijać o meble modułowe, zestawy, kolekcje aranżacyjne, nowe produkty i kolejne układy pomieszczeń bez przebudowy całości.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy można odwzorować całe pomieszczenia?',
|
|
||||||
answer:
|
|
||||||
'Tak. Możemy stworzyć pełne sceny 3D od pustego pokoju po gotowe aranżacje z konfigurowalnymi meblami, kolorami i materiałami.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy klient może zmieniać układ mebli i elementów?',
|
|
||||||
answer:
|
|
||||||
'Tak. Konfigurator może pozwalać na ustawianie elementów, zmianę układu, dobór dodatków i dopasowanie konfiguracji do przestrzeni.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Jakie są główne korzyści z wdrożenia konfiguratora 3D?',
|
|
||||||
answer:
|
|
||||||
'Konfigurator pozwala prezentować produkty w jakości premium, personalizować je w czasie rzeczywistym, ograniczać liczbę zwrotów i pytań oraz skracać proces decyzji zakupowej.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
question: 'Czy konfigurator nadaje się do produktów takich jak drzwi, fronty lub zabudowy?',
|
|
||||||
answer:
|
|
||||||
'Tak. Sprawdza się przy produktach wymagających precyzyjnego dopasowania wariantów, takich jak drzwi, fronty, zabudowy, oświetlenie, wyposażenie i inne produkty custom.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|||||||
Reference in New Issue
Block a user