Skip to content

Commit bb7baa7

Browse files
committed
Add new gemini docs
1 parent 3f092b0 commit bb7baa7

49 files changed

Lines changed: 1262 additions & 225 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/(home)/page.tsx

Lines changed: 208 additions & 64 deletions
Large diffs are not rendered by default.
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
'use client'
2+
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4+
5+
interface ScreenshotsGalleryViewProps {
6+
images: Record<string, string>
7+
className?: string
8+
alt?: string
9+
}
10+
11+
export function ScreenshotsGalleryView({ images, className, alt }: ScreenshotsGalleryViewProps) {
12+
const entries = useMemo(() => Object.entries(images), [images])
13+
const [activeIndex, setActiveIndex] = useState(0)
14+
const [lightboxOpen, setLightboxOpen] = useState(false)
15+
const thumbnailRefs = useRef<Array<HTMLButtonElement | null>>([])
16+
17+
const count = entries.length
18+
const current = entries[activeIndex] ?? entries[0]
19+
20+
useEffect(() => {
21+
if (activeIndex >= count) setActiveIndex(0)
22+
}, [activeIndex, count])
23+
24+
useEffect(() => {
25+
thumbnailRefs.current[activeIndex]?.scrollIntoView({
26+
behavior: 'smooth',
27+
block: 'nearest',
28+
inline: 'center',
29+
})
30+
}, [activeIndex])
31+
32+
const previous = useCallback(() => {
33+
if (count > 1) setActiveIndex(index => (index - 1 + count) % count)
34+
}, [count])
35+
36+
const next = useCallback(() => {
37+
if (count > 1) setActiveIndex(index => (index + 1) % count)
38+
}, [count])
39+
40+
useEffect(() => {
41+
if (!lightboxOpen) return
42+
43+
const onKeyDown = (event: KeyboardEvent) => {
44+
if (event.key === 'Escape') setLightboxOpen(false)
45+
if (event.key === 'ArrowLeft') {
46+
event.preventDefault()
47+
previous()
48+
}
49+
if (event.key === 'ArrowRight') {
50+
event.preventDefault()
51+
next()
52+
}
53+
}
54+
55+
const previousOverflow = document.body.style.overflow
56+
document.body.style.overflow = 'hidden'
57+
document.addEventListener('keydown', onKeyDown)
58+
return () => {
59+
document.body.style.overflow = previousOverflow
60+
document.removeEventListener('keydown', onKeyDown)
61+
}
62+
}, [lightboxOpen, next, previous])
63+
64+
const onGalleryKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
65+
if (lightboxOpen) return
66+
if (event.key === 'ArrowLeft') {
67+
event.preventDefault()
68+
previous()
69+
}
70+
if (event.key === 'ArrowRight') {
71+
event.preventDefault()
72+
next()
73+
}
74+
}
75+
76+
if (!current) return null
77+
78+
const [title, src] = current
79+
const imageAlt = alt ? `${alt}${title}` : title
80+
81+
return (
82+
<div
83+
className={className ?? 'not-prose my-8'}
84+
onKeyDown={onGalleryKeyDown}
85+
aria-label={`${alt || 'Screenshot'} gallery`}
86+
>
87+
<div className="group relative overflow-hidden rounded-xl border border-gray-200 bg-gray-50 shadow-sm dark:border-gray-700 dark:bg-gray-900">
88+
<button
89+
type="button"
90+
onClick={() => setLightboxOpen(true)}
91+
className="block w-full cursor-zoom-in focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-inset"
92+
aria-label={`Open ${title} in full size`}
93+
>
94+
<img
95+
src={src}
96+
alt={imageAlt}
97+
className="aspect-video w-full object-contain"
98+
loading="lazy"
99+
decoding="async"
100+
/>
101+
</button>
102+
103+
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between gap-4 bg-gradient-to-t from-black/75 via-black/20 to-transparent px-4 pb-3 pt-12 text-white">
104+
<span className="min-w-0 truncate text-sm font-medium sm:text-base">{title}</span>
105+
<span className="shrink-0 rounded-full bg-black/45 px-2 py-0.5 text-xs tabular-nums">
106+
{activeIndex + 1} / {count}
107+
</span>
108+
</div>
109+
110+
{count > 1 && (
111+
<>
112+
<button
113+
type="button"
114+
onClick={previous}
115+
className="absolute left-3 top-1/2 grid size-10 -translate-y-1/2 place-items-center rounded-full bg-black/45 text-white opacity-80 shadow transition hover:bg-black/70 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-white sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
116+
aria-label="Previous screenshot"
117+
title="Previous screenshot"
118+
>
119+
<svg viewBox="0 0 24 24" className="size-6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
120+
<path d="m15 18-6-6 6-6" />
121+
</svg>
122+
</button>
123+
<button
124+
type="button"
125+
onClick={next}
126+
className="absolute right-3 top-1/2 grid size-10 -translate-y-1/2 place-items-center rounded-full bg-black/45 text-white opacity-80 shadow transition hover:bg-black/70 hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-white sm:opacity-0 sm:group-hover:opacity-100 sm:group-focus-within:opacity-100"
127+
aria-label="Next screenshot"
128+
title="Next screenshot"
129+
>
130+
<svg viewBox="0 0 24 24" className="size-6" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
131+
<path d="m9 18 6-6-6-6" />
132+
</svg>
133+
</button>
134+
</>
135+
)}
136+
</div>
137+
138+
{count > 1 && (
139+
<div className="mt-3 flex snap-x gap-2 overflow-x-auto pb-2" role="tablist" aria-label="Choose a screenshot">
140+
{entries.map(([itemTitle, itemSrc], index) => (
141+
<button
142+
key={itemTitle}
143+
ref={element => { thumbnailRefs.current[index] = element }}
144+
type="button"
145+
role="tab"
146+
aria-selected={index === activeIndex}
147+
aria-label={`Show ${itemTitle}`}
148+
onClick={() => setActiveIndex(index)}
149+
className={`w-28 shrink-0 snap-center overflow-hidden rounded-lg border-2 bg-white p-1 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 sm:w-32 ${
150+
index === activeIndex
151+
? 'border-blue-500 shadow-sm dark:border-blue-400'
152+
: 'border-gray-200 hover:bg-gray-50 hover:shadow-sm dark:border-gray-700 dark:bg-gray-900 dark:hover:bg-gray-800'
153+
}`}
154+
>
155+
<img
156+
src={itemSrc}
157+
alt=""
158+
className="aspect-video w-full rounded object-cover"
159+
loading="lazy"
160+
decoding="async"
161+
/>
162+
<span className="mt-1 block truncate px-0.5 text-xs font-medium text-gray-700 dark:text-gray-200">
163+
{itemTitle}
164+
</span>
165+
</button>
166+
))}
167+
</div>
168+
)}
169+
170+
{lightboxOpen && (
171+
<div
172+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 p-4 backdrop-blur-sm"
173+
role="dialog"
174+
aria-modal="true"
175+
aria-label={`${title} screenshot`}
176+
onClick={() => setLightboxOpen(false)}
177+
>
178+
<button
179+
type="button"
180+
onClick={() => setLightboxOpen(false)}
181+
className="absolute right-4 top-4 z-20 grid size-10 place-items-center rounded-full text-white transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
182+
aria-label="Close screenshot"
183+
>
184+
<svg viewBox="0 0 24 24" className="size-7" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
185+
<path d="M6 6l12 12M18 6 6 18" />
186+
</svg>
187+
</button>
188+
189+
<div className="absolute left-4 top-4 z-20 rounded-lg bg-black/55 px-3 py-2 text-sm font-medium text-white">
190+
{title} <span className="ml-1 text-white/65">{activeIndex + 1} / {count}</span>
191+
</div>
192+
193+
<img
194+
src={src}
195+
alt={imageAlt}
196+
className="max-h-[92vh] max-w-full rounded-lg object-contain shadow-2xl"
197+
onClick={event => event.stopPropagation()}
198+
/>
199+
200+
{count > 1 && (
201+
<>
202+
<button
203+
type="button"
204+
onClick={event => { event.stopPropagation(); previous() }}
205+
className="absolute left-3 top-1/2 grid size-12 -translate-y-1/2 place-items-center rounded-full text-white transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
206+
aria-label="Previous screenshot"
207+
>
208+
<svg viewBox="0 0 24 24" className="size-8" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
209+
<path d="m15 18-6-6 6-6" />
210+
</svg>
211+
</button>
212+
<button
213+
type="button"
214+
onClick={event => { event.stopPropagation(); next() }}
215+
className="absolute right-3 top-1/2 grid size-12 -translate-y-1/2 place-items-center rounded-full text-white transition hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
216+
aria-label="Next screenshot"
217+
>
218+
<svg viewBox="0 0 24 24" className="size-8" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
219+
<path d="m9 18 6-6-6-6" />
220+
</svg>
221+
</button>
222+
</>
223+
)}
224+
</div>
225+
)}
226+
</div>
227+
)
228+
}

0 commit comments

Comments
 (0)