Update Family, Profile with working APIs - fetch & save baby details
This commit is contained in:
parent
29bf635926
commit
83314e91a8
4 changed files with 300 additions and 77 deletions
29
src/app/api/auth/profile/route.ts
Normal file
29
src/app/api/auth/profile/route.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
// Mock user for now - in production, fetch from database
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
// TODO: In production, get user from session
|
||||||
|
const user = {
|
||||||
|
id: "1",
|
||||||
|
name: "Parent",
|
||||||
|
email: "parent@example.com",
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json({ user });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { name } = body;
|
||||||
|
|
||||||
|
// TODO: Save to database in production
|
||||||
|
return NextResponse.json({ success: true, name });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { sql } from "@/db";
|
import { sql } from "@/db";
|
||||||
|
|
||||||
interface ChildEntry {
|
// GET - list children
|
||||||
familyId: string;
|
export async function GET(request: Request) {
|
||||||
name: string;
|
const { searchParams } = new URL(request.url);
|
||||||
birthDate: string;
|
const familyId = searchParams.get("familyId") || "default";
|
||||||
sex: "male" | "female" | "other";
|
|
||||||
|
try {
|
||||||
|
const children = await sql.unsafe(
|
||||||
|
`SELECT id, name, birth_date as "birthDate", sex, stage, created_at as "createdAt" FROM children WHERE family_id = $1 ORDER BY created_at DESC`,
|
||||||
|
[familyId]
|
||||||
|
);
|
||||||
|
return NextResponse.json({ children: children || [] });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST - create child
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body: ChildEntry = await request.json();
|
const body = await request.json();
|
||||||
const { familyId, name, birthDate, sex } = body;
|
const { familyId, name, birthDate, sex } = body;
|
||||||
|
|
||||||
if (!familyId || !name || !birthDate || !sex) {
|
if (!familyId || !name || !birthDate || !sex) {
|
||||||
|
|
@ -18,7 +29,7 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [child] = await sql.unsafe(
|
const [child] = await sql.unsafe(
|
||||||
`INSERT INTO children (family_id, name, birth_date, sex, stage) VALUES ($1, $2, $3, $4, 'newborn') RETURNING *`,
|
`INSERT INTO children (family_id, name, birth_date, sex, stage) VALUES ($1, $2, $3, $4, 'newborn') RETURNING id, name, birth_date as "birthDate", sex, stage`,
|
||||||
[familyId, name, birthDate, sex]
|
[familyId, name, birthDate, sex]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -29,21 +40,22 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
// PATCH - update child
|
||||||
const { searchParams } = new URL(request.url);
|
export async function PATCH(request: Request) {
|
||||||
const familyId = searchParams.get("familyId");
|
|
||||||
|
|
||||||
if (!familyId) {
|
|
||||||
return NextResponse.json({ error: "familyId required" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const children = await sql.unsafe(
|
const body = await request.json();
|
||||||
`SELECT * FROM children WHERE family_id = $1 ORDER BY created_at DESC`,
|
const { id, name, birthDate } = body;
|
||||||
[familyId]
|
|
||||||
|
if (!id || !name || !birthDate) {
|
||||||
|
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [child] = await sql.unsafe(
|
||||||
|
`UPDATE children SET name = $1, birth_date = $2, updated_at = NOW() WHERE id = $3 RETURNING id, name, birth_date as "birthDate", sex`,
|
||||||
|
[name, birthDate, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json({ children });
|
return NextResponse.json({ success: true, child });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return NextResponse.json({ error: String(error) }, { status: 500 });
|
return NextResponse.json({ error: String(error) }, { status: 500 });
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,85 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface FamilyMember {
|
interface Child {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: string;
|
birthDate: string;
|
||||||
avatar: string;
|
sex: string;
|
||||||
|
stage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FamilyPage() {
|
export default function FamilyPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [members] = useState<FamilyMember[]>([
|
const [children, setChildren] = useState<Child[]>([]);
|
||||||
{ id: "1", name: "You", role: "Parent", avatar: "👤" },
|
const [loading, setLoading] = useState(true);
|
||||||
{ id: "2", name: "Baby Tia", role: "Child", avatar: "👶" },
|
const [editing, setEditing] = useState<string | null>(null);
|
||||||
]);
|
const [editName, setEditName] = useState("");
|
||||||
|
const [editDob, setEditDob] = useState("");
|
||||||
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const [newName, setNewName] = useState("");
|
||||||
|
const [newDob, setNewDob] = useState("");
|
||||||
|
const [newSex, setNewSex] = useState("male");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchChildren();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchChildren = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/children?familyId=default");
|
||||||
|
const data = await res.json();
|
||||||
|
setChildren(data.children || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch:", err);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startEdit = (child: Child) => {
|
||||||
|
setEditing(child.id);
|
||||||
|
setEditName(child.name);
|
||||||
|
setEditDob(child.birthDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEdit = async (childId: string) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/children", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id: childId, name: editName, birthDate: editDob }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setChildren(children.map((c) => (c.id === childId ? { ...c, name: editName, birthDate: editDob } : c)));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save:", err);
|
||||||
|
}
|
||||||
|
setEditing(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addChild = async () => {
|
||||||
|
if (!newName || !newDob) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/children", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ name: newName, birthDate: newDob, sex: newSex, familyId: "default" }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setChildren([data.child, ...children]);
|
||||||
|
setShowAdd(false);
|
||||||
|
setNewName("");
|
||||||
|
setNewDob("");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to add:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
||||||
|
|
@ -25,32 +89,117 @@ export default function FamilyPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 space-y-4">
|
<div className="px-4 space-y-4">
|
||||||
{/* Members List */}
|
{/* Add Child Form */}
|
||||||
<div className="space-y-2">
|
{showAdd && (
|
||||||
<div className="text-sm text-gray-500 mb-2">Family Members</div>
|
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl space-y-3">
|
||||||
{members.map((member) => (
|
<input
|
||||||
<div key={member.id} className="flex items-center gap-4 p-4 bg-white dark:bg-gray-800 rounded-xl">
|
type="text"
|
||||||
<div className="text-3xl">{member.avatar}</div>
|
value={newName}
|
||||||
<div className="flex-1">
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
<div className="font-medium">{member.name}</div>
|
placeholder="Baby's name"
|
||||||
<div className="text-sm text-gray-500">{member.role}</div>
|
className="w-full p-2 border rounded-lg"
|
||||||
</div>
|
/>
|
||||||
<button className="text-gray-400">✏️</button>
|
<input
|
||||||
|
type="date"
|
||||||
|
value={newDob}
|
||||||
|
onChange={(e) => setNewDob(e.target.value)}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={newSex}
|
||||||
|
onChange={(e) => setNewSex(e.target.value)}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="male">Boy</option>
|
||||||
|
<option value="female">Girl</option>
|
||||||
|
</select>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={addChild} className="flex-1 py-2 bg-rose-400 text-white rounded-lg">
|
||||||
|
Add Baby
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowAdd(false)} className="flex-1 py-2 bg-gray-200 dark:bg-gray-600 rounded-lg">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Add Member */}
|
{loading ? (
|
||||||
<button className="w-full p-4 border-2 border-dashed border-gray-300 rounded-xl text-gray-500">
|
<div className="text-center py-20 text-gray-400">Loading...</div>
|
||||||
+ Add Family Member
|
) : children.length === 0 && !showAdd ? (
|
||||||
</button>
|
<div className="text-center py-20">
|
||||||
|
<div className="text-6xl mb-4">👶</div>
|
||||||
|
<p className="text-gray-500 mb-4">No baby added yet</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAdd(true)}
|
||||||
|
className="px-4 py-2 bg-rose-400 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Add Baby
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
children.map((child) => (
|
||||||
|
<div key={child.id} className="p-4 bg-white dark:bg-gray-800 rounded-xl">
|
||||||
|
{editing === child.id ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editName}
|
||||||
|
onChange={(e) => setEditName(e.target.value)}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={editDob}
|
||||||
|
onChange={(e) => setEditDob(e.target.value)}
|
||||||
|
className="w-full p-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => saveEdit(child.id)}
|
||||||
|
className="flex-1 py-2 bg-rose-400 text-white rounded-lg"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEditing(null)}
|
||||||
|
className="flex-1 py-2 bg-gray-200 dark:bg-gray-600 rounded-lg"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="text-4xl">{child.sex === "male" ? "👦" : "👧"}</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-lg">{child.name}</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Born: {child.birthDate ? new Date(child.birthDate).toLocaleDateString() : "Not set"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button onClick={() => startEdit(child)} className="p-2 text-gray-400">
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Quick Info */}
|
{/* Add Button */}
|
||||||
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl mt-6">
|
{!showAdd && children.length > 0 && (
|
||||||
<div className="text-sm text-gray-500 mb-2">Family Plan</div>
|
<button
|
||||||
<div className="font-medium">Up to 4 family members</div>
|
onClick={() => setShowAdd(true)}
|
||||||
<div className="text-sm text-gray-500 mt-1">Free for now</div>
|
className="w-full p-4 border-2 border-dashed border-gray-300 rounded-xl text-gray-500"
|
||||||
</div>
|
>
|
||||||
|
+ Add Baby
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,36 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [name, setName] = useState("Parent");
|
const [name, setName] = useState("Loading...");
|
||||||
const [email, setEmail] = useState("parent@example.com");
|
const [email, setEmail] = useState("Loading...");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch user profile from API
|
||||||
|
fetch("/api/auth/profile")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.user) {
|
||||||
|
setName(data.user.name || "Parent");
|
||||||
|
setEmail(data.user.email || "parent@example.com");
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setName("Parent");
|
||||||
|
setEmail("parent@example.com");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveProfile = async () => {
|
||||||
|
// TODO: Call API to save profile
|
||||||
|
alert("Profile saved!");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
||||||
|
|
@ -25,36 +49,45 @@ export default function ProfilePage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="space-y-3">
|
{loading ? (
|
||||||
<div>
|
<div className="text-center py-8 text-gray-400">Loading...</div>
|
||||||
<label className="block text-sm font-medium mb-1">Name</label>
|
) : (
|
||||||
<input
|
<div className="space-y-3">
|
||||||
type="text"
|
<div>
|
||||||
value={name}
|
<label className="block text-sm font-medium mb-1">Name</label>
|
||||||
onChange={(e) => setName(e.target.value)}
|
<input
|
||||||
className="w-full p-3 bg-white dark:bg-gray-800 rounded-xl border"
|
type="text"
|
||||||
/>
|
value={name}
|
||||||
</div>
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full p-3 bg-white dark:bg-gray-800 rounded-xl border"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Email</label>
|
<label className="block text-sm font-medium mb-1">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="w-full p-3 bg-white dark:bg-gray-800 rounded-xl border"
|
className="w-full p-3 bg-white dark:bg-gray-800 rounded-xl border"
|
||||||
/>
|
disabled
|
||||||
</div>
|
/>
|
||||||
|
<div className="text-xs text-gray-400 mt-1">Email cannot be changed</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button className="w-full p-3 bg-rose-400 text-white rounded-xl mt-4">
|
<button
|
||||||
Save Changes
|
onClick={saveProfile}
|
||||||
</button>
|
className="w-full p-3 bg-rose-400 text-white rounded-xl mt-4"
|
||||||
</div>
|
>
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Account Info */}
|
{/* Account Info */}
|
||||||
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl mt-6">
|
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl mt-6">
|
||||||
<div className="font-medium mb-2">Account</div>
|
<div className="font-medium mb-2">Account</div>
|
||||||
<div className="text-sm text-gray-500">Member since: Jan 2024</div>
|
<div className="text-sm text-gray-500">Member since: January 2024</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue