Skip to content

Commit 65a317a

Browse files
committed
feat: image modals
1 parent 0eb3b19 commit 65a317a

File tree

4 files changed

+309
-132
lines changed

4 files changed

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

src/components/ImageModal.jsx

Lines changed: 11 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,16 @@
1-
import React, { useEffect, useState } from 'react';
2-
import { X, Scan } from '@phosphor-icons/react';
3-
import { motion, AnimatePresence } from 'framer-motion';
1+
import React from 'react';
2+
import { useVisualSettings } from '../context/VisualSettingsContext';
3+
import BrutalistImageModal from './BrutalistImageModal';
4+
import LuxeImageModal from './LuxeImageModal';
45

5-
const ImageModal = ({ src, alt, onClose }) => {
6-
const [dimensions, setDimensions] = useState(null);
6+
const ImageModal = (props) => {
7+
const { fezcodexTheme } = useVisualSettings();
78

8-
useEffect(() => {
9-
if (src) {
10-
document.body.style.overflow = 'hidden';
11-
} else {
12-
document.body.style.overflow = '';
13-
}
9+
if (fezcodexTheme === 'luxe') {
10+
return <LuxeImageModal {...props} />;
11+
}
1412

15-
const handleKeyDown = (e) => {
16-
if (e.key === 'Escape') {
17-
onClose();
18-
}
19-
};
20-
21-
if (src) {
22-
window.addEventListener('keydown', handleKeyDown);
23-
}
24-
25-
return () => {
26-
document.body.style.overflow = '';
27-
window.removeEventListener('keydown', handleKeyDown);
28-
};
29-
}, [src, onClose]);
30-
31-
const handleImageLoad = (e) => {
32-
setDimensions({
33-
width: e.target.naturalWidth,
34-
height: e.target.naturalHeight
35-
});
36-
};
37-
38-
const showAlt = alt && !['Project Detail', 'Enlarged Content', 'Intel Imagery', 'Full size image'].includes(alt);
39-
40-
return (
41-
<AnimatePresence>
42-
{src && (
43-
<motion.div
44-
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/90 backdrop-blur-xl p-2 md:p-4"
45-
onClick={onClose}
46-
initial={{ opacity: 0 }}
47-
animate={{ opacity: 1 }}
48-
exit={{ opacity: 0 }}
49-
transition={{ duration: 0.2 }}
50-
>
51-
{/* Grid Background Overlay */}
52-
<div className="absolute inset-0 pointer-events-none opacity-10"
53-
style={{ backgroundImage: 'radial-gradient(circle, #444 1px, transparent 1px)', backgroundSize: '30px 30px' }}
54-
/>
55-
56-
<motion.div
57-
className="relative w-full h-full max-w-[85vw] max-h-[85vh] flex flex-col"
58-
onClick={(e) => e.stopPropagation()}
59-
initial={{ scale: 0.98, opacity: 0 }}
60-
animate={{ scale: 1, opacity: 1 }}
61-
exit={{ scale: 0.98, opacity: 0 }}
62-
transition={{ type: "spring", damping: 30, stiffness: 300 }}
63-
>
64-
{/* Header Bar */}
65-
<div className="flex items-center justify-between bg-black/60 border border-white/10 border-b-0 p-2 md:p-3 backdrop-blur-md rounded-t-sm">
66-
<div className="flex items-center gap-3">
67-
<Scan className="text-emerald-500 animate-pulse" size={16} />
68-
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-gray-400">
69-
SYSTEM.IMG_VIEWER // SECURE_MODE
70-
</span>
71-
</div>
72-
<div className="flex items-center gap-2">
73-
<span className="hidden md:inline font-mono text-[9px] text-gray-600 uppercase tracking-widest mr-2">Press ESC to exit</span>
74-
<button
75-
onClick={onClose}
76-
className="group flex items-center gap-2 px-4 py-1.5 bg-white/5 hover:bg-red-500 text-gray-400 hover:text-white border border-white/10 rounded-sm transition-all"
77-
>
78-
<X size={16} weight="bold" />
79-
</button>
80-
</div>
81-
</div>
82-
83-
{/* Image Container */}
84-
<div className="relative flex-1 min-h-0 bg-black/40 backdrop-blur-sm rounded-b-sm overflow-hidden flex items-center justify-center">
85-
{/* Corner Accents */}
86-
<div className="absolute top-0 left-0 w-6 h-6 border-l-2 border-t-2 border-emerald-500/30 z-10" />
87-
<div className="absolute top-0 right-0 w-6 h-6 border-r-2 border-t-2 border-emerald-500/30 z-10" />
88-
<div className="absolute bottom-0 left-0 w-6 h-6 border-l-2 border-b-2 border-emerald-500/30 z-10" />
89-
<div className="absolute bottom-0 right-0 w-6 h-6 border-r-2 border-b-2 border-emerald-500/30 z-10" />
90-
91-
<img
92-
src={src}
93-
alt={alt}
94-
onLoad={handleImageLoad}
95-
style={{ maxWidth: '100%', maxHeight: '100%' }}
96-
className="w-auto h-auto object-contain block shadow-[0_0_50px_rgba(0,0,0,0.5)] select-none"
97-
/>
98-
</div>
99-
100-
{/* Footer Metadata */}
101-
<div className="mt-3 flex justify-between items-center px-2">
102-
<div className="flex gap-4">
103-
<span className="font-mono text-sm uppercase tracking-widest text-white font-bold truncate max-w-[50vw]">
104-
{showAlt ? alt : 'RAW_STREAM'}
105-
</span>
106-
</div>
107-
<span className="font-mono text-sm uppercase tracking-widest text-white font-bold text-right">
108-
{dimensions ? `${dimensions.width} x ${dimensions.height} PX` : 'CALCULATING...'}
109-
</span>
110-
</div>
111-
112-
</motion.div>
113-
</motion.div>
114-
)}
115-
</AnimatePresence>
116-
);
13+
return <BrutalistImageModal {...props} />;
11714
};
11815

119-
export default ImageModal;
16+
export default ImageModal;

src/components/LuxeImageModal.jsx

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import { ArrowsInSimple } from '@phosphor-icons/react';
4+
import { motion, AnimatePresence } from 'framer-motion';
5+
6+
const LuxeImageModal = ({ src, alt, onClose }) => {
7+
const [dimensions, setDimensions] = useState(null);
8+
9+
useEffect(() => {
10+
if (src) {
11+
document.body.style.overflow = 'hidden';
12+
} else {
13+
document.body.style.overflow = '';
14+
}
15+
16+
const handleKeyDown = (e) => {
17+
if (e.key === 'Escape') {
18+
onClose();
19+
}
20+
};
21+
22+
if (src) {
23+
window.addEventListener('keydown', handleKeyDown);
24+
}
25+
26+
return () => {
27+
document.body.style.overflow = '';
28+
window.removeEventListener('keydown', handleKeyDown);
29+
};
30+
}, [src, onClose]);
31+
32+
const handleImageLoad = (e) => {
33+
setDimensions({
34+
width: e.target.naturalWidth,
35+
height: e.target.naturalHeight
36+
});
37+
};
38+
39+
const showAlt = alt && !['Project Detail', 'Enlarged Content', 'Intel Imagery', 'Full size image'].includes(alt);
40+
41+
return createPortal(
42+
<AnimatePresence>
43+
{src && (
44+
<motion.div
45+
className="fixed inset-0 z-[1000] flex items-center justify-center bg-[#F5F5F0]/95 backdrop-blur-md p-4 md:p-8"
46+
onClick={onClose}
47+
initial={{ opacity: 0 }}
48+
animate={{ opacity: 1 }}
49+
exit={{ opacity: 0 }}
50+
transition={{ duration: 0.4 }}
51+
>
52+
<motion.div
53+
className="relative w-full h-full max-w-7xl max-h-[90vh] flex flex-col"
54+
onClick={(e) => e.stopPropagation()}
55+
initial={{ scale: 0.98, opacity: 0, y: 20 }}
56+
animate={{ scale: 1, opacity: 1, y: 0 }}
57+
exit={{ scale: 0.98, opacity: 0, y: 20 }}
58+
transition={{ type: "spring", damping: 35, stiffness: 200 }}
59+
>
60+
{/* Header */}
61+
<div className="flex items-center justify-between bg-white/80 backdrop-blur-sm border border-black/5 p-4 md:p-6 rounded-t-sm shadow-sm">
62+
<div className="flex flex-col">
63+
<span className="font-outfit text-[9px] uppercase tracking-[0.4em] text-[#8D4004] font-bold">
64+
Visual Archive
65+
</span>
66+
<h2 className="font-playfairDisplay italic text-xl text-[#1A1A1A] truncate max-w-md">
67+
{showAlt ? alt : 'Selected Specimen'}
68+
</h2>
69+
</div>
70+
<button
71+
onClick={onClose}
72+
className="group flex items-center gap-3 px-6 py-2 bg-white hover:bg-[#1A1A1A] text-[#1A1A1A] hover:text-white border border-black/10 rounded-full transition-all shadow-sm"
73+
>
74+
<span className="font-outfit text-[10px] uppercase tracking-widest font-bold">Minimize</span>
75+
<ArrowsInSimple size={18} weight="light" />
76+
</button>
77+
</div>
78+
79+
{/* Image Container */}
80+
<div className="relative flex-1 min-h-0 bg-white border-x border-black/5 overflow-hidden flex items-center justify-center p-4 md:p-12">
81+
<img
82+
src={src}
83+
alt={alt}
84+
onLoad={handleImageLoad}
85+
style={{ maxWidth: '100%', maxHeight: '100%' }}
86+
className="w-auto h-auto object-contain block shadow-[0_40px_100px_-20px_rgba(0,0,0,0.2)] select-none transition-transform duration-700 hover:scale-[1.02]"
87+
/>
88+
</div>
89+
90+
{/* Footer */}
91+
<div className="bg-[#FAFAF8] border border-black/5 p-4 px-8 flex justify-between items-center rounded-b-sm">
92+
<div className="flex items-center gap-4 font-outfit text-[10px] uppercase tracking-[0.2em] text-[#1A1A1A]/40">
93+
<span>Dimension Analysis</span>
94+
<span className="w-1 h-1 bg-[#8D4004]/30 rounded-full" />
95+
<span className="text-[#1A1A1A]/60 font-bold">
96+
{dimensions ? `${dimensions.width} x ${dimensions.height} PX` : 'SCANNING...'}
97+
</span>
98+
</div>
99+
<span className="font-playfairDisplay italic text-[10px] text-[#1A1A1A]/20">
100+
Fezcodex High-Fidelity Viewer
101+
</span>
102+
</div>
103+
</motion.div>
104+
</motion.div>
105+
)}
106+
</AnimatePresence>,
107+
document.body
108+
);
109+
};
110+
111+
export default LuxeImageModal;

0 commit comments

Comments
 (0)