chore: make able to switch screenshots in zoom
This commit is contained in:
parent
0079da3b49
commit
5b99a76745
40
package-lock.json
generated
40
package-lock.json
generated
@ -9,18 +9,18 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marsidev/react-turnstile": "^1.5.0",
|
"@marsidev/react-turnstile": "^1.5.0",
|
||||||
"@vnedyalk0v/react19-simple-maps": "^2.0.3",
|
"@vnedyalk0v/react19-simple-maps": "^2.0.7",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.15.0",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"i18next": "^26.0.3",
|
"i18next": "^26.0.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.8.0",
|
||||||
"or": "^0.2.0",
|
"or": "^0.2.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.5",
|
||||||
"react-i18next": "^17.0.2",
|
"react-i18next": "^17.0.2",
|
||||||
"react-is": "^19.2.4",
|
"react-is": "^19.2.5",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.14.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.5.2",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
@ -37,8 +37,8 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.1",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@ -1042,13 +1042,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "25.5.2",
|
"version": "25.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
"integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.18.0"
|
"undici-types": "~7.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
@ -1978,9 +1978,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.334",
|
"version": "1.5.335",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
|
||||||
"integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==",
|
"integrity": "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@ -4829,9 +4829,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.18.2",
|
"version": "7.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
22
package.json
22
package.json
@ -12,18 +12,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marsidev/react-turnstile": "^1.5.0",
|
"@marsidev/react-turnstile": "^1.5.0",
|
||||||
"@vnedyalk0v/react19-simple-maps": "^2.0.3",
|
"@vnedyalk0v/react19-simple-maps": "^2.0.7",
|
||||||
"axios": "^1.14.0",
|
"axios": "^1.15.0",
|
||||||
"d3-geo": "^3.1.1",
|
"d3-geo": "^3.1.1",
|
||||||
"d3-scale": "^4.0.2",
|
"d3-scale": "^4.0.2",
|
||||||
"i18next": "^26.0.3",
|
"i18next": "^26.0.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.8.0",
|
||||||
"or": "^0.2.0",
|
"or": "^0.2.0",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.5",
|
||||||
"react-i18next": "^17.0.2",
|
"react-i18next": "^17.0.2",
|
||||||
"react-is": "^19.2.4",
|
"react-is": "^19.2.5",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.14.0",
|
"react-router-dom": "^7.14.0",
|
||||||
"recharts": "^3.8.1",
|
"recharts": "^3.8.1",
|
||||||
@ -31,7 +31,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.5.2",
|
"@types/node": "^25.6.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
@ -40,10 +40,10 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.1",
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.8"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "^8.0.4"
|
"vite": "^8.0.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,12 +9,12 @@ import SEO from '../components/SEO';
|
|||||||
|
|
||||||
const REPORT_REASONS = [
|
const REPORT_REASONS = [
|
||||||
'Malware or malicious code',
|
'Malware or malicious code',
|
||||||
'Stolen or plagiarised content',
|
'Stolen or plagiarised content',
|
||||||
'Misleading description',
|
'Misleading description',
|
||||||
'Broken / non-functional',
|
'Broken / non-functional',
|
||||||
'Inappropriate content',
|
'Inappropriate content',
|
||||||
'Spam or fake listing',
|
'Spam or fake listing',
|
||||||
'Other',
|
'Other',
|
||||||
];
|
];
|
||||||
|
|
||||||
const MAX_REVIEW_LENGTH = 2000;
|
const MAX_REVIEW_LENGTH = 2000;
|
||||||
@ -73,6 +73,7 @@ const PluginDetail = () => {
|
|||||||
const [adShown, setAdShown] = useState(false);
|
const [adShown, setAdShown] = useState(false);
|
||||||
const [mainScreenshot, setMainScreenshot] = useState<string | null>(null);
|
const [mainScreenshot, setMainScreenshot] = useState<string | null>(null);
|
||||||
const [lightboxImage, setLightboxImage] = useState<string | null>(null);
|
const [lightboxImage, setLightboxImage] = useState<string | null>(null);
|
||||||
|
const [lightboxIndex, setLightboxIndex] = useState<number>(0);
|
||||||
const [selectedLanguage] = useState('en');
|
const [selectedLanguage] = useState('en');
|
||||||
const [currentDescription, setCurrentDescription] = useState('');
|
const [currentDescription, setCurrentDescription] = useState('');
|
||||||
const [setLoading] = useState(true);
|
const [setLoading] = useState(true);
|
||||||
@ -141,6 +142,42 @@ const PluginDetail = () => {
|
|||||||
}
|
}
|
||||||
}, [plugin, selectedLanguage]);
|
}, [plugin, selectedLanguage]);
|
||||||
|
|
||||||
|
// ── Lightbox helpers ─────────────────────────────────────────────────────
|
||||||
|
const screenshots = plugin?.screenshots ?? [];
|
||||||
|
|
||||||
|
const openLightbox = (index: number) => {
|
||||||
|
setLightboxIndex(index);
|
||||||
|
setLightboxImage(screenshots[index]?.path ?? null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightboxPrev = () => {
|
||||||
|
if (!screenshots.length) return;
|
||||||
|
const newIndex = (lightboxIndex - 1 + screenshots.length) % screenshots.length;
|
||||||
|
setLightboxIndex(newIndex);
|
||||||
|
setLightboxImage(screenshots[newIndex].path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lightboxNext = () => {
|
||||||
|
if (!screenshots.length) return;
|
||||||
|
const newIndex = (lightboxIndex + 1) % screenshots.length;
|
||||||
|
setLightboxIndex(newIndex);
|
||||||
|
setLightboxImage(screenshots[newIndex].path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLightbox = () => setLightboxImage(null);
|
||||||
|
|
||||||
|
// ── Keyboard navigation for lightbox ────────────────────────────────────
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lightboxImage) return;
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'ArrowLeft') lightboxPrev();
|
||||||
|
else if (e.key === 'ArrowRight') lightboxNext();
|
||||||
|
else if (e.key === 'Escape') closeLightbox();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handleKey);
|
||||||
|
return () => window.removeEventListener('keydown', handleKey);
|
||||||
|
}, [lightboxImage, lightboxIndex, screenshots]);
|
||||||
|
|
||||||
const hasReviewed = useMemo(() => {
|
const hasReviewed = useMemo(() => {
|
||||||
if (!user || !plugin?.reviews || !Array.isArray(plugin.reviews)) return false;
|
if (!user || !plugin?.reviews || !Array.isArray(plugin.reviews)) return false;
|
||||||
return plugin.reviews.some((r: any) => r.username === user.username);
|
return plugin.reviews.some((r: any) => r.username === user.username);
|
||||||
@ -357,7 +394,10 @@ const PluginDetail = () => {
|
|||||||
alt="Main"
|
alt="Main"
|
||||||
className="main-screenshot"
|
className="main-screenshot"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => setLightboxImage(mainScreenshot || plugin.screenshots[0].path)}
|
onClick={() => {
|
||||||
|
const idx = plugin.screenshots.findIndex((s: any) => s.path === (mainScreenshot || plugin.screenshots[0].path));
|
||||||
|
openLightbox(idx >= 0 ? idx : 0);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="thumbnail-strip">
|
<div className="thumbnail-strip">
|
||||||
{plugin.screenshots.map((shot: any) => (
|
{plugin.screenshots.map((shot: any) => (
|
||||||
@ -549,7 +589,6 @@ const PluginDetail = () => {
|
|||||||
<div className="stat-item"><strong>{plugin.views.toLocaleString()}</strong> Views</div>
|
<div className="stat-item"><strong>{plugin.views.toLocaleString()}</strong> Views</div>
|
||||||
<div className="stat-item">Category: <strong>{plugin.category}</strong></div>
|
<div className="stat-item">Category: <strong>{plugin.category}</strong></div>
|
||||||
|
|
||||||
{/* ADD THESE TWO */}
|
|
||||||
{plugin.version && (
|
{plugin.version && (
|
||||||
<div className="stat-item">Version: <strong>{plugin.version}</strong></div>
|
<div className="stat-item">Version: <strong>{plugin.version}</strong></div>
|
||||||
)}
|
)}
|
||||||
@ -616,12 +655,48 @@ const PluginDetail = () => {
|
|||||||
|
|
||||||
{/* LIGHTBOX MODAL */}
|
{/* LIGHTBOX MODAL */}
|
||||||
{lightboxImage && (
|
{lightboxImage && (
|
||||||
<div className="lightbox-overlay" onClick={() => setLightboxImage(null)}>
|
<div className="lightbox-overlay" onClick={closeLightbox}>
|
||||||
<div className="lightbox-content" onClick={(e) => e.stopPropagation()}>
|
<div className="lightbox-content" onClick={(e) => e.stopPropagation()}>
|
||||||
<img src={lightboxImage} alt="Expanded view" className="lightbox-img" />
|
<img src={lightboxImage} alt="Expanded view" className="lightbox-img" />
|
||||||
<button className="lightbox-close" onClick={() => setLightboxImage(null)}>
|
<button className="lightbox-close" onClick={closeLightbox}>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Left arrow — only shown when multiple screenshots exist */}
|
||||||
|
{screenshots.length > 1 && (
|
||||||
|
<button
|
||||||
|
className="lightbox-arrow lightbox-arrow-left"
|
||||||
|
onClick={(e) => { e.stopPropagation(); lightboxPrev(); }}
|
||||||
|
aria-label="Previous screenshot"
|
||||||
|
>
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right arrow */}
|
||||||
|
{screenshots.length > 1 && (
|
||||||
|
<button
|
||||||
|
className="lightbox-arrow lightbox-arrow-right"
|
||||||
|
onClick={(e) => { e.stopPropagation(); lightboxNext(); }}
|
||||||
|
aria-label="Next screenshot"
|
||||||
|
>
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dot indicators */}
|
||||||
|
{screenshots.length > 1 && (
|
||||||
|
<div className="lightbox-dots">
|
||||||
|
{screenshots.map((_: any, i: number) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={`lightbox-dot ${i === lightboxIndex ? 'active' : ''}`}
|
||||||
|
onClick={(e) => { e.stopPropagation(); openLightbox(i); }}
|
||||||
|
aria-label={`Go to screenshot ${i + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user