swadhinbiswas Initial release v0.1.0 · 1/16/2026
ed7a4e4
150 lines · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { X, Grid, Download, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'
import type { Post } from '../../types/api'
import { getImageUrl, getThumbnailUrl } from '../../utils/apiClient'

interface ImageGalleryProps {
    posts: Post[]
    board: string
    isOpen: boolean
    onClose: () => void
    onImageClick?: (index: number) => void // Opens lightbox
}

interface GalleryImage {
    post: Post
    url: string
    thumbnail: string
    filename: string
}

export default function ImageGallery({ posts, board, isOpen, onClose, onImageClick }: ImageGalleryProps) {
    const [selectedIndex, setSelectedIndex] = useState<number | null>(null)

    if (!isOpen) return null

    // Extract all images from posts
    const images: GalleryImage[] = posts
        .filter(p => p.tim && p.ext)
        .map(p => ({
            post: p,
            url: getImageUrl(board, p.tim!, p.ext!),
            thumbnail: getThumbnailUrl(board, p.tim!),
            filename: `${p.filename || p.tim}${p.ext}`
        }))

    const handleDownloadAll = async () => {
        // Download each image
        for (const img of images) {
            const link = document.createElement('a')
            link.href = img.url
            link.download = img.filename
            link.target = '_blank'
            document.body.appendChild(link)
            link.click()
            document.body.removeChild(link)
            await new Promise(resolve => setTimeout(resolve, 200)) // Rate limit
        }
    }

    return (
        <AnimatePresence>
            <div className="fixed inset-0 z-50 flex flex-col">
                {/* Backdrop */}
                <motion.div
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    exit={{ opacity: 0 }}
                    className="absolute inset-0 bg-black/90 backdrop-blur-sm"
                    onClick={onClose}
                />

                {/* Content */}
                <motion.div
                    initial={{ opacity: 0, y: 20 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: 20 }}
                    className="relative flex-1 flex flex-col m-4 bg-dark-surface border border-dark-border rounded-xl overflow-hidden"
                    onClick={e => e.stopPropagation()}
                >
                    {/* Header */}
                    <div className="flex items-center justify-between p-4 border-b border-dark-border">
                        <div className="flex items-center gap-3">
                            <Grid className="w-5 h-5 text-purple-400" />
                            <h2 className="text-lg font-semibold">Image Gallery</h2>
                            <span className="text-sm text-gray-400">
                                {images.length} images
                            </span>
                        </div>
                        <div className="flex items-center gap-2">
                            <button
                                onClick={handleDownloadAll}
                                className="btn btn-secondary flex items-center gap-2 text-sm"
                            >
                                <Download className="w-4 h-4" />
                                Download All
                            </button>
                            <button
                                onClick={onClose}
                                className="btn-ghost p-2 hover:bg-dark-hover rounded-lg"
                            >
                                <X className="w-5 h-5" />
                            </button>
                        </div>
                    </div>

                    {/* Gallery Grid */}
                    <div className="flex-1 overflow-y-auto p-4">
                        <div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-3">
                            {images.map((img, index) => (
                                <motion.div
                                    key={img.post.no}
                                    initial={{ opacity: 0, scale: 0.8 }}
                                    animate={{ opacity: 1, scale: 1 }}
                                    transition={{ delay: index * 0.02 }}
                                    className="group relative aspect-square bg-dark-elevated rounded-lg overflow-hidden cursor-pointer hover:ring-2 hover:ring-purple-500 transition-all"
                                    onClick={() => onImageClick?.(index)}
                                >
                                    <img
                                        src={img.thumbnail}
                                        alt={img.filename}
                                        loading="lazy"
                                        className="w-full h-full object-cover"
                                    />

                                    {/* Hover overlay */}
                                    <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
                                        <div className="absolute bottom-0 left-0 right-0 p-2">
                                            <p className="text-xs text-white truncate">
                                                {img.filename}
                                            </p>
                                            <p className="text-xs text-gray-400">
                                                No.{img.post.no}
                                            </p>
                                        </div>
                                    </div>

                                    {/* Video indicator */}
                                    {img.post.ext === '.webm' && (
                                        <div className="absolute top-2 right-2 px-1.5 py-0.5 bg-purple-600 rounded text-xs font-medium">
                                            VIDEO
                                        </div>
                                    )}
                                </motion.div>
                            ))}
                        </div>

                        {images.length === 0 && (
                            <div className="flex flex-col items-center justify-center py-12 text-gray-500">
                                <Grid className="w-12 h-12 mb-4 opacity-50" />
                                <p>No images in this thread</p>
                            </div>
                        )}
                    </div>
                </motion.div>
            </div>
        </AnimatePresence>
    )
}