ギャラリー/

ライトボックスギャラリー

Preview

クリックで拡大表示できるモーダル付きの洗練されたギャラリーセクション。前後ナビゲーションで作品を順に閲覧可能

Source Code
tsx
282 lines
1"use client";
2
3import { useState } from "react";
4
5const galleryItems = [
6 {
7 id: 1,
8 title: "幾何学パターン",
9 category: "グラフィック",
10 color: "from-foreground/5 to-foreground/10",
11 accent: "bg-foreground/20",
12 },
13 {
14 id: 2,
15 title: "都市の輪郭",
16 category: "建築",
17 color: "from-foreground/8 to-foreground/3",
18 accent: "bg-foreground/15",
19 },
20 {
21 id: 3,
22 title: "静寂の水面",
23 category: "自然",
24 color: "from-foreground/3 to-foreground/12",
25 accent: "bg-foreground/25",
26 },
27 {
28 id: 4,
29 title: "構造と秩序",
30 category: "建築",
31 color: "from-foreground/10 to-foreground/5",
32 accent: "bg-foreground/10",
33 },
34 {
35 id: 5,
36 title: "光と影の対話",
37 category: "グラフィック",
38 color: "from-foreground/6 to-foreground/14",
39 accent: "bg-foreground/18",
40 },
41 {
42 id: 6,
43 title: "風の記憶",
44 category: "自然",
45 color: "from-foreground/12 to-foreground/4",
46 accent: "bg-foreground/22",
47 },
48];
49
50export function GalleryLightbox001() {
51 const [selectedId, setSelectedId] = useState<number | null>(null);
52
53 const selectedItem = galleryItems.find((item) => item.id === selectedId);
54
55 return (
56 <section className="bg-background py-28 border-t border-border">
57 <div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
58 {/* ヘッダー */}
59 <div className="text-center">
60 <div className="mx-auto flex items-center justify-center gap-4">
61 <div className="h-px w-8 bg-border/40" />
62 <div className="h-1.5 w-1.5 rounded-full bg-foreground/20" />
63 <div className="h-px w-8 bg-border/40" />
64 </div>
65
66 <p className="mt-8 text-[10px] uppercase tracking-[0.3em] text-muted-foreground">
67 Gallery
68 </p>
69 <h2 className="mt-3 text-2xl font-medium tracking-wide text-foreground sm:text-3xl">
70 作品コレクション
71 </h2>
72 <p className="mt-4 text-sm font-light leading-relaxed text-muted-foreground">
73 厳選されたクリエイティブワークの数々をご覧ください
74 </p>
75 </div>
76
77 {/* グリッド */}
78 <div className="mt-16 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
79 {galleryItems.map((item) => (
80 <button
81 key={item.id}
82 onClick={() => setSelectedId(item.id)}
83 className="group relative aspect-[4/3] overflow-hidden rounded-lg border border-border text-left transition-all duration-300 hover:border-foreground/20"
84 >
85 {/* プレースホルダー背景 */}
86 <div
87 className={`absolute inset-0 bg-gradient-to-br ${item.color}`}
88 />
89
90 {/* 装飾パターン */}
91 <div className="absolute inset-0 flex items-center justify-center">
92 <div
93 className={`h-16 w-16 rounded-full ${item.accent} transition-transform duration-500 group-hover:scale-110`}
94 />
95 </div>
96 <div className="absolute left-6 top-6">
97 <div className="h-px w-6 bg-foreground/10 transition-all duration-300 group-hover:w-10 group-hover:bg-foreground/20" />
98 </div>
99 <div className="absolute bottom-6 right-6">
100 <div className="h-px w-6 bg-foreground/10 transition-all duration-300 group-hover:w-10 group-hover:bg-foreground/20" />
101 </div>
102
103 {/* オーバーレイ情報 */}
104 <div className="absolute inset-x-0 bottom-0 p-6">
105 <p className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
106 {item.category}
107 </p>
108 <h3 className="mt-1.5 text-sm font-medium tracking-wide text-foreground">
109 {item.title}
110 </h3>
111 </div>
112
113 {/* 拡大アイコン */}
114 <div className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full border border-border/60 bg-background/80 opacity-0 backdrop-blur-sm transition-all duration-300 group-hover:opacity-100">
115 <svg
116 className="h-3.5 w-3.5 text-foreground"
117 fill="none"
118 stroke="currentColor"
119 viewBox="0 0 24 24"
120 >
121 <path
122 strokeLinecap="round"
123 strokeLinejoin="round"
124 strokeWidth={1.5}
125 d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
126 />
127 </svg>
128 </div>
129 </button>
130 ))}
131 </div>
132
133 {/* 件数表示 */}
134 <div className="mt-10 flex items-center justify-center gap-6">
135 <p className="text-[10px] tracking-[0.15em] text-muted-foreground/50">
136 6 作品を表示中
137 </p>
138 <div className="h-3 w-px bg-border/40" />
139 <p className="text-[10px] tracking-[0.15em] text-muted-foreground/50">
140 全コレクション
141 </p>
142 </div>
143 </div>
144
145 {/* ライトボックスモーダル */}
146 {selectedItem && (
147 <div
148 className="fixed inset-0 z-50 flex items-center justify-center bg-background/90 backdrop-blur-sm"
149 onClick={() => setSelectedId(null)}
150 >
151 <div
152 className="relative mx-4 w-full max-w-3xl"
153 onClick={(e) => e.stopPropagation()}
154 >
155 {/* 閉じるボタン */}
156 <button
157 onClick={() => setSelectedId(null)}
158 className="absolute -top-12 right-0 flex h-8 w-8 items-center justify-center rounded-full border border-border text-muted-foreground transition-colors duration-200 hover:text-foreground"
159 >
160 <svg
161 className="h-4 w-4"
162 fill="none"
163 stroke="currentColor"
164 viewBox="0 0 24 24"
165 >
166 <path
167 strokeLinecap="round"
168 strokeLinejoin="round"
169 strokeWidth={1.5}
170 d="M6 18L18 6M6 6l12 12"
171 />
172 </svg>
173 </button>
174
175 {/* メインコンテンツ */}
176 <div className="overflow-hidden rounded-lg border border-border">
177 <div
178 className={`relative aspect-[16/10] bg-gradient-to-br ${selectedItem.color}`}
179 >
180 <div className="absolute inset-0 flex items-center justify-center">
181 <div
182 className={`h-32 w-32 rounded-full ${selectedItem.accent}`}
183 />
184 </div>
185
186 {/* コーナー装飾 */}
187 <div className="absolute left-8 top-8">
188 <div className="h-1.5 w-1.5 rounded-full bg-foreground/20" />
189 </div>
190 <div className="absolute right-8 top-8">
191 <div className="h-1.5 w-1.5 rounded-full bg-foreground/20" />
192 </div>
193 <div className="absolute bottom-8 left-8">
194 <div className="h-1.5 w-1.5 rounded-full bg-foreground/20" />
195 </div>
196 <div className="absolute bottom-8 right-8">
197 <div className="h-1.5 w-1.5 rounded-full bg-foreground/20" />
198 </div>
199 </div>
200
201 {/* 情報パネル */}
202 <div className="border-t border-border bg-background p-6">
203 <div className="flex items-start justify-between">
204 <div>
205 <p className="text-[10px] uppercase tracking-[0.3em] text-muted-foreground">
206 {selectedItem.category}
207 </p>
208 <h3 className="mt-2 text-lg font-medium tracking-wide text-foreground">
209 {selectedItem.title}
210 </h3>
211 </div>
212 <div className="flex items-center gap-1.5 text-[10px] tracking-[0.15em] text-muted-foreground/50">
213 <span>{selectedItem.id}</span>
214 <span>/</span>
215 <span>{galleryItems.length}</span>
216 </div>
217 </div>
218 </div>
219 </div>
220
221 {/* ナビゲーション */}
222 <div className="mt-4 flex items-center justify-between">
223 <button
224 onClick={() => {
225 const currentIndex = galleryItems.findIndex(
226 (item) => item.id === selectedId
227 );
228 const prevIndex =
229 (currentIndex - 1 + galleryItems.length) %
230 galleryItems.length;
231 setSelectedId(galleryItems[prevIndex].id);
232 }}
233 className="flex items-center gap-2 text-[10px] uppercase tracking-[0.2em] text-muted-foreground transition-colors duration-200 hover:text-foreground"
234 >
235 <svg
236 className="h-3.5 w-3.5"
237 fill="none"
238 stroke="currentColor"
239 viewBox="0 0 24 24"
240 >
241 <path
242 strokeLinecap="round"
243 strokeLinejoin="round"
244 strokeWidth={1.5}
245 d="M7 16l-4-4m0 0l4-4m-4 4h18"
246 />
247 </svg>
248 前へ
249 </button>
250 <button
251 onClick={() => {
252 const currentIndex = galleryItems.findIndex(
253 (item) => item.id === selectedId
254 );
255 const nextIndex =
256 (currentIndex + 1) % galleryItems.length;
257 setSelectedId(galleryItems[nextIndex].id);
258 }}
259 className="flex items-center gap-2 text-[10px] uppercase tracking-[0.2em] text-muted-foreground transition-colors duration-200 hover:text-foreground"
260 >
261 次へ
262 <svg
263 className="h-3.5 w-3.5"
264 fill="none"
265 stroke="currentColor"
266 viewBox="0 0 24 24"
267 >
268 <path
269 strokeLinecap="round"
270 strokeLinejoin="round"
271 strokeWidth={1.5}
272 d="M17 8l4 4m0 0l-4 4m4-4H3"
273 />
274 </svg>
275 </button>
276 </div>
277 </div>
278 </div>
279 )}
280 </section>
281 );
282}