Skip to content

Commit f26cd34

Browse files
committed
feat(terracotta): add TerracottaImageModal component
1 parent 50f9b18 commit f26cd34

1 file changed

Lines changed: 114 additions & 0 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import { X, Scan } from '@phosphor-icons/react';
4+
import { motion, AnimatePresence } from 'framer-motion';
5+
6+
const TerracottaImageModal = ({ src, alt, onClose }) => {
7+
const [dimensions, setDimensions] = useState(null);
8+
9+
useEffect(() => {
10+
if (src) document.body.style.overflow = 'hidden';
11+
else document.body.style.overflow = '';
12+
13+
const handleKeyDown = (e) => {
14+
if (e.key === 'Escape') onClose();
15+
};
16+
17+
if (src) window.addEventListener('keydown', handleKeyDown);
18+
19+
return () => {
20+
document.body.style.overflow = '';
21+
window.removeEventListener('keydown', handleKeyDown);
22+
};
23+
}, [src, onClose]);
24+
25+
const handleImageLoad = (e) => {
26+
setDimensions({
27+
width: e.target.naturalWidth,
28+
height: e.target.naturalHeight,
29+
});
30+
};
31+
32+
const showAlt =
33+
alt &&
34+
!['Project Detail', 'Enlarged Content', 'Intel Imagery', 'Full size image'].includes(alt);
35+
36+
return createPortal(
37+
<AnimatePresence>
38+
{src && (
39+
<motion.div
40+
className="fixed inset-0 z-[1000] flex items-center justify-center bg-[#1A1613]/85 backdrop-blur-xl p-2 md:p-4"
41+
onClick={onClose}
42+
initial={{ opacity: 0 }}
43+
animate={{ opacity: 1 }}
44+
exit={{ opacity: 0 }}
45+
transition={{ duration: 0.2 }}
46+
>
47+
<div
48+
className="absolute inset-0 pointer-events-none opacity-10"
49+
style={{
50+
backgroundImage: 'radial-gradient(circle, #F3ECE0 1px, transparent 1px)',
51+
backgroundSize: '30px 30px',
52+
}}
53+
/>
54+
55+
<motion.div
56+
className="relative w-full h-full max-w-[85vw] max-h-[85vh] flex flex-col"
57+
onClick={(e) => e.stopPropagation()}
58+
initial={{ scale: 0.98, opacity: 0 }}
59+
animate={{ scale: 1, opacity: 1 }}
60+
exit={{ scale: 0.98, opacity: 0 }}
61+
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
62+
>
63+
<div className="flex items-center justify-between bg-[#F3ECE0]/95 border border-[#1A161320] border-b-0 p-2 md:p-3 backdrop-blur-md rounded-t-sm">
64+
<div className="flex items-center gap-3">
65+
<Scan className="text-[#C96442] animate-pulse" size={16} />
66+
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-[#2E2620]/70">
67+
PLUMB.IMG_VIEWER // CALM_MODE
68+
</span>
69+
</div>
70+
<div className="flex items-center gap-2">
71+
<span className="hidden md:inline font-mono text-[9px] text-[#2E2620]/50 uppercase tracking-widest mr-2">
72+
Press ESC to exit
73+
</span>
74+
<button
75+
onClick={onClose}
76+
className="group flex items-center gap-2 px-4 py-1.5 bg-[#E8DECE] hover:bg-[#C96442] text-[#2E2620] hover:text-[#F3ECE0] border border-[#1A161320] rounded-sm transition-all"
77+
>
78+
<X size={16} weight="bold" />
79+
</button>
80+
</div>
81+
</div>
82+
83+
<div className="relative flex-1 min-h-0 bg-[#F3ECE0]/60 backdrop-blur-sm rounded-b-sm overflow-hidden flex items-center justify-center">
84+
<div className="absolute top-0 left-0 w-6 h-6 border-l-2 border-t-2 border-[#C96442]/50 z-10" />
85+
<div className="absolute top-0 right-0 w-6 h-6 border-r-2 border-t-2 border-[#C96442]/50 z-10" />
86+
<div className="absolute bottom-0 left-0 w-6 h-6 border-l-2 border-b-2 border-[#C96442]/50 z-10" />
87+
<div className="absolute bottom-0 right-0 w-6 h-6 border-r-2 border-b-2 border-[#C96442]/50 z-10" />
88+
89+
<img
90+
src={src}
91+
alt={alt}
92+
onLoad={handleImageLoad}
93+
style={{ maxWidth: '100%', maxHeight: '100%' }}
94+
className="w-auto h-auto object-contain block shadow-[0_0_50px_rgba(26,22,19,0.4)] select-none"
95+
/>
96+
</div>
97+
98+
<div className="mt-3 flex justify-between items-center px-2">
99+
<span className="font-mono text-sm uppercase tracking-widest text-[#F3ECE0] font-bold truncate max-w-[50vw]">
100+
{showAlt ? alt : 'RAW_STREAM'}
101+
</span>
102+
<span className="font-mono text-sm uppercase tracking-widest text-[#F3ECE0] font-bold text-right">
103+
{dimensions ? `${dimensions.width} x ${dimensions.height} PX` : 'CALCULATING...'}
104+
</span>
105+
</div>
106+
</motion.div>
107+
</motion.div>
108+
)}
109+
</AnimatePresence>,
110+
document.body,
111+
);
112+
};
113+
114+
export default TerracottaImageModal;

0 commit comments

Comments
 (0)