Integracja Directus LabAI, naprawa Schema.org, poprawki UI
- Zmiana kolekcji Directus z Laboratorium_AI na LabAI - Usunięcie fallbacku na fake dane — brak wpisów = komunikat - revalidate=0 na stronie głównej (zawsze świeże dane) - BlogPosting przeniesiony do @graph (poprawna spec Schema.org) - Inbox Libredesk zaktualizowany - Hero image-wrap powiększony (scale 1.56) - Dokumentacja projektu w CLAUDE.md Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
import ComingSoon from '../../sections/ComingSoon'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Dla zaawansowanych — Laboratorium AI',
|
||||||
|
description: 'Pogłębione materiały dla doświadczonych praktyków AI.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DlaZaawansowanychPage() {
|
||||||
|
return <ComingSoon />
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import ContactPage from '../../sections/ContactPage'
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Kontakt — Laboratorium AI',
|
||||||
|
description: 'Napisz do nas. Odpowiadamy w ciągu 48h.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KontaktPage() {
|
||||||
|
return <ContactPage />
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import Script from 'next/script'
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import '../index.css'
|
||||||
|
import Nav from '../components/Nav'
|
||||||
|
import Footer from '../components/Footer'
|
||||||
|
import CookieBanner from '../components/CookieBanner'
|
||||||
|
import SchemaOrg from '../components/SchemaOrg'
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ['latin-ext'],
|
||||||
|
weight: ['300', '400', '500', '600', '700', '800'],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Laboratorium AI — Automatyzacja procesów biznesowych',
|
||||||
|
description: 'Automatyzacja procesów biznesowych bez programowania. Wdrażamy AI i no-code w 48h.',
|
||||||
|
openGraph: {
|
||||||
|
type: 'website',
|
||||||
|
title: 'Laboratorium AI — Automatyzacja procesów biznesowych',
|
||||||
|
description: 'Automatyzacja procesów biznesowych bez programowania. Wdrażamy AI i no-code w 48h.',
|
||||||
|
images: ['/img/robot.png'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<SchemaOrg />
|
||||||
|
</head>
|
||||||
|
<body className={inter.className}>
|
||||||
|
<Nav />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
<CookieBanner />
|
||||||
|
<Script src="https://stat.dexterlab.pl/api/script.js" data-site-id="e7d7e9823a23" strategy="afterInteractive" />
|
||||||
|
<Script id="libredesk-config" strategy="beforeInteractive">{`window.LibredeskSettings = { baseURL: 'https://pomoc.dexterlab.pl', inboxID: 'bffd72d7-a528-42ca-bfed-377670d49364' };`}</Script>
|
||||||
|
<Script src="https://pomoc.dexterlab.pl/widget.js" strategy="afterInteractive" />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import Hero from '../sections/Hero'
|
||||||
|
import Benefits from '../sections/Benefits'
|
||||||
|
import ProductsPreview from '../sections/ProductsPreview'
|
||||||
|
import BentoCard from '../components/BentoCard'
|
||||||
|
import BentoSlideshow from '../components/BentoSlideshow'
|
||||||
|
import SubscribeBox from '../components/SubscribeBox'
|
||||||
|
import { fetchLaboratoriumData, mapDirectusToContent } from '../lib/directus'
|
||||||
|
import { newsData, videosData } from '../data/content'
|
||||||
|
|
||||||
|
export const revalidate = 0
|
||||||
|
|
||||||
|
async function getContent() {
|
||||||
|
try {
|
||||||
|
const items = await fetchLaboratoriumData()
|
||||||
|
return mapDirectusToContent(items)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function HomePage() {
|
||||||
|
const content = await getContent()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<Benefits />
|
||||||
|
<ProductsPreview />
|
||||||
|
<div className="page-wrapper">
|
||||||
|
<main className="bento-area" aria-label="Artykuły">
|
||||||
|
{!content ? (
|
||||||
|
<p style={{ gridColumn: '1 / 5', padding: '4rem', textAlign: 'center', color: '#888' }}>
|
||||||
|
W tej chwili brak wpisów.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<BentoSlideshow
|
||||||
|
slides={content.slideshow}
|
||||||
|
style={{ gridColumn: '1 / 5', gridRow: '1 / 2' }}
|
||||||
|
/>
|
||||||
|
{content.articles.map((article, index) => (
|
||||||
|
<BentoCard
|
||||||
|
key={index}
|
||||||
|
type={article.type}
|
||||||
|
badge={article.badge}
|
||||||
|
title={article.title}
|
||||||
|
category={article.category}
|
||||||
|
readTime={article.readTime}
|
||||||
|
bgImage={article.bgImage}
|
||||||
|
hero={article.hero}
|
||||||
|
style={{ gridColumn: article.gridColumn, gridRow: article.gridRow }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<aside className="sidebar sidebar-right" aria-label="Najnowsze wideo">
|
||||||
|
<div>
|
||||||
|
<h3 className="sidebar-title">LATEST VIDEOS</h3>
|
||||||
|
{videosData.map((video, index) => (
|
||||||
|
<div key={index} className="video-item" tabIndex={0}>
|
||||||
|
<div
|
||||||
|
className="video-thumb"
|
||||||
|
style={{ backgroundImage: `url(${video.bgImage})` }}
|
||||||
|
role="img"
|
||||||
|
aria-label={video.title}
|
||||||
|
/>
|
||||||
|
<div className="video-info">
|
||||||
|
<div className="video-title">{video.title}</div>
|
||||||
|
<div className="video-views">{video.views}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SubscribeBox />
|
||||||
|
|
||||||
|
<div style={{ marginTop: '2rem' }}>
|
||||||
|
<h3 className="sidebar-title">LATEST AI NEWS</h3>
|
||||||
|
<div className="news-list">
|
||||||
|
{newsData.map((item, index) => (
|
||||||
|
<div key={index} className="news-item" tabIndex={0}>
|
||||||
|
<div className="news-time">{item.time}</div>
|
||||||
|
<div className="news-title">{item.title}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import ProductCard from '../../components/ProductCard'
|
||||||
|
import { productSchema } from '../../lib/productSchema'
|
||||||
|
|
||||||
|
const DIRECTUS = 'https://cms.dexterlab.pl'
|
||||||
|
|
||||||
|
export const revalidate = 300
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'Produkty — Laboratorium AI',
|
||||||
|
description: 'Moduły AI-RaaS — gotowe automatyzacje dla mikroprzedsiębiorców.',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProducts() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${DIRECTUS}/items/products?filter[site][_eq]=labai.pro&filter[status][_eq]=published&sort=sort`,
|
||||||
|
{ next: { revalidate: 300 } }
|
||||||
|
)
|
||||||
|
if (!res.ok) return []
|
||||||
|
return (await res.json()).data ?? []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProduktyPage() {
|
||||||
|
const products = await getProducts()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{products.map((p) => (
|
||||||
|
<script key={p.id} type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(productSchema(p)) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="products-page">
|
||||||
|
<div className="products-header">
|
||||||
|
<div className="coming-soon-badge">Moduły AI-RaaS</div>
|
||||||
|
<h1 className="products-title">Wybierz swój moduł AI</h1>
|
||||||
|
<p className="products-subtitle">Gotowe automatyzacje dla mikroprzedsiębiorców i twórców treści.</p>
|
||||||
|
</div>
|
||||||
|
<div className="products-grid">
|
||||||
|
{products.map((p) => <ProductCard key={p.id} p={p} headingLevel="h2" />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
const DIRECTUS_URL = 'https://cms.dexterlab.pl'
|
||||||
|
const DOMAIN = 'labai.pro'
|
||||||
|
const BASE_URL = `https://${DOMAIN}`
|
||||||
|
|
||||||
|
const FALLBACK = {
|
||||||
|
name: 'Laboratorium AI',
|
||||||
|
description: 'Automatyzacja procesów biznesowych bez programowania. Wdrażamy AI i no-code w 48h.',
|
||||||
|
logo: `${BASE_URL}/logo.webp`,
|
||||||
|
sameAs: [
|
||||||
|
'https://linkedin.com/company/dexterlab',
|
||||||
|
'https://x.com/dexterlab_pl',
|
||||||
|
'https://facebook.com/dexterlab',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchSiteData() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${DIRECTUS_URL}/items/sites?filter[domain][_eq]=${DOMAIN}`,
|
||||||
|
{ next: { revalidate: 3600 } }
|
||||||
|
)
|
||||||
|
if (!res.ok) return null
|
||||||
|
const json = await res.json()
|
||||||
|
return json.data?.[0] ?? null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchArticles() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${DIRECTUS_URL}/items/LabAI?sort=-date_published&limit=5&fields=Title,Slug`,
|
||||||
|
{ next: { revalidate: 300 } }
|
||||||
|
)
|
||||||
|
if (!res.ok) return []
|
||||||
|
const json = await res.json()
|
||||||
|
return json.data ?? []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSchema(site, articles) {
|
||||||
|
const s = site ?? {}
|
||||||
|
const baseUrl = (s.url ?? BASE_URL).replace(/\/$/, '')
|
||||||
|
const name = s.site_name ?? FALLBACK.name
|
||||||
|
const description = s.site_description ?? FALLBACK.description
|
||||||
|
const logo = s.logo ?? { '@type': 'ImageObject', url: FALLBACK.logo }
|
||||||
|
const sameAs = Array.isArray(s.social_links) && s.social_links.length
|
||||||
|
? s.social_links
|
||||||
|
: FALLBACK.sameAs
|
||||||
|
|
||||||
|
const blogPostings = articles.map((a) => ({
|
||||||
|
'@type': 'BlogPosting',
|
||||||
|
'@id': `${baseUrl}/blog/${a.Slug}`,
|
||||||
|
headline: a.Title,
|
||||||
|
url: `${baseUrl}/blog/${a.Slug}`,
|
||||||
|
isPartOf: { '@id': `${baseUrl}/#webpage` },
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@graph': [
|
||||||
|
{
|
||||||
|
'@type': 'WebSite',
|
||||||
|
'@id': `${baseUrl}/#website`,
|
||||||
|
url: `${baseUrl}/`,
|
||||||
|
name,
|
||||||
|
publisher: { '@id': `${baseUrl}/#organization` },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'WebPage',
|
||||||
|
'@id': `${baseUrl}/#webpage`,
|
||||||
|
url: `${baseUrl}/`,
|
||||||
|
name,
|
||||||
|
isPartOf: { '@id': `${baseUrl}/#website` },
|
||||||
|
description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@type': 'Organization',
|
||||||
|
'@id': `${baseUrl}/#organization`,
|
||||||
|
name: s.company_name ?? name,
|
||||||
|
url: `${baseUrl}/`,
|
||||||
|
logo,
|
||||||
|
sameAs,
|
||||||
|
...(s.contact && { contactPoint: s.contact }),
|
||||||
|
},
|
||||||
|
...blogPostings,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SchemaOrg() {
|
||||||
|
const [site, articles] = await Promise.all([fetchSiteData(), fetchArticles()])
|
||||||
|
const schema = buildSchema(site, articles)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
+387
-8
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
--card-radius: 1.25rem;
|
--card-radius: 1.25rem;
|
||||||
--grid-gap: 1.5rem;
|
--grid-gap: 1.5rem;
|
||||||
|
--content-width: 1200px;
|
||||||
|
|
||||||
/* Badges */
|
/* Badges */
|
||||||
--badge-hot: #e11d48;
|
--badge-hot: #e11d48;
|
||||||
@@ -181,7 +182,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
|
|
||||||
/* ===== PAGE WRAPPER ===== */
|
/* ===== PAGE WRAPPER ===== */
|
||||||
.page-wrapper {
|
.page-wrapper {
|
||||||
max-width: 1600px;
|
max-width: var(--content-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -663,7 +664,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-container {
|
.footer-container {
|
||||||
max-width: 1200px;
|
max-width: var(--content-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -681,6 +682,31 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.footer-social {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social__link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-social__link:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
background: rgba(79, 70, 229, 0.15);
|
||||||
|
border-color: rgba(79, 70, 229, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
.footer-links {
|
.footer-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -711,7 +737,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.footer-bottom {
|
.footer-bottom {
|
||||||
max-width: 1200px;
|
max-width: var(--content-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -818,7 +844,6 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.cookie-banner {
|
.cookie-banner {
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@@ -827,14 +852,257 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== PRODUCTS PAGE ===== */
|
||||||
|
.products-page {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-subtitle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card {
|
||||||
|
background: var(--bg-accent);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card:hover { border-color: rgba(79, 70, 229, 0.4); }
|
||||||
|
|
||||||
|
.product-img {
|
||||||
|
height: 220px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card--featured { border-color: rgba(79, 70, 229, 0.5); }
|
||||||
|
|
||||||
|
.product-featured-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-card { position: relative; }
|
||||||
|
|
||||||
|
.product-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-tag {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: rgba(79, 70, 229, 0.12);
|
||||||
|
color: #818cf8;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-type {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-desc {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 1rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price { display: flex; align-items: baseline; gap: 0.5rem; }
|
||||||
|
|
||||||
|
.product-price--promo {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price--old {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-price--regular {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) { .products-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||||
|
@media (max-width: 600px) { .products-grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
.products-preview {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== BENEFITS SECTION ===== */
|
||||||
|
.benefits {
|
||||||
|
padding: 5rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__header {
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__subtitle {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__grid {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__card {
|
||||||
|
background: var(--bg-accent);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
padding: 1.75rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__card:hover {
|
||||||
|
border-color: rgba(79, 70, 229, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
color: var(--accent-color);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__card-desc {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits__cta {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.benefits__grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.benefits__grid { grid-template-columns: 1fr; }
|
||||||
|
.benefits__title { font-size: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== HERO SECTION ===== */
|
/* ===== HERO SECTION ===== */
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 5rem 2rem 4rem;
|
padding: 4rem 2rem 4rem;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero__eyebrow-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero__eyebrow-list li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
.hero__bg {
|
.hero__bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -847,10 +1115,10 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
.hero__inner {
|
.hero__inner {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
max-width: 1200px;
|
max-width: var(--content-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 2fr 1fr;
|
||||||
gap: 4rem;
|
gap: 4rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -925,6 +1193,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
background: var(--accent-color);
|
background: var(--accent-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
padding: 0.9rem 2rem;
|
padding: 0.9rem 2rem;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@@ -1011,6 +1280,8 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
/* ── Prawa strona (zdjęcie) ── */
|
/* ── Prawa strona (zdjęcie) ── */
|
||||||
.hero__image-wrap {
|
.hero__image-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transform: scale(1.56);
|
||||||
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* warstwa 2: float — wszystko lata razem */
|
/* warstwa 2: float — wszystko lata razem */
|
||||||
@@ -1030,7 +1301,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
/* warstwa 4: obraz z blaskiem żarówki (żółty drop-shadow kierunkowy) */
|
/* warstwa 4: obraz z blaskiem żarówki (żółty drop-shadow kierunkowy) */
|
||||||
.hero__robot-layers {
|
.hero__robot-layers {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 1350px;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -1193,6 +1464,114 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coming-soon-page {
|
||||||
|
min-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(79, 70, 229, 0.15);
|
||||||
|
border: 1px solid rgba(79, 70, 229, 0.4);
|
||||||
|
color: #818cf8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0.35rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-desc {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 480px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input,
|
||||||
|
.contact-form textarea {
|
||||||
|
background: var(--bg-accent);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input::placeholder,
|
||||||
|
.contact-form textarea::placeholder {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form input:focus,
|
||||||
|
.contact-form textarea:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-success {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #6ee7b7;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-back {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-hover);
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coming-soon-back:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
.error-banner {
|
.error-banner {
|
||||||
background: rgba(239, 68, 68, 0.1);
|
background: rgba(239, 68, 68, 0.1);
|
||||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||||
|
|||||||
+46
-119
@@ -1,145 +1,72 @@
|
|||||||
/**
|
const DIRECTUS_URL = 'https://cms.dexterlab.pl'
|
||||||
* Directus API Client
|
const COLLECTION = 'LabAI'
|
||||||
* Źródło danych: https://cms.dexterlab.pl/ - kolekcja Laboratorium_AI
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DIRECTUS_URL = import.meta.env.DEV ? '/api' : 'https://cms.dexterlab.pl';
|
|
||||||
const COLLECTION = 'Laboratorium_AI';
|
|
||||||
|
|
||||||
// Cache storage
|
|
||||||
let dataCache = null;
|
|
||||||
let cacheTimestamp = 0;
|
|
||||||
const CACHE_DURATION = 5 * 60 * 1000; // 5 minut
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pobiera wszystkie elementy z kolekcji Laboratorium_AI
|
|
||||||
*/
|
|
||||||
export async function fetchLaboratoriumData() {
|
export async function fetchLaboratoriumData() {
|
||||||
// Sprawdź cache
|
const url = `${DIRECTUS_URL}/items/${COLLECTION}?filter[status][_eq]=published&sort=-date_published&limit=-1`
|
||||||
if (dataCache && Date.now() - cacheTimestamp < CACHE_DURATION) {
|
const headers = process.env.DIRECTUS_TOKEN
|
||||||
return dataCache;
|
? { Authorization: `Bearer ${process.env.DIRECTUS_TOKEN}` }
|
||||||
}
|
: {}
|
||||||
|
const response = await fetch(url, { headers })
|
||||||
try {
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
const response = await fetch(
|
const result = await response.json()
|
||||||
`${DIRECTUS_URL}/items/${COLLECTION}?sort=-date_published&limit=-1`,
|
return result.data
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Origin': window.location.origin,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
|
|
||||||
dataCache = result.data;
|
|
||||||
cacheTimestamp = Date.now();
|
|
||||||
|
|
||||||
return result.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd pobierania danych z Directus:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Pobiera pojedynczy element po ID
|
|
||||||
*/
|
|
||||||
export async function fetchLaboratoriumItem(id) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${DIRECTUS_URL}/items/${COLLECTION}/${id}`);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
return result.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Błąd pobierania elementu ${id}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Czyści cache - wymusza odświeżenie
|
|
||||||
*/
|
|
||||||
export function clearCache() {
|
|
||||||
dataCache = null;
|
|
||||||
cacheTimestamp = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mapuje dane z Directus na format strony
|
|
||||||
*/
|
|
||||||
export function mapDirectusToContent(items) {
|
export function mapDirectusToContent(items) {
|
||||||
if (!items || items.length === 0) return null;
|
if (!items || items.length === 0) return null
|
||||||
|
|
||||||
// Preferuj published, fallback na wszystkie
|
const sourceItems = items
|
||||||
const published = items.filter(item => item.status === 'published');
|
|
||||||
const sourceItems = published.length > 0 ? published : items;
|
|
||||||
|
|
||||||
// Slideshow - featured lub pierwsze 3
|
const featured = sourceItems.filter(item => item.Is_Featured === true)
|
||||||
const featured = sourceItems.filter(item => item.is_featured === true);
|
|
||||||
const slideshow = (featured.length > 0 ? featured : sourceItems.slice(0, 3)).map(item => ({
|
const slideshow = (featured.length > 0 ? featured : sourceItems.slice(0, 3)).map(item => ({
|
||||||
type: 'hot',
|
type: 'hot',
|
||||||
badge: item.badge || null,
|
badge: item.Badge || null,
|
||||||
title: item.title,
|
title: item.Title,
|
||||||
category: item.category || 'AI',
|
category: item.Category || 'AI',
|
||||||
readTime: '5 min read',
|
readTime: '5 min read',
|
||||||
bgImage: item.cover_image ? `${DIRECTUS_URL}/assets/${item.cover_image}` : '',
|
bgImage: item.Cover_Image ? `${DIRECTUS_URL}/assets/${item.Cover_Image}` : '',
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const slideshowTitles = new Set(slideshow.map(s => s.title));
|
const slideshowTitles = new Set(slideshow.map(s => s.title))
|
||||||
|
|
||||||
// Pozostałe jako articles (max 8)
|
|
||||||
const articles = sourceItems
|
const articles = sourceItems
|
||||||
.filter(item => !slideshowTitles.has(item.title))
|
.filter(item => !slideshowTitles.has(item.Title))
|
||||||
.slice(0, 8)
|
.slice(0, 8)
|
||||||
.map((item, index) => ({
|
.map((item, index) => ({
|
||||||
type: item.badge?.toLowerCase() || 'article',
|
type: item.Badge?.toLowerCase() || 'article',
|
||||||
badge: item.badge || null,
|
badge: item.Badge || null,
|
||||||
title: item.title,
|
title: item.Title,
|
||||||
category: item.category || 'AI',
|
category: item.Category || 'AI',
|
||||||
readTime: item.read_time || '5 min read',
|
readTime: '5 min read',
|
||||||
bgImage: item.cover_image ? `${DIRECTUS_URL}/assets/${item.cover_image}` : '',
|
bgImage: item.Cover_Image ? `${DIRECTUS_URL}/assets/${item.Cover_Image}` : '',
|
||||||
gridColumn: getGridColumn(index),
|
gridColumn: getGridColumn(index),
|
||||||
gridRow: getGridRow(index),
|
gridRow: getGridRow(index),
|
||||||
hero: index === 0,
|
hero: index === 0,
|
||||||
}));
|
}))
|
||||||
|
|
||||||
return { slideshow, articles };
|
return { slideshow, articles }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGridColumn(index) {
|
function getGridColumn(index) {
|
||||||
// Jasny układ dla 4-kolumnowej siatki
|
if (index === 0) return '1 / 3'
|
||||||
// 1: 2/3 (2 kol), 2: 3/4 (1 kol), 3: 4/5 (1 kol), 4: 4/5 (1 kol)
|
if (index === 1) return '3 / 4'
|
||||||
// 5: 1/2 (1 kol), 6: 2/3 (1 kol), 7: 3/4 (1 kol), 8: 4/5 (1 kol)
|
if (index === 2) return '4 / 5'
|
||||||
if (index === 0) return '1 / 3'; // 2 kolumny
|
if (index === 3) return '4 / 5'
|
||||||
if (index === 1) return '3 / 4'; // 1 kolumna
|
if (index === 4) return '1 / 2'
|
||||||
if (index === 2) return '4 / 5'; // 1 kolumna
|
if (index === 5) return '2 / 3'
|
||||||
if (index === 3) return '4 / 5'; // 1 kolumna
|
if (index === 6) return '3 / 4'
|
||||||
if (index === 4) return '1 / 2'; // 1 kolumna
|
if (index === 7) return '4 / 5'
|
||||||
if (index === 5) return '2 / 3'; // 1 kolumna
|
return '1 / 2'
|
||||||
if (index === 6) return '3 / 4'; // 1 kolumna
|
|
||||||
if (index === 7) return '4 / 5'; // 1 kolumna
|
|
||||||
return '1 / 2';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGridRow(index) {
|
function getGridRow(index) {
|
||||||
// Wiersze dla układu: 2 duże na górze (2/4), reszta po 1 (3/4, 4/5)
|
if (index === 0) return '2 / 4'
|
||||||
if (index === 0) return '2 / 4'; // 2 rzędy
|
if (index === 1) return '2 / 4'
|
||||||
if (index === 1) return '2 / 4'; // 2 rzędy
|
if (index === 2) return '2 / 3'
|
||||||
if (index === 2) return '2 / 3'; // 1 rząd
|
if (index === 3) return '3 / 4'
|
||||||
if (index === 3) return '3 / 4'; // 1 rząd
|
if (index === 4) return '4 / 5'
|
||||||
if (index === 4) return '4 / 5'; // 1 rząd
|
if (index === 5) return '4 / 5'
|
||||||
if (index === 5) return '4 / 5'; // 1 rząd
|
if (index === 6) return '4 / 5'
|
||||||
if (index === 6) return '4 / 5'; // 1 rząd
|
if (index === 7) return '4 / 5'
|
||||||
if (index === 7) return '4 / 5'; // 1 rząd
|
return '4 / 5'
|
||||||
return '4 / 5';
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user