| | import { useState, useEffect } from 'react'; |
| | import { X, ArrowLeft, Wand2 } from 'lucide-react'; |
| | import Masonry from 'react-masonry-css'; |
| |
|
| | |
| | const getDateFromFilename = (filename) => { |
| | |
| | const dateMatch = filename.match(/chrome-study - (\d{4}-\d{2}-\d{2}T\d{6})/); |
| | if (dateMatch) { |
| | return new Date(dateMatch[1].replace('T', 'T').slice(0, 19)); |
| | } |
| | |
| | |
| | const numMatch = filename.match(/chrome-study[- ]\(?(\d+)/); |
| | if (numMatch) { |
| | return Number.parseInt(numMatch[1], 10); |
| | } |
| | |
| | return 0; |
| | }; |
| |
|
| | const LibraryPage = ({ onBack, onUseAsTemplate }) => { |
| | const [images, setImages] = useState([]); |
| | const [fullscreenImage, setFullscreenImage] = useState(null); |
| | const [isLoading, setIsLoading] = useState(true); |
| | const [hoveredImage, setHoveredImage] = useState(null); |
| | |
| | |
| | const breakpointColumnsObj = { |
| | default: 4, |
| | 1100: 3, |
| | 700: 2, |
| | 500: 1 |
| | }; |
| | |
| | useEffect(() => { |
| | |
| | const fetchImages = async () => { |
| | try { |
| | |
| | |
| | const imageFiles = [ |
| | "chrome-study (17).png", |
| | "chrome-study (19).png", |
| | "chrome-study (27).png", |
| | "chrome-study (43).png", |
| | "chrome-study (47).png", |
| | "chrome-study (48).png", |
| | "chrome-study (55).png", |
| | "chrome-study (56).png", |
| | "chrome-study (58).png", |
| | "chrome-study (62).png", |
| | "chrome-study (64).png", |
| | "chrome-study (72).png", |
| | "chrome-study (76).png", |
| | "chrome-study (77).png", |
| | "chrome-study (78).png", |
| | "chrome-study (79).png", |
| | "chrome-study (81).png", |
| | "chrome-study (83).png", |
| | "chrome-study (84).png", |
| | "chrome-study (86).png", |
| | "chrome-study (87).png", |
| | "chrome-study (92).png", |
| | "chrome-study (94).png", |
| | "chrome-study (95).png", |
| | "chrome-study (98).png", |
| | "chrome-study (99).png", |
| | "chrome-study - 2025-03-29T231111.407.png", |
| | "chrome-study - 2025-03-29T231628.676.png", |
| | "chrome-study - 2025-03-29T231852.687.png", |
| | "chrome-study - 2025-03-29T232157.263.png", |
| | "chrome-study - 2025-03-29T232601.690.png", |
| | "chrome-study - 2025-03-29T235802.886.png", |
| | "chrome-study - 2025-03-30T000256.137.png", |
| | "chrome-study - 2025-03-30T000847.148.png", |
| | "chrome-study - 2025-03-30T001126.978.png", |
| | "chrome-study - 2025-03-30T001518.410.png", |
| | "chrome-study - 2025-03-30T002129.834.png", |
| | "chrome-study - 2025-03-30T002928.187.png", |
| | "chrome-study - 2025-03-30T003503.053.png", |
| | "chrome-study - 2025-03-30T003713.255.png", |
| | "chrome-study - 2025-03-30T003942.300.png", |
| | "chrome-study - 2025-03-30T011127.402.png", |
| | "chrome-study-11.png", |
| | "chrome-study-6.png" |
| | ]; |
| | |
| | |
| | |
| | const sortedImages = imageFiles.sort((a, b) => { |
| | const dateA = getDateFromFilename(a); |
| | const dateB = getDateFromFilename(b); |
| | return dateB - dateA; |
| | }); |
| | |
| | setImages(sortedImages); |
| | setIsLoading(false); |
| | } catch (error) { |
| | console.error("Error fetching library images:", error); |
| | setIsLoading(false); |
| | } |
| | }; |
| | |
| | fetchImages(); |
| | }, []); |
| | |
| | const handleImageClick = (imagePath) => { |
| | setFullscreenImage(imagePath); |
| | }; |
| | |
| | const handleKeyDown = (event, imagePath) => { |
| | if (event.key === 'Enter' || event.key === ' ') { |
| | setFullscreenImage(imagePath); |
| | } |
| | }; |
| | |
| | return ( |
| | <div className="flex min-h-screen flex-col items-center justify-start bg-gray-50 p-2 md:p-4 overflow-y-auto"> |
| | <div className="w-full max-w-[1800px] mx-auto pb-32"> |
| | {/* Fixed header section */} |
| | <div className="fixed top-0 left-0 right-0 bg-gray-50 z-10 px-2 md:px-4 pt-2 md:pt-4 pb-3"> |
| | <div className="w-full max-w-[1800px] mx-auto"> |
| | {/* Simple Header */} |
| | <div className="flex items-center justify-between mt-4 mx-1"> |
| | <button |
| | type="button" |
| | onClick={onBack} |
| | className="flex items-center text-gray-800 hover:text-gray-600 hover:cursor-pointer transition-colors text-lg font-medium" |
| | aria-label="Go back to gallery" |
| | > |
| | <ArrowLeft className="w-5 h-5 mr-1" /> |
| | Gallery |
| | </button> |
| | |
| | <div> |
| | <span className="inline-flex items-center rounded-full border px-5 py-2 border-gray-200 bg-gray-100 text-base text-gray-500"> |
| | Submit by replying to this{" "}<a href="https://x.com/dev_valladares/status/1799888888888888888" target="_blank" rel="noreferrer" className="underline ml-1">tweet</a> |
| | </span> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | |
| | {/* Content with padding to account for fixed header */} |
| | <div className="space-y-4 mt-28"> |
| | {/* Loading state */} |
| | {isLoading && ( |
| | <div className="flex items-center justify-center h-64"> |
| | <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-400" /> |
| | </div> |
| | )} |
| | |
| | {/* Masonry grid of images */} |
| | <Masonry |
| | breakpointCols={breakpointColumnsObj} |
| | className="flex w-auto -ml-4" |
| | columnClassName="pl-4 bg-clip-padding" |
| | > |
| | {images.map((image, index) => ( |
| | <button |
| | key={image} |
| | className="mb-4 cursor-pointer transform transition-transform hover:scale-[1.01] text-left block w-full p-0 border-0 bg-transparent" |
| | onClick={() => handleImageClick(`/library/${image}`)} |
| | onMouseEnter={() => setHoveredImage(image)} |
| | onMouseLeave={() => setHoveredImage(null)} |
| | type="button" |
| | aria-label={`Screenshot ${index + 1}`} |
| | > |
| | <div className="relative rounded-xl overflow-hidden border border-gray-200 shadow-sm bg-white"> |
| | <img |
| | src={`/library/${image}`} |
| | alt={`Screenshot ${index + 1}`} |
| | className="w-full h-auto object-cover" |
| | loading="lazy" |
| | /> |
| | |
| | {/* Use as template button */} |
| | {hoveredImage === image && onUseAsTemplate && ( |
| | <div className="absolute bottom-2 right-2 z-10"> |
| | <button |
| | onClick={(e) => { |
| | e.stopPropagation(); // Prevent opening the fullscreen view |
| | onUseAsTemplate(`/library/${image}`); |
| | }} |
| | className="flex items-center gap-1 bg-white/90 hover:bg-white text-gray-800 px-3 py-1.5 rounded-full text-xs font-medium shadow-md transition-all" |
| | type="button" |
| | > |
| | <Wand2 className="w-3 h-3" /> |
| | Use as template |
| | </button> |
| | </div> |
| | )} |
| | </div> |
| | </button> |
| | ))} |
| | </Masonry> |
| | |
| | {/* No images state */} |
| | {!isLoading && images.length === 0 && ( |
| | <div className="flex flex-col items-center justify-center h-64 text-gray-500"> |
| | <p className="text-lg mb-2">No images in library</p> |
| | <p className="text-sm">Create some images to see them here</p> |
| | </div> |
| | )} |
| | </div> |
| | </div> |
| | |
| | {/* Fullscreen image modal */} |
| | {fullscreenImage && ( |
| | <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4"> |
| | <div className="relative max-w-4xl w-full max-h-[90vh]"> |
| | <button |
| | type="button" |
| | onClick={() => setFullscreenImage(null)} |
| | className="absolute -top-12 right-0 p-2 text-white hover:text-gray-300 transition-colors" |
| | aria-label="Close fullscreen view" |
| | > |
| | <X className="w-6 h-6" /> |
| | </button> |
| | <img |
| | src={fullscreenImage} |
| | alt="Fullscreen view" |
| | className="w-full h-auto object-contain max-h-[90vh] rounded-lg" |
| | /> |
| | </div> |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|
| | export default LibraryPage; |