// Fix: Refactored component rendering from `React.createElement` to JSX syntax.
// This resolves TypeScript errors related to incorrect prop type inference for
// DOM elements and improves code readability and maintainability.
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { createRoot } from 'react-dom/client';
// --- TYPE DEFINITIONS ---
type Photo = {
id: number;
src: string;
title: string;
date: string;
cycle: string;
description: string;
technique: string;
dimensions: string;
tags: string[];
};
type Filters = {
cycle: string[];
year: number[];
searchTerm: string;
};
type SortOrder = 'random' | 'date-desc' | 'date-asc' | 'cycle-asc' | 'cycle-desc' | 'size-desc' | 'size-asc';
// --- DATA STRUCTURE ---
const navItems = [
{ id: 'portfolio', title: 'Portfolio' },
{ id: 'bio', title: 'Biografia' },
{ id: 'exhibitions', title: 'Wystawy' },
{ id: 'contact', title: 'Kontakt' }
] as const;
type PageName = typeof navItems[number]['id'];
// --- UTILITY FUNCTIONS ---
const shuffleArray = (array: Photo[]): Photo[] => {
const newArr = [...array];
for (let i = newArr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArr[i], newArr[j]] = [newArr[j], newArr[i]];
}
return newArr;
};
const parseDimensions = (dimString: string): number => {
if (!dimString || typeof dimString !== 'string') return 0;
const parts = dimString.toLowerCase().replace(/cm/g, '').split('x');
if (parts.length !== 2) return 0;
const width = parseFloat(parts[0]);
const height = parseFloat(parts[1]);
if (isNaN(width) || isNaN(height)) return 0;
return width * height;
};
// --- COMPONENTS ---
const Carousel = ({ images }: { images: string[] }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const goToNext = useCallback(() => {
setCurrentIndex(prevIndex => (prevIndex === images.length - 1 ? 0 : prevIndex + 1));
}, [images.length]);
const goToPrevious = () => {
setCurrentIndex(prevIndex => (prevIndex === 0 ? images.length - 1 : prevIndex - 1));
};
useEffect(() => {
const timer = setInterval(() => {
goToNext();
}, 5000); // Zmień slajd co 5 sekund
return () => clearInterval(timer);
}, [goToNext]);
if (!images || images.length === 0) {
return null;
}
return (
{images.map((src, index) => (
))}
❮
❯
);
};
type HeaderProps = {
currentPage: PageName;
setCurrentPage: React.Dispatch>;
};
const Header = ({ currentPage, setCurrentPage }: HeaderProps) => (
setCurrentPage('portfolio')}>MAREK PRZYBYŁ
{navItems.map(item => (
setCurrentPage(item.id)}
>
{item.title}
))}
);
type SortControlsProps = {
sortOrder: SortOrder;
setSortOrder: React.Dispatch>;
};
const SortControls = ({ sortOrder, setSortOrder }: SortControlsProps) => (
setSortOrder(e.target.value as SortOrder)}
>
Losowo
Data (od najnowszych)
Data (od najstarszych)
Cykl (A-Z)
Cykl (Z-A)
Wymiary (od największych)
Wymiary (od najmniejszych)
);
type FilterPanelProps = {
photos: Photo[];
activeFilters: Filters;
setFilters: React.Dispatch>;
sortOrder: SortOrder;
setSortOrder: React.Dispatch>;
};
const FilterPanel = ({ photos, activeFilters, setFilters, sortOrder, setSortOrder }: FilterPanelProps) => {
const [isCollapsed, setIsCollapsed] = useState(true);
const cycles = useMemo(() => [...new Set(photos.map(p => p.cycle))], [photos]);
const years = useMemo(() => [...new Set(photos.map(p => new Date(p.date).getFullYear()))].sort((a: number, b: number) => b - a), [photos]);
const handleFilterClick = (type: 'cycle' | 'year', value: string | number, e: React.MouseEvent) => {
const ctrlPressed = e.ctrlKey || e.metaKey; // metaKey for Mac support
setFilters(prev => {
const newFilters = { ...prev };
const currentFilterValues = prev[type] as (string | number)[];
if (ctrlPressed) {
const newValues = currentFilterValues.includes(value)
? currentFilterValues.filter(v => v !== value)
: [...currentFilterValues, value];
newFilters[type] = newValues as any;
} else {
const isOnlyOneSelected = currentFilterValues.length === 1 && currentFilterValues[0] === value;
newFilters[type] = isOnlyOneSelected ? [] : [value] as any;
}
return newFilters;
});
};
const handleSearchChange = (e: React.ChangeEvent) => {
setFilters(prev => ({ ...prev, searchTerm: e.target.value, cycle: prev.cycle, year: prev.year }));
};
const clearFilters = () => setFilters({ cycle: [], year: [], searchTerm: '' });
return (
);
};
type PhotoGridProps = {
photos: Photo[];
onPhotoClick: (index: number) => void;
};
const PhotoGrid = ({ photos, onPhotoClick }: PhotoGridProps) => (
{photos.map((photo, index) => (
onPhotoClick(index)}>
{photo.title}
))}
);
type LightboxProps = {
photos: Photo[];
activeIndex: number | null;
onClose: () => void;
onNavigate: (direction: number) => void;
};
const Lightbox = ({ photos, activeIndex, onClose, onNavigate }: LightboxProps) => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft') onNavigate(-1);
if (e.key === 'ArrowRight') onNavigate(1);
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose, onNavigate]);
if (activeIndex === null || !photos[activeIndex]) return null;
const photo = photos[activeIndex];
return (
e.stopPropagation()}>
{photo.title}
Rok
{' '}{new Date(photo.date).getFullYear()}
{photo.description &&
Opis
{' '}{photo.description}
}
{photo.technique &&
Technika
{' '}{photo.technique}
}
{photo.dimensions &&
Wymiary
{' '}{photo.dimensions}
}
×
{ e.stopPropagation(); onNavigate(-1); }}>❮
{ e.stopPropagation(); onNavigate(1); }}>❯
);
};
type ContentPageProps = {
pageName: Exclude;
};
const ContentPage = ({ pageName }: ContentPageProps) => {
const [parts, setParts] = useState([]);
const [carouselImages, setCarouselImages] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
setParts([]);
setCarouselImages([]);
const fetchContent = async () => {
const cacheBuster = `?v=${new Date().getTime()}`;
try {
const response = await fetch(`./${pageName}.html${cacheBuster}`);
if (!response.ok) {
throw new Error(`Nie można wczytać pliku ${pageName}.html`);
}
const html = await response.text();
const htmlParts = html.split('');
setParts(htmlParts);
if ((pageName === 'bio' || pageName === 'exhibitions') && htmlParts.length > 1) {
try {
const carouselResponse = await fetch(`./carousel_${pageName}.json${cacheBuster}`);
if (carouselResponse.ok) {
const images = await carouselResponse.json();
setCarouselImages(images);
} else {
console.warn(`Plik carousel_${pageName}.json nie został znaleziony lub jest pusty.`);
}
} catch (carouselError) {
console.error(`Błąd wczytywania karuzeli dla ${pageName}:`, carouselError);
}
}
} catch (err) {
console.error(err);
setError('Wystąpił błąd podczas ładowania treści.');
} finally {
setLoading(false);
}
};
fetchContent();
}, [pageName]);
const containerClasses = ['container', 'content-page'];
if (pageName === 'bio' || pageName === 'exhibitions') {
containerClasses.push('content-page--full-width');
}
if (pageName === 'exhibitions') {
containerClasses.push('content-page--exhibitions');
}
return (
{loading &&
Ładowanie...
}
{error &&
{error}
}
{!loading && !error && (
<>
{parts[0] &&
}
{parts.length > 1 &&
}
{parts[1] &&
}
>
)}
);
};
const Footer = () => (
);
const App = () => {
const [photos, setPhotos] = useState([]);
const [initialOrder, setInitialOrder] = useState([]);
const [currentPage, setCurrentPage] = useState('portfolio');
const [filters, setFilters] = useState({ cycle: [], year: [], searchTerm: '' });
const [sortOrder, setSortOrder] = useState('random');
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(null);
const [visibleCount, setVisibleCount] = useState(12); // Pokaż 12 zdjęć na start
useEffect(() => {
const cacheBuster = `?v=${new Date().getTime()}`;
fetch(`./images.json${cacheBuster}`)
.then(response => {
if (!response.ok) {
throw new Error("Nie można wczytać pliku images.json");
}
return response.json();
})
.then((data: Photo[]) => {
setPhotos(data);
setInitialOrder(shuffleArray(data));
})
.catch(error => console.error('Błąd wczytywania danych zdjęć:', error));
}, []);
const processedPhotos = useMemo(() => {
const photosToFilter = sortOrder === 'random' ? initialOrder : photos;
const filtered = photosToFilter.filter(photo => {
const year = new Date(photo.date).getFullYear();
if (filters.cycle.length > 0 && !filters.cycle.includes(photo.cycle)) return false;
if (filters.year.length > 0 && !filters.year.includes(year)) return false;
if (filters.searchTerm) {
const searchTerm = filters.searchTerm.toLowerCase();
const searchableText = `${photo.title} ${photo.cycle} ${photo.date} ${photo.description} ${photo.technique} ${photo.tags.join(' ')}`.toLowerCase();
if (!searchableText.includes(searchTerm)) return false;
}
return true;
});
if (sortOrder === 'random') {
return filtered;
}
const sorted = [...filtered].sort((a, b) => {
switch (sortOrder) {
case 'date-desc':
return new Date(b.date).getTime() - new Date(a.date).getTime();
case 'date-asc':
return new Date(a.date).getTime() - new Date(b.date).getTime();
case 'cycle-asc':
return a.cycle.localeCompare(b.cycle);
case 'cycle-desc':
return b.cycle.localeCompare(a.cycle);
case 'size-desc':
return parseDimensions(b.dimensions) - parseDimensions(a.dimensions);
case 'size-asc':
return parseDimensions(a.dimensions) - parseDimensions(b.dimensions);
default:
return 0;
}
});
return sorted;
}, [filters, photos, initialOrder, sortOrder]);
const visiblePhotos = useMemo(() => {
return processedPhotos.slice(0, visibleCount);
}, [processedPhotos, visibleCount]);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight - 500) {
if (visibleCount < processedPhotos.length) {
setVisibleCount(prevCount => prevCount + 8); // Doładuj 8 kolejnych zdjęć
}
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [visibleCount, processedPhotos.length]);
useEffect(() => {
setVisibleCount(12); // Resetuj widoczne zdjęcia przy zmianie filtrów
}, [filters, sortOrder]);
const handleNavigateLightbox = (direction: number) => {
if (selectedPhotoIndex === null) return;
let newIndex = selectedPhotoIndex + direction;
// Pętla nawigacji
if (newIndex < 0) newIndex = processedPhotos.length - 1;
if (newIndex >= processedPhotos.length) newIndex = 0;
setSelectedPhotoIndex(newIndex);
};
const findOriginalIndex = (photoId: number) => {
return processedPhotos.findIndex(p => p.id === photoId);
}
return (
{currentPage === 'portfolio' ? (
{
const photoId = visiblePhotos[index].id;
setSelectedPhotoIndex(findOriginalIndex(photoId));
}}
/>
) : (
)}
setSelectedPhotoIndex(null)}
onNavigate={handleNavigateLightbox}
/>
);
};
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render( );
} else {
console.error('Błąd krytyczny: Nie znaleziono elementu #root w dokumencie. Aplikacja nie może zostać uruchomiona.');
}