tia/src/app/settings/page.tsx
Mannu fdd2a67f7a Fix hardcoded IDs and data fetching across all pages
- 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>
2026-05-11 00:32:31 +05:30

339 lines
No EOL
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTheme } from "../ThemeProvider";
import { useFamily } from "../FamilyProvider";
interface Member {
id: string;
userId: string;
role: string;
displayName: string;
name: string;
email: string;
}
interface Invite {
id: string;
email: string;
displayName: string;
role: string;
expiresAt: string;
}
export default function SettingsPage() {
const router = useRouter();
const { theme, mode, setMode } = useTheme();
const { tier, memberCount, familyId } = useFamily();
const [themeOpen, setThemeOpen] = useState(false);
const [inviteOpen, setInviteOpen] = useState(false);
const [membersOpen, setMembersOpen] = useState(false);
const [familyOpen, setFamilyOpen] = useState(false);
const [members, setMembers] = useState<Member[]>([]);
const [invites, setInvites] = useState<Invite[]>([]);
const [showAddInvite, setShowAddInvite] = useState(false);
const [inviteEmail, setInviteEmail] = useState("");
const [inviteRole, setInviteRole] = useState("caregiver");
const [inviteLoading, setInviteLoading] = useState(false);
const [familyName, setFamilyName] = useState("");
const [signingOut, setSigningOut] = useState(false);
// Check if can invite more members
const canInvite = tier === "pro" || memberCount < 2;
const handleSignOut = async () => {
if (!confirm("Are you sure you want to sign out?")) return;
setSigningOut(true);
try {
await fetch("/api/auth/signout", { method: "POST" });
router.push("/login");
} catch (err) {
console.error("Sign out failed:", err);
}
setSigningOut(false);
};
const themeOptions = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System" },
{ value: "time", label: "Time of Day" },
] as const;
useEffect(() => {
fetchInvites();
fetchMembers();
}, [familyId]);
const fetchMembers = async () => {
if (!familyId) return;
try {
const res = await fetch(`/api/family/members?familyId=${familyId}`);
const data = await res.json();
setMembers(data.members || []);
} catch (err) {
console.error("Failed to fetch members:", err);
}
};
const fetchInvites = async () => {
if (!familyId) return;
try {
const res = await fetch(`/api/invites?familyId=${familyId}`);
const data = await res.json();
setInvites(data.invites || []);
} catch (err) {
console.error("Failed to fetch invites:", err);
}
};
const sendInvite = async () => {
if (!inviteEmail || !familyId) return;
setInviteLoading(true);
try {
const res = await fetch("/api/invites", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
familyId: familyId,
email: inviteEmail,
role: inviteRole,
displayName: inviteEmail.split("@")[0],
}),
});
const data = await res.json();
if (data.success) {
setInviteEmail("");
fetchInvites();
} else {
alert(data.error);
}
} catch (err) {
console.error("Failed to send invite:", err);
}
setInviteLoading(false);
};
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">
<button onClick={() => router.back()} className="p-2"></button>
<h1 className="text-xl font-bold">Settings</h1>
</div>
<div className="px-4 space-y-3">
{/* Notifications */}
<Link href="/notifications" className="flex items-center justify-between p-4 bg-white dark:bg-gray-800 rounded-xl">
<div className="flex items-center gap-3">
<span className="text-xl">🔔</span>
<div className="font-medium">Notifications</div>
</div>
<span className="text-gray-400"></span>
</Link>
{/* Profile */}
<Link href="/profile" className="flex items-center justify-between p-4 bg-white dark:bg-gray-800 rounded-xl">
<div className="flex items-center gap-3">
<span className="text-xl">👤</span>
<div className="font-medium">Profile</div>
</div>
<span className="text-gray-400"></span>
</Link>
{/* Family - Collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<button
onClick={() => setFamilyOpen(!familyOpen)}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<span className="text-xl">🏠</span>
<div className="font-medium">Family Settings</div>
</div>
<span className={`text-gray-400 transition-transform ${familyOpen ? "rotate-180" : ""}`}></span>
</button>
{familyOpen && (
<div className="px-4 pb-4 space-y-3">
<input
type="text"
value={familyName}
onChange={(e) => setFamilyName(e.target.value)}
placeholder="Family name"
className="w-full p-2 border rounded-lg text-sm"
/>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-500">Plan: {tier}</span>
{tier === "free" && (
<button className="text-sm text-rose-500">Upgrade to Pro</button>
)}
</div>
</div>
)}
</div>
<Link href="/family" className="flex items-center justify-between p-4 bg-white dark:bg-gray-800 rounded-xl">
<div className="flex items-center gap-3">
<span className="text-xl">👨👩👧</span>
<div className="font-medium">Family</div>
</div>
<span className="text-gray-400"></span>
</Link>
{/* Invite Members - Collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<button
onClick={() => setInviteOpen(!inviteOpen)}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<span className="text-xl"></span>
<div className="font-medium">Invite Members</div>
{tier === "free" && (
<span className="text-xs px-2 py-0.5 bg-rose-100 text-rose-600 rounded-full">Free: {memberCount}/2</span>
)}
</div>
<span className={`text-gray-400 transition-transform ${inviteOpen ? "rotate-180" : ""}`}></span>
</button>
{inviteOpen && (
<div className="px-4 pb-4">
{/* Pro upgrade prompt */}
{tier === "free" && !canInvite && (
<div className="p-3 bg-rose-50 rounded-lg mb-3">
<p className="text-sm text-rose-600">Upgrade to Pro for unlimited family members</p>
</div>
)}
{/* Pending invites */}
{invites.length > 0 && (
<div className="mb-3">
<div className="text-sm text-gray-500 mb-2">Pending Invites</div>
{invites.map((invite) => (
<div key={invite.id} className="flex justify-between items-center p-2 bg-gray-50 rounded text-sm">
<span>{invite.email}</span>
<span className="text-gray-400">Pending</span>
</div>
))}
</div>
)}
{/* Add invite form */}
{canInvite && (
<div className="space-y-2">
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Email address"
className="w-full p-2 border rounded-lg text-sm"
/>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className="w-full p-2 border rounded-lg text-sm"
>
<option value="caregiver">Caregiver</option>
<option value="viewer">Viewer (read-only)</option>
</select>
<button
onClick={sendInvite}
disabled={inviteLoading || !inviteEmail}
className="w-full p-2 bg-rose-400 text-white rounded-lg text-sm disabled:opacity-50"
>
{inviteLoading ? "Sending..." : "Send Invite"}
</button>
</div>
)}
</div>
)}
</div>
{/* Family Members - Collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<button
onClick={() => setMembersOpen(!membersOpen)}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<span className="text-xl">👥</span>
<div className="font-medium">Family Members</div>
<span className="text-xs text-gray-400">({members.length})</span>
</div>
<span className={`text-gray-400 transition-transform ${membersOpen ? "rotate-180" : ""}`}></span>
</button>
{membersOpen && (
<div className="px-4 pb-4">
{members.length > 0 ? (
<div className="space-y-2">
{members.map((member) => (
<div key={member.id} className="flex items-center justify-between p-2 bg-gray-50 rounded">
<div>
<div className="font-medium text-sm">{member.displayName || member.name}</div>
<div className="text-xs text-gray-400">{member.email}</div>
</div>
<span className={`text-xs px-2 py-1 rounded ${member.role === "admin" ? "bg-rose-100 text-rose-600" : "bg-gray-100"}`}>
{member.role}
</span>
</div>
))}
</div>
) : (
<p className="text-gray-500 text-sm">No members yet</p>
)}
</div>
)}
</div>
{/* Theme - Collapsible */}
<div className="bg-white dark:bg-gray-800 rounded-xl overflow-hidden">
<button
onClick={() => setThemeOpen(!themeOpen)}
className="w-full flex items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<span className="text-xl">{theme === "dark" ? "🌙" : "☀️"}</span>
<div className="font-medium">Theme</div>
</div>
<span className={`text-gray-400 transition-transform ${themeOpen ? "rotate-180" : ""}`}></span>
</button>
{themeOpen && (
<div className="px-4 pb-4">
<div className="grid grid-cols-2 gap-2">
{themeOptions.map((opt) => (
<button
key={opt.value}
onClick={() => setMode(opt.value)}
className={`p-3 rounded-lg text-sm ${
mode === opt.value ? "bg-rose-400 text-white" : "bg-gray-100 dark:bg-gray-700"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
)}
</div>
{/* App Version */}
<div className="p-4 bg-white dark:bg-gray-800 rounded-xl mt-4">
<div className="font-medium">App Version</div>
<div className="text-sm text-gray-500">Tia v1.0.0</div>
</div>
{/* Sign Out */}
<button
onClick={handleSignOut}
disabled={signingOut}
className="w-full p-4 mt-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl font-medium disabled:opacity-50"
>
{signingOut ? "Signing out..." : "Sign Out"}
</button>
</div>
</div>
);
}