From d51dc983ec5ae190c7b7163583ca78c22170017a Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Mon, 23 Jun 2025 13:15:44 +0200 Subject: [PATCH] very nice --- .gitignore | 6 +- src/app/posts/[...slug]/page.tsx | 187 ++++++++++++++++++++++++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 91cf545..da35784 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ node_modules electron/dist posts/admin.json posts/admin.json.tmp -.vscode \ No newline at end of file +.vscode +posts/pinned.json +posts/Aquaworld/tag-1.md +posts/pinned.json +posts/pinned.json diff --git a/src/app/posts/[...slug]/page.tsx b/src/app/posts/[...slug]/page.tsx index 82c5b10..bc3aa0e 100644 --- a/src/app/posts/[...slug]/page.tsx +++ b/src/app/posts/[...slug]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { format } from 'date-fns'; import Link from 'next/link'; @@ -16,6 +16,73 @@ interface Post { export default function PostPage({ params }: { params: { slug: string[] } }) { const [post, setPost] = useState(null); + // Modal state for zoomed image + const [zoomImgSrc, setZoomImgSrc] = useState(null); + const [zoomLevel, setZoomLevel] = useState(1.5); // Start zoomed in + const [imgOffset, setImgOffset] = useState({ x: 0, y: 0 }); + const [dragging, setDragging] = useState(false); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); + const [imgStart, setImgStart] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); + const modalImgRef = useRef(null); + const modalContainerRef = useRef(null); + + // Prevent background scroll when modal is open + useEffect(() => { + if (zoomImgSrc) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + return () => { + document.body.style.overflow = ''; + }; + }, [zoomImgSrc]); + + // Reset offset and zoom when opening a new image + useEffect(() => { + if (zoomImgSrc) { + setImgOffset({ x: 0, y: 0 }); + setZoomLevel(1.5); + } + }, [zoomImgSrc]); + + // Drag logic (mouse) + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + setDragging(true); + setDragStart({ x: e.clientX, y: e.clientY }); + setImgStart(imgOffset); + }; + const handleMouseMove = (e: React.MouseEvent) => { + if (!dragging || !dragStart) return; + setImgOffset({ + x: imgStart.x + (e.clientX - dragStart.x), + y: imgStart.y + (e.clientY - dragStart.y), + }); + }; + const handleMouseUp = () => { + setDragging(false); + setDragStart(null); + }; + + // Drag logic (touch) + const handleTouchStart = (e: React.TouchEvent) => { + if (e.touches.length !== 1) return; + setDragging(true); + setDragStart({ x: e.touches[0].clientX, y: e.touches[0].clientY }); + setImgStart(imgOffset); + }; + const handleTouchMove = (e: React.TouchEvent) => { + if (!dragging || !dragStart || e.touches.length !== 1) return; + setImgOffset({ + x: imgStart.x + (e.touches[0].clientX - dragStart.x), + y: imgStart.y + (e.touches[0].clientY - dragStart.y), + }); + }; + const handleTouchEnd = () => { + setDragging(false); + setDragStart(null); + }; // Join the slug array to get the full path const slugPath = Array.isArray(params.slug) ? params.slug.join('/') : params.slug; @@ -493,6 +560,42 @@ export default function PostPage({ params }: { params: { slug: string[] } }) { }; }, [post]); + // Attach click handler to images in .prose + useEffect(() => { + if (!post) return; + const prose = document.querySelectorAll('.prose'); + const handleImgClick = (e: Event) => { + const target = e.target as HTMLElement; + if (target.tagName === 'IMG') { + setZoomImgSrc((target as HTMLImageElement).src); + setZoomLevel(1.5); + } + }; + prose.forEach((el) => el.addEventListener('click', handleImgClick)); + return () => { + prose.forEach((el) => el.removeEventListener('click', handleImgClick)); + }; + }, [post]); + + // Keyboard ESC to close modal + useEffect(() => { + if (!zoomImgSrc) return; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setZoomImgSrc(null); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [zoomImgSrc]); + + // Zoom controls for desktop + const handleWheel = (e: React.WheelEvent) => { + if (window.innerWidth < 640) return; // skip on mobile + e.preventDefault(); + setZoomLevel((z) => Math.max(0.2, Math.min(5, z + (e.deltaY < 0 ? 0.1 : -0.1)))); + }; + + // Pinch-to-zoom is native on mobile if image is in a scrollable container with touch gestures enabled + const loadPost = async () => { try { const response = await fetch(`/api/posts/${encodeURIComponent(slugPath)}`); @@ -509,6 +612,88 @@ export default function PostPage({ params }: { params: { slug: string[] } }) { return (
+ {/* Modal for zoomed image */} + {zoomImgSrc && ( +
setZoomImgSrc(null)} + onWheel={handleWheel} + style={{ touchAction: 'none' }} + > +
e.stopPropagation()} + style={{ + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + touchAction: 'pinch-zoom', + }} + > + {/* Mobile X button */} + + Zoomed + {/* Desktop zoom controls */} + {window.innerWidth >= 640 && ( +
+ + + +
+ )} +
+
+ )} {/* Mobile: Full width, no borders */}
{/* Mobile back button */}