- Add signout button to menu (below Settings) - Fix profile API to fetch user from database session - Fix profile page to save name to database - Fix settings page to use familyId from FamilyProvider - Fix family page to use FamilyProvider - Fix activity, ai, medical, memories pages to use FamilyProvider - Remove all hardcoded "default" familyId and childId values Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
No EOL
4.5 KiB
TypeScript
144 lines
No EOL
4.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import Link from "next/link";
|
|
import { useFamily } from "../FamilyProvider";
|
|
|
|
interface Memory {
|
|
key: string;
|
|
url: string;
|
|
size: number;
|
|
lastModified: string;
|
|
}
|
|
|
|
export default function MemoriesPage() {
|
|
const { childId } = useFamily();
|
|
const [memories, setMemories] = useState<Memory[]>([]);
|
|
const [selected, setSelected] = useState<Memory | null>(null);
|
|
const [uploading, setUploading] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
const fileRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (childId) {
|
|
fetchMemories();
|
|
}
|
|
}, [childId]);
|
|
|
|
const fetchMemories = async () => {
|
|
if (!childId) return;
|
|
try {
|
|
const res = await fetch(`/api/upload?childId=${childId}`);
|
|
const data = await res.json();
|
|
setMemories(data.items || []);
|
|
} catch (err) {
|
|
console.error("Failed to fetch memories:", err);
|
|
}
|
|
};
|
|
|
|
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
setUploading(true);
|
|
setUploadProgress(0);
|
|
|
|
try {
|
|
// Get upload key
|
|
const res = await fetch("/api/upload", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ filename: file.name, contentType: file.type, childId }),
|
|
});
|
|
const data = await res.json();
|
|
|
|
if (data.error) {
|
|
alert("Error: " + data.error);
|
|
setUploading(false);
|
|
return;
|
|
}
|
|
|
|
const { key, publicUrl } = data;
|
|
|
|
// Upload through our server (no CORS issue)
|
|
const uploadRes = await fetch(`/api/upload?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(file.type)}`, {
|
|
method: "PUT",
|
|
body: file,
|
|
});
|
|
|
|
const uploadData = await uploadRes.json();
|
|
|
|
if (uploadData.success) {
|
|
setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]);
|
|
} else {
|
|
alert("Upload failed: " + uploadData.error);
|
|
}
|
|
} catch (err) {
|
|
console.error("Upload failed:", err);
|
|
alert("Error: " + err);
|
|
}
|
|
setUploading(false);
|
|
setUploadProgress(0);
|
|
if (fileRef.current) fileRef.current.value = "";
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
|
<div className="p-4 flex items-center gap-4">
|
|
<Link href="/menu" className="p-2">←</Link>
|
|
<h1 className="text-xl font-bold">Memories 📸</h1>
|
|
</div>
|
|
|
|
<div className="px-4">
|
|
{memories.length === 0 ? (
|
|
<div className="text-center py-20 text-gray-400">
|
|
<div className="text-6xl mb-4">📷</div>
|
|
<p>No memories yet</p>
|
|
<p className="text-sm">Tap + to add your first photo</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-3 gap-1">
|
|
{memories.map((mem) => (
|
|
<button
|
|
key={mem.key}
|
|
onClick={() => setSelected(mem)}
|
|
className="aspect-square bg-gray-200 dark:bg-gray-700 rounded-lg overflow-hidden"
|
|
>
|
|
<img src={mem.url} alt={mem.key} className="w-full h-full object-cover" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Upload Button */}
|
|
<div className="fixed bottom-4 right-4">
|
|
<label className="w-14 h-14 bg-rose-400 text-white rounded-full text-2xl shadow-lg flex items-center justify-center cursor-pointer">
|
|
{uploading ? (
|
|
<span className="text-sm">{uploadProgress}%</span>
|
|
) : (
|
|
<span>+</span>
|
|
)}
|
|
<input
|
|
ref={fileRef}
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleUpload}
|
|
className="hidden"
|
|
disabled={uploading}
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Modal */}
|
|
{selected && (
|
|
<div className="fixed inset-0 bg-black/90 flex items-center justify-center z-50" onClick={() => setSelected(null)}>
|
|
<div className="w-full h-full flex items-center justify-center p-4" onClick={e => e.stopPropagation()}>
|
|
<img src={selected.url} alt={selected.key} className="max-w-full max-h-full object-contain" />
|
|
</div>
|
|
<button onClick={() => setSelected(null)} className="absolute top-4 right-4 text-white text-xl p-2">✕</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |