diff --git a/src/app/memories/page.tsx b/src/app/memories/page.tsx index 0a1b118..33df0ba 100644 --- a/src/app/memories/page.tsx +++ b/src/app/memories/page.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { useFamily } from "../FamilyProvider"; import { Button, ConfirmDialog, Modal } from "@/components/ui"; -const FOLDERS = [ +const PRESET_FOLDERS = [ { id: "", label: "All", emoji: "🌟" }, { id: "first-steps", label: "First Steps", emoji: "πŸ‘£" }, { id: "bath-time", label: "Bath Time", emoji: "πŸ›" }, @@ -17,6 +17,10 @@ const FOLDERS = [ { id: "smiles", label: "Smiles", emoji: "😊" }, ]; +const EMOJI_OPTIONS = ["πŸ“","🌈","⭐","🎡","πŸ–","🎈","🐣","πŸ’›","🌸","πŸ¦‹","🐾","πŸŽ€","πŸŒ™","🏠","🎠"]; + +interface Folder { id: string; label: string; emoji: string; } + interface Memory { id: string; key: string; @@ -48,9 +52,39 @@ export default function MemoriesPage() { const [query, setQuery] = useState(""); const [searchResults, setSearchResults] = useState(null); const [searching, setSearching] = useState(false); - const fileRef = useRef(null); + + // Custom folders + const [customFolders, setCustomFolders] = useState([]); + const [showNewFolder, setShowNewFolder] = useState(false); + const [newFolderName, setNewFolderName] = useState(""); + const [newFolderEmoji, setNewFolderEmoji] = useState("πŸ“"); + + const fileRef = useRef(null); const loaderRef = useRef(null); + useEffect(() => { + try { + const saved = localStorage.getItem("tia_custom_folders"); + if (saved) setCustomFolders(JSON.parse(saved)); + } catch {} + }, []); + + const allFolders: Folder[] = [...PRESET_FOLDERS, ...customFolders]; + + const handleCreateFolder = () => { + const trimmed = newFolderName.trim(); + if (!trimmed) return; + const id = trimmed.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") || `folder-${Date.now()}`; + if (allFolders.some(f => f.id === id)) { alert("A folder with this name already exists."); return; } + const folder: Folder = { id, label: trimmed, emoji: newFolderEmoji }; + const updated = [...customFolders, folder]; + setCustomFolders(updated); + localStorage.setItem("tia_custom_folders", JSON.stringify(updated)); + setNewFolderName(""); + setNewFolderEmoji("πŸ“"); + setShowNewFolder(false); + }; + const fetchMemories = useCallback(async (cursor?: string) => { if (!childId) return; try { @@ -66,7 +100,6 @@ export default function MemoriesPage() { useEffect(() => { if (childId) fetchMemories(); }, [childId, fetchMemories]); - // Infinite scroll useEffect(() => { if (!loaderRef.current) return; const observer = new IntersectionObserver(entries => { @@ -79,9 +112,7 @@ export default function MemoriesPage() { return () => observer.disconnect(); }, [nextCursor, loadingMore, fetchMemories]); - const handleUploadClick = () => { - setShowFolderPicker(true); - }; + const handleUploadClick = () => setShowFolderPicker(true); const handleFolderChosen = (folderId: string) => { setPendingFolder(folderId); @@ -92,10 +123,8 @@ export default function MemoriesPage() { const handleUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file || !childId) return; - setUploading(true); try { - // Step 1: Get key + memoryId const initRes = await fetch("/api/upload", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -104,19 +133,15 @@ export default function MemoriesPage() { const { key, memoryId, publicUrl, error } = await initRes.json(); if (error) { alert("Error: " + error); return; } - // Step 2: Upload via server proxy const putParams = new URLSearchParams({ key, contentType: file.type }); const putRes = await fetch(`/api/upload?${putParams}`, { - method: "PUT", - body: file, - headers: { "Content-Type": file.type }, + method: "PUT", body: file, headers: { "Content-Type": file.type }, }); if (!putRes.ok) { const e = await putRes.json().catch(() => ({})) as { error?: string }; throw new Error(e.error ?? `Upload failed (${putRes.status})`); } - // Step 2.5: Assign folder (description) if one was chosen if (pendingFolder) { await fetch(`/api/memories/${memoryId}`, { method: "PATCH", @@ -125,7 +150,6 @@ export default function MemoriesPage() { }); } - // Step 3: Confirm β†’ fires thumbnail + vision pipeline await fetch(`/api/memories/${memoryId}/confirm`, { method: "POST" }); const optimistic: Memory = { @@ -137,7 +161,6 @@ export default function MemoriesPage() { }; setMemories(prev => [optimistic, ...prev]); } catch (err) { alert("Upload failed: " + err); } - setUploading(false); setPendingFolder(""); if (fileRef.current) fileRef.current.value = ""; @@ -182,7 +205,7 @@ export default function MemoriesPage() {
← -

Memories πŸ“Έ

+

Memories πŸ“Έ

@@ -202,7 +225,7 @@ export default function MemoriesPage() { {/* Folder pills */}
- {FOLDERS.map(folder => { + {allFolders.map(folder => { const count = folder.id === "" ? memories.length : (folderCounts[folder.id] || 0); const isActive = activeFolder === folder.id; return ( @@ -218,13 +241,19 @@ export default function MemoriesPage() { {folder.emoji} {folder.label} {count > 0 && ( - - {count} - + {count} )} ); })} + {/* New folder button */} +
{/* Gallery grid */} @@ -246,7 +275,6 @@ export default function MemoriesPage() { ))} )} -
{loadingMore && "Loading more..."}
@@ -263,39 +291,80 @@ export default function MemoriesPage() { > {uploading ? "…" : "+"} - + {/* Folder picker modal */} setShowFolderPicker(false)} title="Add to folder" maxWidth="sm">
- {FOLDERS.map(folder => ( + {allFolders.map(folder => ( ))} +

Tap "All" to upload without a folder

+ {/* New folder modal */} + { setShowNewFolder(false); setNewFolderName(""); setNewFolderEmoji("πŸ“"); }} title="Create folder" maxWidth="sm"> +
+
+

Choose an emoji

+
+ {EMOJI_OPTIONS.map(em => ( + + ))} +
+
+
+

Folder name

+ setNewFolderName(e.target.value)} + onKeyDown={e => e.key === "Enter" && handleCreateFolder()} + placeholder="e.g. Park days" + maxLength={30} + className="w-full px-3 py-2 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm dark:text-white focus:outline-none focus:ring-2 focus:ring-rose-300" + /> +
+
+ + +
+
+
+ {/* Fullscreen viewer */} {selected && ( setSelected(null)} onDelete={() => setDeleteTarget(selected.id)} onUpdate={updated => { @@ -323,12 +392,8 @@ export default function MemoriesPage() { function MemoryTile({ memory, onClick }: { memory: Memory; onClick: () => void }) { const [loaded, setLoaded] = useState(false); const src = memory.thumbnailUrl || memory.url; - return ( - ); } // ─── Fullscreen viewer ──────────────────────────────────────────────────────── -function MemoryViewer({ memory, onClose, onDelete, onUpdate }: { +function MemoryViewer({ memory, allFolders, onClose, onDelete, onUpdate }: { memory: Memory; + allFolders: Folder[]; onClose: () => void; onDelete: () => void; onUpdate: (updated: Partial & { id: string }) => void; }) { - const [isPrivate, setIsPrivate] = useState(memory.isPrivate); - const [toggling, setToggling] = useState(false); - const [showMeta, setShowMeta] = useState(false); + const [isPrivate, setIsPrivate] = useState(memory.isPrivate); + const [toggling, setToggling] = useState(false); + const [showChrome, setShowChrome] = useState(true); + const [zoomed, setZoomed] = useState(false); const [folderEditing, setFolderEditing] = useState(false); const togglePrivate = async () => { @@ -390,19 +453,27 @@ function MemoryViewer({ memory, onClose, onDelete, onUpdate }: { setFolderEditing(false); }; - const currentFolder = FOLDERS.find(f => f.id === memory.description); + const currentFolder = allFolders.find(f => f.id === memory.description); + + const handleImageTap = () => { + if (zoomed) { + setZoomed(false); + } else { + setShowChrome(s => !s); + } + }; return ( -
+
e.target === e.currentTarget && onClose()}> {/* Top bar */} -
- +
+
- {/* Image */} -
setShowMeta(s => !s)}> + {/* Image area */} +
{memory.title
- {/* Caption/tags β€” tap image to toggle */} - {showMeta && (memory.visionCaption || (memory.visionTags && memory.visionTags.length > 0)) && ( -
+ {/* Zoom hint */} + {showChrome && !zoomed && ( +
+ tap to hide UI Β· tap again to zoom +
+ )} + + {/* Caption/tags */} + {showChrome && !zoomed && (memory.visionCaption || (memory.visionTags && memory.visionTags.length > 0)) && ( +
{memory.visionCaption && (

{memory.visionCaption}

)} @@ -442,25 +524,19 @@ function MemoryViewer({ memory, onClose, onDelete, onUpdate }: {
)} - {/* Folder assignment modal */} + {/* Folder assignment bottom sheet */} {folderEditing && ( -
setFolderEditing(false)} - > -
e.stopPropagation()} - > +
setFolderEditing(false)}> +
e.stopPropagation()}>

Move to folder

-
- {FOLDERS.map(folder => ( +
+ {allFolders.map(folder => (