// 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) => (
{`Slajd
))}
); }; type HeaderProps = { currentPage: PageName; setCurrentPage: React.Dispatch>; }; const Header = ({ currentPage, setCurrentPage }: HeaderProps) => (

setCurrentPage('portfolio')}>MAREK PRZYBYŁ

); type SortControlsProps = { sortOrder: SortOrder; setSortOrder: React.Dispatch>; }; const SortControls = ({ sortOrder, setSortOrder }: SortControlsProps) => (
); 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}
{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}
); }; 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)); }} />
) : ( )}