Skip to content

Commit ec29951

Browse files
committed
feat: add new fullscreen image preview on blog post pages
1 parent 6d32623 commit ec29951

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed

next.config.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ const { withPlaiceholder } = require('@plaiceholder/next')
1919
const nextConfig = {
2020
swcMinify: true,
2121
reactStrictMode: true,
22+
images: {
23+
remotePatterns: [
24+
{
25+
protocol: 'https',
26+
hostname: 'dev-to-uploads.s3.amazonaws.com',
27+
port: '',
28+
pathname: '/uploads/**',
29+
},
30+
],
31+
},
2232

2333
async redirects() {
2434
return [

src/components/MDXImage.tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { useState, useEffect } from 'react'
2+
import { XMarkIcon, EyeIcon } from '@heroicons/react/24/outline'
3+
import Image from 'next/image'
4+
import { posthog } from 'posthog-js'
5+
6+
interface MDXImageProps {
7+
src: string
8+
alt: string
9+
width?: number
10+
height?: number
11+
className?: string
12+
title?: string
13+
}
14+
15+
export const MDXImage = ({
16+
src,
17+
alt,
18+
width,
19+
height,
20+
className = '',
21+
title,
22+
}: MDXImageProps) => {
23+
const [isFullscreen, setIsFullscreen] = useState(false)
24+
25+
const isExternalImage =
26+
src.startsWith('http://') || src.startsWith('https://')
27+
const isLocalImage = src.startsWith('/static/images/')
28+
29+
useEffect(() => {
30+
const handleEscape = (e: KeyboardEvent) => {
31+
if (e.key === 'Escape') {
32+
setIsFullscreen(false)
33+
}
34+
}
35+
36+
if (isFullscreen) {
37+
document.addEventListener('keydown', handleEscape)
38+
document.body.style.overflow = 'hidden'
39+
}
40+
41+
return () => {
42+
document.removeEventListener('keydown', handleEscape)
43+
document.body.style.overflow = 'unset'
44+
}
45+
}, [isFullscreen])
46+
47+
const openFullscreen = () => {
48+
setIsFullscreen(true)
49+
posthog.capture('mdx-image-fullscreen-open', {
50+
src,
51+
alt,
52+
isExternal: isExternalImage,
53+
isLocal: isLocalImage,
54+
})
55+
}
56+
57+
const closeFullscreen = () => {
58+
setIsFullscreen(false)
59+
posthog.capture('mdx-image-fullscreen-close', {
60+
src,
61+
alt,
62+
isExternal: isExternalImage,
63+
isLocal: isLocalImage,
64+
})
65+
}
66+
67+
return (
68+
<>
69+
<div
70+
className={`relative cursor-pointer group transition-transform duration-300 hover:scale-[1.02] rounded-lg overflow-hidden ${className}`}
71+
onClick={openFullscreen}
72+
>
73+
{isExternalImage ? (
74+
<img
75+
src={src}
76+
alt={alt}
77+
width={width || 800}
78+
height={height || 600}
79+
className="w-full h-auto object-cover transition-transform duration-300 group-hover:scale-105"
80+
loading="lazy"
81+
/>
82+
) : (
83+
<Image
84+
src={src}
85+
alt={alt}
86+
width={width || 800}
87+
height={height || 600}
88+
className="w-full h-auto object-cover transition-transform duration-300 group-hover:scale-105"
89+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 70vw"
90+
/>
91+
)}
92+
93+
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
94+
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-full p-3">
95+
<EyeIcon className="h-6 w-6 text-gray-900 dark:text-white" />
96+
</div>
97+
</div>
98+
</div>
99+
100+
{isFullscreen && (
101+
<div
102+
className="fixed inset-0 z-50 bg-black/95 backdrop-blur-sm flex items-center justify-center p-4"
103+
onClick={closeFullscreen}
104+
>
105+
<div className="relative max-w-7xl max-h-full w-full h-full flex items-center justify-center">
106+
<button
107+
onClick={closeFullscreen}
108+
className="absolute top-4 right-4 z-10 p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors duration-200 backdrop-blur-sm"
109+
aria-label="Fechar imagem em tela cheia"
110+
>
111+
<XMarkIcon className="h-6 w-6" />
112+
</button>
113+
114+
<div
115+
className="relative w-full h-full max-w-none max-h-none"
116+
onClick={(e) => e.stopPropagation()}
117+
>
118+
{isExternalImage ? (
119+
<img
120+
src={src}
121+
alt={alt}
122+
className="w-full h-full object-contain"
123+
/>
124+
) : (
125+
<Image
126+
src={src}
127+
alt={alt}
128+
fill
129+
className="object-contain"
130+
sizes="100vw"
131+
quality={100}
132+
priority
133+
/>
134+
)}
135+
</div>
136+
137+
<div className="absolute bottom-4 left-4 right-4 text-center">
138+
<div className="inline-block bg-black/50 backdrop-blur-sm rounded-lg px-4 py-2 text-white">
139+
<p className="text-sm font-medium">{title || alt}</p>
140+
<p className="text-xs text-gray-300 mt-1">
141+
Pressione ESC ou clique fora da imagem para fechar
142+
</p>
143+
</div>
144+
</div>
145+
</div>
146+
</div>
147+
)}
148+
</>
149+
)
150+
}

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './Header'
77
export * from './Icons'
88
export * from './IconsGroup'
99
export * from './Layout'
10+
export * from './MDXImage'

src/pages/blog/[slug].tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
2-
import { Layout, CodeBlock, Pre } from '@/components'
2+
import { Layout, CodeBlock, Pre, MDXImage } from '@/components'
33
import { POSTS_PATH } from '@/constants'
44
import fs from 'fs'
55
import { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
@@ -16,6 +16,8 @@ import { SITE_URL } from '@/utils/constants'
1616
const components = {
1717
pre: (props: any) => <Pre {...props} />,
1818
code: (props: any) => <CodeBlock {...props} />,
19+
img: (props: any) => <MDXImage {...props} />,
20+
Image: (props: any) => <MDXImage {...props} />,
1921
}
2022

2123
const PostPage = ({

0 commit comments

Comments
 (0)