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;
|
||||
--grid-gap: 1.5rem;
|
||||
--content-width: 1200px;
|
||||
|
||||
/* Badges */
|
||||
--badge-hot: #e11d48;
|
||||
@@ -181,7 +182,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
/* ===== PAGE WRAPPER ===== */
|
||||
.page-wrapper {
|
||||
max-width: 1600px;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
@@ -663,7 +664,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
max-width: 1200px;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -681,6 +682,31 @@ h1, h2, h3, h4, h5, h6 {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -711,7 +737,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
max-width: 1200px;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
padding-top: 2rem;
|
||||
text-align: center;
|
||||
@@ -818,7 +844,6 @@ h1, h2, h3, h4, h5, h6 {
|
||||
padding: 0.75rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
.cookie-banner {
|
||||
bottom: 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 {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 5rem 2rem 4rem;
|
||||
padding: 4rem 2rem 4rem;
|
||||
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 {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -847,10 +1115,10 @@ h1, h2, h3, h4, h5, h6 {
|
||||
.hero__inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 1200px;
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 4rem;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -925,6 +1193,7 @@ h1, h2, h3, h4, h5, h6 {
|
||||
background: var(--accent-color);
|
||||
color: #fff;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
padding: 0.9rem 2rem;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
@@ -1011,6 +1280,8 @@ h1, h2, h3, h4, h5, h6 {
|
||||
/* ── Prawa strona (zdjęcie) ── */
|
||||
.hero__image-wrap {
|
||||
position: relative;
|
||||
transform: scale(1.56);
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
/* 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) */
|
||||
.hero__robot-layers {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-width: 1350px;
|
||||
aspect-ratio: 1 / 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -1193,6 +1464,114 @@ h1, h2, h3, h4, h5, h6 {
|
||||
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 {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
|
||||
+46
-119
@@ -1,145 +1,72 @@
|
||||
/**
|
||||
* Directus API Client
|
||||
* Źródło danych: https://cms.dexterlab.pl/ - kolekcja Laboratorium_AI
|
||||
*/
|
||||
const DIRECTUS_URL = 'https://cms.dexterlab.pl'
|
||||
const COLLECTION = 'LabAI'
|
||||
|
||||
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() {
|
||||
// Sprawdź cache
|
||||
if (dataCache && Date.now() - cacheTimestamp < CACHE_DURATION) {
|
||||
return dataCache;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${DIRECTUS_URL}/items/${COLLECTION}?sort=-date_published&limit=-1`,
|
||||
{
|
||||
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;
|
||||
}
|
||||
const url = `${DIRECTUS_URL}/items/${COLLECTION}?filter[status][_eq]=published&sort=-date_published&limit=-1`
|
||||
const headers = process.env.DIRECTUS_TOKEN
|
||||
? { Authorization: `Bearer ${process.env.DIRECTUS_TOKEN}` }
|
||||
: {}
|
||||
const response = await fetch(url, { headers })
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
|
||||
const result = await response.json()
|
||||
return result.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (!items || items.length === 0) return null;
|
||||
if (!items || items.length === 0) return null
|
||||
|
||||
// Preferuj published, fallback na wszystkie
|
||||
const published = items.filter(item => item.status === 'published');
|
||||
const sourceItems = published.length > 0 ? published : items;
|
||||
const sourceItems = 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 => ({
|
||||
type: 'hot',
|
||||
badge: item.badge || null,
|
||||
title: item.title,
|
||||
category: item.category || 'AI',
|
||||
badge: item.Badge || null,
|
||||
title: item.Title,
|
||||
category: item.Category || 'AI',
|
||||
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
|
||||
.filter(item => !slideshowTitles.has(item.title))
|
||||
.filter(item => !slideshowTitles.has(item.Title))
|
||||
.slice(0, 8)
|
||||
.map((item, index) => ({
|
||||
type: item.badge?.toLowerCase() || 'article',
|
||||
badge: item.badge || null,
|
||||
title: item.title,
|
||||
category: item.category || 'AI',
|
||||
readTime: item.read_time || '5 min read',
|
||||
bgImage: item.cover_image ? `${DIRECTUS_URL}/assets/${item.cover_image}` : '',
|
||||
type: item.Badge?.toLowerCase() || 'article',
|
||||
badge: item.Badge || null,
|
||||
title: item.Title,
|
||||
category: item.Category || 'AI',
|
||||
readTime: '5 min read',
|
||||
bgImage: item.Cover_Image ? `${DIRECTUS_URL}/assets/${item.Cover_Image}` : '',
|
||||
gridColumn: getGridColumn(index),
|
||||
gridRow: getGridRow(index),
|
||||
hero: index === 0,
|
||||
}));
|
||||
}))
|
||||
|
||||
return { slideshow, articles };
|
||||
return { slideshow, articles }
|
||||
}
|
||||
|
||||
function getGridColumn(index) {
|
||||
// Jasny układ dla 4-kolumnowej siatki
|
||||
// 1: 2/3 (2 kol), 2: 3/4 (1 kol), 3: 4/5 (1 kol), 4: 4/5 (1 kol)
|
||||
// 5: 1/2 (1 kol), 6: 2/3 (1 kol), 7: 3/4 (1 kol), 8: 4/5 (1 kol)
|
||||
if (index === 0) return '1 / 3'; // 2 kolumny
|
||||
if (index === 1) return '3 / 4'; // 1 kolumna
|
||||
if (index === 2) return '4 / 5'; // 1 kolumna
|
||||
if (index === 3) return '4 / 5'; // 1 kolumna
|
||||
if (index === 4) return '1 / 2'; // 1 kolumna
|
||||
if (index === 5) return '2 / 3'; // 1 kolumna
|
||||
if (index === 6) return '3 / 4'; // 1 kolumna
|
||||
if (index === 7) return '4 / 5'; // 1 kolumna
|
||||
return '1 / 2';
|
||||
if (index === 0) return '1 / 3'
|
||||
if (index === 1) return '3 / 4'
|
||||
if (index === 2) return '4 / 5'
|
||||
if (index === 3) return '4 / 5'
|
||||
if (index === 4) return '1 / 2'
|
||||
if (index === 5) return '2 / 3'
|
||||
if (index === 6) return '3 / 4'
|
||||
if (index === 7) return '4 / 5'
|
||||
return '1 / 2'
|
||||
}
|
||||
|
||||
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'; // 2 rzędy
|
||||
if (index === 1) return '2 / 4'; // 2 rzędy
|
||||
if (index === 2) return '2 / 3'; // 1 rząd
|
||||
if (index === 3) return '3 / 4'; // 1 rząd
|
||||
if (index === 4) return '4 / 5'; // 1 rząd
|
||||
if (index === 5) return '4 / 5'; // 1 rząd
|
||||
if (index === 6) return '4 / 5'; // 1 rząd
|
||||
if (index === 7) return '4 / 5'; // 1 rząd
|
||||
return '4 / 5';
|
||||
if (index === 0) return '2 / 4'
|
||||
if (index === 1) return '2 / 4'
|
||||
if (index === 2) return '2 / 3'
|
||||
if (index === 3) return '3 / 4'
|
||||
if (index === 4) return '4 / 5'
|
||||
if (index === 5) return '4 / 5'
|
||||
if (index === 6) return '4 / 5'
|
||||
if (index === 7) return '4 / 5'
|
||||
return '4 / 5'
|
||||
}
|
||||
Reference in New Issue
Block a user