Navbar: logo, ikony z animacjami, shrink przy scroll

- Logo LabAI_pro.png zamiast tekstu DEX_OS (Nav + Footer)
- Navbar shrink przy scroll >60px (compact mode)
- Ikony SVG przy linkach: góra→lewa przy przejściu w compact
- Animacje ikon na aktywnym linku (shake, bounce, glow)
- backdrop-filter przeniesiony na ::before (fix overflow)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paweł Domański
2026-07-05 21:09:28 +02:00
parent 8d4dc8a6f1
commit 1ca53c235c
10 changed files with 433 additions and 29 deletions
+1 -2
View File
@@ -3,8 +3,7 @@ const Footer = () => (
<div className="footer-container"> <div className="footer-container">
<div className="footer-brand"> <div className="footer-brand">
<div className="nav-logo"> <div className="nav-logo">
<span className="pulse-dot" aria-hidden="true"></span> <img src="/img/LabAI_pro.png" alt="Laboratorium AI" height={36} />
DEX_OS
</div> </div>
<p>Zbudowane przez deweloperów sztucznej inteligencji, dla agentów przyszłości.</p> <p>Zbudowane przez deweloperów sztucznej inteligencji, dla agentów przyszłości.</p>
<div className="footer-social" aria-label="Social media"> <div className="footer-social" aria-label="Social media">
+58 -17
View File
@@ -1,15 +1,58 @@
import React, { useState } from 'react'; 'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const icons = {
news: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
<path d="M18 14h-8M15 18h-5M10 6h8v4h-8z"/>
</svg>
),
products: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.29 7 12 12 20.71 7"/><line x1="12" y1="22" x2="12" y2="12"/>
</svg>
),
ai: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/>
<path d="M12 7v4M8 11V9M16 11V9M6 15h.01M10 15h.01M14 15h.01M18 15h.01M6 19h.01M18 19h.01"/>
</svg>
),
contact: (
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
),
}
const NavLink = ({ href, label, icon, active }) => (
<Link href={href} className={`nav-link${active ? ' active' : ''}`} aria-current={active ? 'page' : undefined} role="menuitem">
<span className="nav-link__icon">{icon}</span>
<span className="nav-link__label">{label}</span>
</Link>
)
const Nav = () => { const Nav = () => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false)
const [compact, setCompact] = useState(false)
const pathname = usePathname()
useEffect(() => {
const onScroll = () => setCompact(window.scrollY > 60)
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
return ( return (
<nav className="main-nav" aria-label="Główna nawigacja"> <nav className={`main-nav${compact ? ' main-nav--compact' : ''}`} aria-label="Główna nawigacja">
<div className="nav-container"> <div className="nav-container">
<div className="nav-logo"> <Link href="/" className="nav-logo">
<span className="pulse-dot" aria-hidden="true"></span> <img src="/img/LabAI_pro.png" alt="Laboratorium AI" height={132} />
DEX_OS </Link>
</div>
<button <button
className={`nav-hamburger ${menuOpen ? 'nav-hamburger--open' : ''}`} className={`nav-hamburger ${menuOpen ? 'nav-hamburger--open' : ''}`}
@@ -17,16 +60,14 @@ const Nav = () => {
aria-label={menuOpen ? 'Zamknij menu' : 'Otwórz menu'} aria-label={menuOpen ? 'Zamknij menu' : 'Otwórz menu'}
aria-expanded={menuOpen} aria-expanded={menuOpen}
> >
<span></span> <span></span><span></span><span></span>
<span></span>
<span></span>
</button> </button>
<div className={`nav-links ${menuOpen ? 'nav-links--open' : ''}`} role="menu"> <div className={`nav-links ${menuOpen ? 'nav-links--open' : ''}`} role="menu">
<a href="#" className="active" aria-current="page" role="menuitem">Wiadomości</a> <NavLink href="/" label="Wiadomości" icon={icons.news} active={pathname === '/'} />
<a href="#" role="menuitem">Dla zaawansowanych</a> <NavLink href="/produkty" label="Produkty" icon={icons.products} active={pathname === '/produkty'} />
<a href="#" role="menuitem">Modele AI</a> <NavLink href="#" label="Modele AI" icon={icons.ai} active={false} />
<a href="#" role="menuitem">Biblioteka Promptów</a> <NavLink href="/kontakt" label="Kontakt" icon={icons.contact} active={pathname === '/kontakt'} />
</div> </div>
<div className="nav-actions"> <div className="nav-actions">
@@ -34,7 +75,7 @@ const Nav = () => {
</div> </div>
</div> </div>
</nav> </nav>
); )
}; }
export default Nav; export default Nav
+38
View File
@@ -0,0 +1,38 @@
const DIRECTUS = 'https://cms.dexterlab.pl'
const TYPE_LABEL = { course: 'Kurs', service: 'Usługa', book: 'Ebook' }
const ProductCard = ({ p, headingLevel: H = 'h3' }) => (
<div className={`product-card${p.featured ? ' product-card--featured' : ''}`}>
{p.featured && <span className="product-featured-badge">Polecany</span>}
{p.product_image && (
<div className="product-img" style={{ backgroundImage: `url(${DIRECTUS}/assets/${p.product_image})` }} />
)}
<div className="product-body">
<span className="product-type">{TYPE_LABEL[p.type] ?? p.type}</span>
<H className="product-name">{p.title}</H>
{p.categories?.length > 0 && (
<div className="product-tags">
{p.categories.map((c) => <span key={c} className="product-tag">{c}</span>)}
</div>
)}
<p className="product-desc">{p.short_description}</p>
<div className="product-footer">
<div className="product-price">
{p.discount_price ? (
<>
<span className="product-price--promo">{p.discount_price} {p.currency}</span>
<span className="product-price--old">{p.price} {p.currency}</span>
</>
) : (
<span className="product-price--regular">{p.price} {p.currency}</span>
)}
</div>
<a href={p.url} target="_blank" rel="noopener noreferrer" className="hero__btn-primary">
Kup teraz
</a>
</div>
</div>
</div>
)
export default ProductCard
+17
View File
@@ -0,0 +1,17 @@
'use client'
const SubscribeBox = () => (
<div className="subscribe-box">
<h3>Bądź na bieżąco</h3>
<p>Otrzymuj najnowsze wiadomości o AI prosto na swoją skrzynkę.</p>
<form method="post" action="https://mail.dexterlab.pl/subscription/form">
<input type="hidden" name="nonce" />
<input type="email" name="email" required placeholder="Twój email" />
<input type="text" name="name" placeholder="Nazwa (opcjonalnie)" style={{ marginTop: '0.5rem' }} />
<input id="827b2" type="checkbox" name="l" defaultChecked value="5e40f006-7648-4932-88f4-564c95ed337f" style={{ display: 'none' }} />
<button type="submit">Subskrybuj</button>
</form>
</div>
)
export default SubscribeBox
+97 -10
View File
@@ -58,24 +58,47 @@ h1, h2, h3, h4, h5, h6 {
/* ===== NAVIGATION ===== */ /* ===== NAVIGATION ===== */
.main-nav { .main-nav {
width: 100%; width: 100%;
background: rgba(15, 16, 22, 0.85);
border-bottom: 1px solid var(--border-color);
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 100; z-index: 100;
overflow: visible;
}
.main-nav::before {
content: '';
position: absolute;
inset: 0;
background: rgba(15, 16, 22, 0.85);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border-color);
z-index: -1;
} }
.nav-container { .nav-container {
max-width: 1600px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding: 1rem 2rem; padding: 1rem 2rem;
transition: padding 0.3s ease;
overflow: visible;
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 1rem; gap: 1rem;
} }
.nav-logo img {
transition: height 0.3s ease;
}
.main-nav--compact .nav-container {
padding: 0.4rem 2rem;
}
.main-nav--compact .nav-logo img {
height: 44px;
}
.nav-logo { .nav-logo {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 800; font-weight: 800;
@@ -92,22 +115,86 @@ h1, h2, h3, h4, h5, h6 {
gap: 2rem; gap: 2rem;
} }
.nav-links a { .nav-link {
position: relative; position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
color: var(--text-muted); color: var(--text-muted);
text-decoration: none; text-decoration: none;
font-size: 0.95rem; font-size: 0.85rem;
font-weight: 500; font-weight: 500;
transition: color 0.3s ease; transition: color 0.3s ease;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
} }
.nav-links a::after { @keyframes nav-bounce {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-4px); }
60% { transform: translateY(-2px); }
}
@keyframes nav-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes nav-pulse {
0%, 100% { opacity: 1; filter: drop-shadow(0 0 0px var(--accent-color)); }
50% { opacity: 0.8; filter: drop-shadow(0 0 6px var(--accent-color)); }
}
@keyframes nav-shake {
0%, 100% { transform: rotate(0deg); }
20% { transform: rotate(-8deg); }
40% { transform: rotate(8deg); }
60% { transform: rotate(-4deg); }
80% { transform: rotate(4deg); }
}
.nav-link__icon {
display: flex;
transition: transform 0.3s ease;
}
/* ikona Wiadomości — shake jak dzwonek */
.nav-link:nth-child(1).active .nav-link__icon {
animation: nav-shake 1.5s ease-in-out infinite;
}
/* ikona Produkty — bounce */
.nav-link:nth-child(2).active .nav-link__icon {
animation: nav-bounce 1.2s ease-in-out infinite;
}
/* ikona Modele AI — pulse/glow */
.nav-link:nth-child(3).active .nav-link__icon {
animation: nav-pulse 1.8s ease-in-out infinite;
}
/* ikona Kontakt — bounce */
.nav-link:nth-child(4).active .nav-link__icon {
animation: nav-bounce 1.2s ease-in-out infinite;
}
.nav-link__label {
font-weight: 500;
letter-spacing: 0.02em;
}
.main-nav--compact .nav-link {
flex-direction: row;
gap: 0.4rem;
align-items: center;
}
.nav-link::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 2px; height: 2px;
bottom: 0; bottom: -4px;
left: 0; left: 0;
background-color: var(--accent-color); background-color: var(--accent-color);
transform: scaleX(0); transform: scaleX(0);
@@ -117,13 +204,13 @@ h1, h2, h3, h4, h5, h6 {
border-radius: 2px; border-radius: 2px;
} }
.nav-links a:hover, .nav-link:hover,
.nav-links a.active { .nav-link.active {
color: #fff; color: #fff;
} }
.nav-links a:hover::after, .nav-link:hover::after,
.nav-links a.active::after { .nav-link.active::after {
transform: scaleX(1); transform: scaleX(1);
transform-origin: left; transform-origin: left;
} }
+23
View File
@@ -0,0 +1,23 @@
const DIRECTUS = 'https://cms.dexterlab.pl'
const BRAND = { '@type': 'Organization', name: 'Laboratorium AI' }
export function productSchema(p) {
return {
'@context': 'https://schema.org/',
'@type': 'Product',
name: p.title,
image: p.product_image ? `${DIRECTUS}/assets/${p.product_image}` : undefined,
description: p.short_description,
...(p.sku && { sku: p.sku }),
brand: BRAND,
offers: {
'@type': 'Offer',
url: p.url,
priceCurrency: p.currency,
price: String(p.discount_price ?? p.price),
priceValidUntil: '2026-12-31',
availability: 'https://schema.org/InStock',
itemCondition: 'https://schema.org/NewCondition',
},
}
}
+67
View File
@@ -0,0 +1,67 @@
const items = [
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 7v1m0 8v1M9.5 9.5A2.5 2.5 0 0 1 12 8a2.5 2.5 0 0 1 0 5 2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 2.5-1.5"/></svg>,
title: 'Redukcja kosztów',
desc: 'Automatyzacja rutynowych zadań administracyjnych i marketingowych eliminuje potrzebę zatrudniania agencji lub dodatkowych pracowników, drastycznie podnosząc rentowność operacyjną firmy.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 12l9-9 9 9M5 10v9a1 1 0 0 0 1 1h4v-5h4v5h4a1 1 0 0 0 1-1v-9"/><path d="M16 6l2-3"/></svg>,
title: 'Skalowanie wideo',
desc: 'Twój jeden długi materiał (podcast/YouTube) zostaje automatycznie pocięty i przekształcony w dziesiątki viralowych klipów Shorts, Reels i TikTok, budując gigantyczne zasięgi na autopilocie.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M12 2a10 10 0 1 1 0 20A10 10 0 0 1 12 2"/><path d="M12 6v6l4 2"/></svg>,
title: 'Maksymalna szybkość',
desc: 'Skracamy czas obsługi zapytań ofertowych i powtarzalnej produkcji contentu do kilku minut. Twoja firma reaguje na ból klienta natychmiast, zostawiając ociężałą konkurencję daleko w tyle.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M12 2C8 2 5 5 5 8c0 4 4 7 7 10 3-3 7-6 7-10 0-3-2.5-6-7-6z"/><path d="M12 8v4m0 0v.01"/></svg>,
title: 'Święty spokój',
desc: 'Nasz mechanizm z pętlą „Ludzkiego Bezpiecznika" bierze na siebie całą odpowiedzialność za błędy i halucynacje AI. Odzyskujesz wolne weekendy, podczas gdy maszyny bezbłędnie wykonują brudną robotę.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 12l2 2 4-4"/></svg>,
title: 'Gotowe rezultaty',
desc: 'Nie dostajesz skomplikowanego panelu SaaS ani promptów do wklejenia. Logujesz się bezpośrednio do panelu po odbiór finalnego, zwalidowanego i perfekcyjnie przygotowanego produktu końcowego.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M20 8c0 2-2 3-2 3s-2-1-2-3a2 2 0 0 1 4 0z"/></svg>,
title: 'Autentyczny głos',
desc: 'Nasze modele GenAI trenujemy wyłącznie na Twoim unikalnym stylu komunikacji (Tone of Voice). Treści zachowują Twoją merytorykę i pasję, będąc całkowicie nieodróżnialnymi od pisanych ręcznie.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="M22 6l-10 7L2 6"/><path d="M12 13l1.5-1"/></svg>,
title: 'Oferty 5 minut',
desc: 'Inteligentne boty natychmiast skanują przychodzące maile, sprawdzają kalendarz i automatycznie generują szkice spersonalizowanych wycen biznesowych, eliminując ryzyko utraty gorącego leada.',
},
{
icon: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M3 18h3v-4H3v4zm4 0h3v-8H7v8zm4 0h3v-6h-3v6zm4 0h3V6h-3v12z"/></svg>,
title: 'Przejrzyste ROI',
desc: 'W dedykowanym dashboardzie na bieżąco monitorujesz twarde dane operacyjne. Dokładnie widzisz, ile żetonów czasu i pieniędzy zaoszczędziłeś w tym tygodniu dzięki naszym automatyzacjom.',
},
]
import Link from 'next/link'
const Benefits = () => (
<section className="benefits">
<div className="benefits__header">
<h2 className="benefits__title">Gotowe moduły AI stworzone, by natychmiast odciążyć&nbsp;Twoim biznesie</h2>
<p className="benefits__subtitle">Nie kupujesz u nas skomplikowanych wdrożeń ani godzin pracy programistów. Wybierasz konkretny, gotowy moduł AI do jednej, denerwującej czynności jak automatyczny recykling wideo czy błyskawiczne wycenianie maili i uruchamiasz go w swojej firmie w 48 godzin.</p>
</div>
<div className="benefits__grid">
{items.map((item) => (
<div key={item.title} className="benefits__card">
<div className="benefits__icon">{item.icon}</div>
<h3 className="benefits__card-title">{item.title}</h3>
<p className="benefits__card-desc">{item.desc}</p>
</div>
))}
</div>
<div className="benefits__cta">
<Link href="/produkty" className="hero__btn-primary">Wybierz moduł AI i odzyskaj czas</Link>
</div>
</section>
)
export default Benefits
+21
View File
@@ -0,0 +1,21 @@
import Link from 'next/link'
const ComingSoon = () => {
return (
<div className="coming-soon-page">
<div className="coming-soon-content">
<div className="coming-soon-badge">Wkrótce</div>
<h1 className="coming-soon-title">Dla zaawansowanych</h1>
<p className="coming-soon-desc">
Przygotowujemy pogłębione materiały dla doświadczonych praktyków AI
architektura systemów, fine-tuning, wdrożenia produkcyjne i więcej.
</p>
<Link href="/" className="coming-soon-back">
Wróć do głównej
</Link>
</div>
</div>
)
}
export default ComingSoon
+64
View File
@@ -0,0 +1,64 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
const ContactPage = () => {
const [sent, setSent] = useState(false)
const [form, setForm] = useState({ name: '', email: '', message: '' })
const handleSubmit = (e) => {
e.preventDefault()
if (form.name && form.email && form.message) setSent(true)
}
return (
<div className="coming-soon-page">
<div className="contact-content">
<div className="coming-soon-badge">Kontakt</div>
<h1 className="coming-soon-title">Napisz do nas</h1>
<p className="coming-soon-desc">
Pytania, współpraca, propozycje tematów odpisujemy w ciągu 48h.
</p>
{sent ? (
<div className="contact-success">
Wiadomość wysłana. Odezwiemy się wkrótce.
</div>
) : (
<form className="contact-form" onSubmit={handleSubmit} noValidate>
<input
type="text"
placeholder="Imię"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
required
/>
<input
type="email"
placeholder="Email"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
<textarea
placeholder="Wiadomość"
rows={5}
value={form.message}
onChange={(e) => setForm({ ...form, message: e.target.value })}
required
/>
<button type="submit" className="nav-button" style={{ width: '100%' }}>
Wyślij wiadomość
</button>
</form>
)}
<Link href="/" className="coming-soon-back" style={{ marginTop: '1.5rem', display: 'inline-block' }}>
Wróć do głównej
</Link>
</div>
</div>
)
}
export default ContactPage
+47
View File
@@ -0,0 +1,47 @@
import Link from 'next/link'
import ProductCard from '../components/ProductCard'
import { productSchema } from '../lib/productSchema'
const DIRECTUS = 'https://cms.dexterlab.pl'
async function getProducts() {
try {
const res = await fetch(
`${DIRECTUS}/items/products?filter[site][_eq]=labai.pro&filter[status][_eq]=published&sort=-date_updated&limit=3`,
{ next: { revalidate: 300 } }
)
if (!res.ok) return []
return (await res.json()).data ?? []
} catch {
return []
}
}
export default async function ProductsPreview() {
const products = await getProducts()
if (!products.length) return null
return (
<>
{products.map((p) => (
<script key={p.id} type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema(p)) }}
/>
))}
<section className="products-preview">
<div className="products-header">
<h2 className="products-title">Nasze produkty</h2>
<p className="products-subtitle">Gotowe narzędzia i kursy AI dla twórców i przedsiębiorców.</p>
</div>
<div className="products-grid">
{products.map((p) => <ProductCard key={p.id} p={p} headingLevel="h3" />)}
</div>
<div className="benefits__cta">
<Link href="/produkty" className="hero__btn-primary" style={{ textDecoration: 'none' }}>
Więcej produktów
</Link>
</div>
</section>
</>
)
}