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/api/admin/posts/route.ts b/src/app/api/admin/posts/route.ts index f475e88..023b4da 100644 --- a/src/app/api/admin/posts/route.ts +++ b/src/app/api/admin/posts/route.ts @@ -50,7 +50,7 @@ export async function POST(request: Request) { export async function GET(request: Request) { // Return the current pinned.json object try { - const pinnedPath = path.join(postsDirectory, 'pinned.json'); + const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); console.log('Reading pinned.json from:', pinnedPath); let pinnedData = { pinned: [], folderEmojis: {} }; if (fs.existsSync(pinnedPath)) { @@ -73,7 +73,7 @@ export async function PATCH(request: Request) { if (!Array.isArray(pinned) || typeof folderEmojis !== 'object') { return NextResponse.json({ error: 'Invalid pinned or folderEmojis data' }, { status: 400 }); } - const pinnedPath = path.join(postsDirectory, 'pinned.json'); + const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); console.log('Saving pinned.json to:', pinnedPath); console.log('Saving data:', { pinned, folderEmojis }); fs.writeFileSync(pinnedPath, JSON.stringify({ pinned, folderEmojis }, null, 2), 'utf8'); diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index b9a328d..af5fd5e 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -12,19 +12,6 @@ import { getPostsDirectory } from '@/lib/postsDirectory'; const postsDirectory = getPostsDirectory(); -const pinnedPath = path.join(postsDirectory, 'pinned.json'); -let pinnedData: { pinned: string[]; folderEmojis: Record } = { pinned: [], folderEmojis: {} }; -if (fs.existsSync(pinnedPath)) { - try { - const raw = fs.readFileSync(pinnedPath, 'utf8'); - pinnedData = JSON.parse(raw); - if (!pinnedData.pinned) pinnedData.pinned = []; - if (!pinnedData.folderEmojis) pinnedData.folderEmojis = {}; - } catch { - pinnedData = { pinned: [], folderEmojis: {} }; - } -} - // Function to get file creation date function getFileCreationDate(filePath: string): Date { const stats = fs.statSync(filePath); @@ -62,7 +49,53 @@ marked.setOptions({ renderer, }); -async function getPostByPath(filePath: string, relPath: string) { +// Replace top-level pinnedData logic with a function +function getPinnedData() { + const pinnedPath = path.join(process.cwd(), 'posts', 'pinned.json'); + let pinnedData = { pinned: [], folderEmojis: {} }; + if (fs.existsSync(pinnedPath)) { + try { + const raw = fs.readFileSync(pinnedPath, 'utf8'); + pinnedData = JSON.parse(raw); + if (!pinnedData.pinned) pinnedData.pinned = []; + if (!pinnedData.folderEmojis) pinnedData.folderEmojis = {}; + } catch { + pinnedData = { pinned: [], folderEmojis: {} }; + } + } + return pinnedData; +} + +// Update readPostsDir to accept pinnedData as an argument +async function readPostsDir(dir: string, relDir = '', pinnedData: { pinned: string[]; folderEmojis: Record }): Promise { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const folders: any[] = []; + const posts: any[] = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + const relPath = relDir ? path.join(relDir, entry.name) : entry.name; + + if (entry.isDirectory()) { + const children = await readPostsDir(fullPath, relPath, pinnedData); + // Debug log for emoji lookup + console.log('[FOLDER EMOJI DEBUG]', { relPath, allEmojis: pinnedData.folderEmojis, emoji: pinnedData.folderEmojis[relPath] }); + const emoji = pinnedData.folderEmojis[relPath] || '📁'; + folders.push({ type: 'folder', name: entry.name, path: relPath, emoji, children }); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + posts.push(await getPostByPath(fullPath, relPath, pinnedData)); + } + } + + // Sort posts by creation date (newest first) + posts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + // Folders first, then posts + return [...folders, ...posts]; +} + +// Update getPostByPath to accept pinnedData +async function getPostByPath(filePath: string, relPath: string, pinnedData: { pinned: string[]; folderEmojis: Record }) { const fileContents = fs.readFileSync(filePath, 'utf8'); const { data, content } = matter(fileContents); const createdAt = getFileCreationDate(filePath); @@ -109,34 +142,11 @@ async function getPostByPath(filePath: string, relPath: string) { }; } -async function readPostsDir(dir: string, relDir = ''): Promise { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - const folders: any[] = []; - const posts: any[] = []; - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - const relPath = relDir ? path.join(relDir, entry.name) : entry.name; - - if (entry.isDirectory()) { - const children = await readPostsDir(fullPath, relPath); - const emoji = pinnedData.folderEmojis[relPath] || '📁'; - folders.push({ type: 'folder', name: entry.name, path: relPath, emoji, children }); - } else if (entry.isFile() && entry.name.endsWith('.md')) { - posts.push(await getPostByPath(fullPath, relPath)); - } - } - - // Sort posts by creation date (newest first) - posts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); - - // Folders first, then posts - return [...folders, ...posts]; -} - +// Update GET handler to use fresh pinnedData export async function GET() { try { - const tree = await readPostsDir(postsDirectory); + const pinnedData = getPinnedData(); + const tree = await readPostsDir(postsDirectory, '', pinnedData); return NextResponse.json(tree); } catch (error) { console.error('Error loading posts:', error); 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 */}