Dodano obsługę Directus CMS, układ Bento Grid 2x2+1x2+1x1 z danymi z Laboratorium_AI

This commit is contained in:
Paweł Domański
2026-05-18 07:40:52 +02:00
parent 30fd7c59b5
commit b3ecfe85e1
6 changed files with 271 additions and 26 deletions
+2 -2
View File
@@ -19,8 +19,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-D2vGqyJ4.js"></script> <script type="module" crossorigin src="/assets/index-DVtSebH9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CKyw65S0.css"> <link rel="stylesheet" crossorigin href="/assets/index-wZazZb-P.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

+43 -4
View File
@@ -1,9 +1,10 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import './index.css'; import './index.css';
import Nav from './components/Nav'; import Nav from './components/Nav';
import BentoCard from './components/BentoCard'; import BentoCard from './components/BentoCard';
import BentoSlideshow from './components/BentoSlideshow'; import BentoSlideshow from './components/BentoSlideshow';
import CookieBanner from './components/CookieBanner'; import CookieBanner from './components/CookieBanner';
import { fetchLaboratoriumData, mapDirectusToContent } from './lib/directus';
import { slideshowData, articlesData, newsData, videosData } from './data/content'; import { slideshowData, articlesData, newsData, videosData } from './data/content';
import Hero from './sections/Hero'; import Hero from './sections/Hero';
@@ -44,21 +45,59 @@ const SubscribeBox = () => {
}; };
function App() { function App() {
const [directusData, setDirectusData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadData() {
try {
const items = await fetchLaboratoriumData();
const mapped = mapDirectusToContent(items);
setDirectusData(mapped);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadData();
}, []);
// Użyj danych z Directus lub fallback na statyczne
const displaySlideshow = directusData?.slideshow || slideshowData;
const displayArticles = directusData?.articles || articlesData;
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner" />
<p>Ładowanie danych z Laboratorium AI...</p>
</div>
);
}
return ( return (
<> <>
<Nav /> <Nav />
<Hero /> <Hero />
{error && (
<div className="error-banner">
Nie można połączyć z bazą danych. Wyświetlam dane offline.
</div>
)}
<div className="page-wrapper"> <div className="page-wrapper">
{/* CENTER BENTO AREA */} {/* CENTER BENTO AREA */}
<main className="bento-area" aria-label="Artykuły"> <main className="bento-area" aria-label="Artykuły">
<BentoSlideshow <BentoSlideshow
slides={slideshowData} slides={displaySlideshow}
style={{ gridColumn: '1 / 5', gridRow: '1 / 2' }} style={{ gridColumn: '1 / 5', gridRow: '1 / 2' }}
/> />
{articlesData.map((article, index) => ( {displayArticles.map((article, index) => (
<BentoCard <BentoCard
key={index} key={index}
type={article.type} type={article.type}
@@ -150,4 +189,4 @@ function App() {
); );
} }
export default App; export default App;
+71 -19
View File
@@ -722,35 +722,22 @@ h1, h2, h3, h4, h5, h6 {
/* ===== RESPONSYWNOŚĆ ===== */ /* ===== RESPONSYWNOŚĆ ===== */
@media (max-width: 1400px) { @media (max-width: 1400px) {
.page-wrapper { .page-wrapper {
grid-template-columns: 250px 1fr; grid-template-columns: 1fr 280px;
}
.sidebar-right {
display: none;
} }
} }
@media (max-width: 1024px) { @media (max-width: 1100px) {
.page-wrapper { .page-wrapper {
grid-template-columns: 1fr; grid-template-columns: 1fr;
padding: 1rem; padding: 1rem;
} }
.sidebar-left { .sidebar-right {
display: none; display: none;
} }
.bento-area { .bento-area {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(4, 1fr);
grid-template-rows: 300px auto; grid-template-rows: 360px repeat(3, 240px);
grid-auto-rows: 220px; grid-auto-rows: 240px;
}
.bento-area > * {
grid-column: auto !important;
grid-row: auto !important;
}
.slideshow-card {
grid-column: 1 / -1 !important;
}
.hero-article {
grid-column: 1 / -1 !important;
} }
} }
@@ -788,9 +775,34 @@ h1, h2, h3, h4, h5, h6 {
.main-nav { .main-nav {
position: relative; position: relative;
} }
.bento-area {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 300px auto;
grid-auto-rows: 220px;
}
.bento-area > * {
grid-column: auto !important;
grid-row: auto !important;
}
.slideshow-card {
grid-column: 1 / -1 !important;
}
.hero-article {
grid-column: 1 / -1 !important;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.hero {
padding: 4rem 1.25rem 3rem;
}
.hero__cta-group {
flex-direction: column;
align-items: stretch;
}
.hero__btn-primary {
justify-content: center;
}
.bento-area { .bento-area {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-auto-rows: 200px; grid-auto-rows: 200px;
@@ -806,6 +818,7 @@ 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;
@@ -1149,3 +1162,42 @@ h1, h2, h3, h4, h5, h6 {
} }
} }
/* ===== LOADING & ERROR STATES ===== */
.loading-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
background: var(--bg-color);
gap: 1.5rem;
}
.loading-spinner {
width: 48px;
height: 48px;
border: 3px solid var(--border-color);
border-top-color: var(--accent-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-screen p {
color: var(--text-muted);
font-size: 1rem;
}
.error-banner {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
padding: 1rem 2rem;
text-align: center;
}
+145
View File
@@ -0,0 +1,145 @@
/**
* Directus API Client
* Ź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() {
// 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);
return [];
}
}
/**
* 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;
// Preferuj published, fallback na wszystkie
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 slideshow = (featured.length > 0 ? featured : sourceItems.slice(0, 3)).map(item => ({
type: 'hot',
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}` : '',
}));
const slideshowTitles = new Set(slideshow.map(s => s.title));
// Pozostałe jako articles (max 8)
const articles = sourceItems
.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}` : '',
gridColumn: getGridColumn(index),
gridRow: getGridRow(index),
hero: index === 0,
}));
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';
}
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';
}
+10 -1
View File
@@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
}) server: {
proxy: {
'/api': {
target: 'https://cms.dexterlab.pl',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})