Files
falah-mobile/src/app/profile/page.tsx
T
root 5483dd291e Initial Falah Mobile rebuild — all 7 blocks complete
- Auth system (login/register/profile)
- Nur AI chat with persona system
- Souq marketplace with FLH economy
- Forum community with Shariah moderation
- Wallet & FLH top-up
- Premium/Pro tier upgrade with Polar.sh
- Halal Monitor with map & bookmarks
- Home dashboard with daily verse & streaks
- Health endpoints

Next.js 16.2.7, Prisma v5 SQLite, JWT auth, Tailwind CSS v4
2026-06-15 09:28:22 +02:00

344 lines
12 KiB
TypeScript

"use client";
import { useState, useEffect, FormEvent } from "react";
import { useAuth } from "@/lib/AuthContext";
import { useRouter } from "next/navigation";
import { getUserTier } from "@/lib/tiers";
import { PERSONAS } from "@/lib/personas";
import {
User,
Crown,
Zap,
LogOut,
ChevronRight,
Save,
Sparkles,
Shield,
} from "lucide-react";
const VALID_MADHABS = ["Hanafi", "Maliki", "Shafi'i", "Hanbali"];
export default function ProfilePage() {
const { user, token, loading, logout, refreshUser } = useAuth();
const router = useRouter();
// Editable fields
const [name, setName] = useState("");
const [preferredName, setPreferredName] = useState("");
const [coachPersona, setCoachPersona] = useState("");
const [madhab, setMadhab] = useState("");
const [coachingGoals, setCoachingGoals] = useState("");
const [saving, setSaving] = useState(false);
const [saveMessage, setSaveMessage] = useState("");
useEffect(() => {
if (!loading && !token) router.push("/login");
}, [loading, token, router]);
useEffect(() => {
if (user) {
setName(user.name || "");
setPreferredName(user.preferredName || "");
setCoachPersona(user.coachPersona || "");
setMadhab(user.madhab || "");
setCoachingGoals(user.coachingGoals || "");
}
}, [user]);
const tier = user ? getUserTier(user.isPremium, user.isPro) : "free";
const tierColors = {
free: { bg: "bg-gray-900/50", text: "text-gray-400", border: "border-gray-700/50", icon: User },
premium: { bg: "bg-[#D4AF37]/10", text: "text-[#D4AF37]", border: "border-[#D4AF37]/30", icon: Sparkles },
pro: { bg: "bg-purple-900/20", text: "text-purple-400", border: "border-purple-500/30", icon: Crown },
};
const tc = tierColors[tier];
const handleSave = async (e: FormEvent) => {
e.preventDefault();
if (!token) return;
setSaving(true);
setSaveMessage("");
try {
const res = await fetch("/api/auth/profile", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
name: name.trim() || undefined,
preferredName: preferredName.trim() || undefined,
coachPersona: coachPersona || undefined,
madhab: madhab || undefined,
coachingGoals: coachingGoals.trim() || undefined,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Save failed");
}
setSaveMessage("Profile updated successfully");
await refreshUser();
setTimeout(() => setSaveMessage(""), 3000);
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : "Failed to save profile";
setSaveMessage(message);
} finally {
setSaving(false);
}
};
const handleLogout = () => {
logout();
router.push("/login");
};
if (loading) {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
);
}
if (!user) return null;
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
{/* Header */}
<div className="px-4 pt-6 pb-2">
<h1 className="text-xl font-bold text-white">Profile</h1>
</div>
<div className="px-4 space-y-5 animate-fade-in">
{/* User Card */}
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
<div className="flex items-center gap-4">
<div className="w-14 h-14 rounded-full bg-[#D4AF37]/15 flex items-center justify-center">
<User size={24} className="text-[#D4AF37]" />
</div>
<div className="flex-1">
<h2 className="text-lg font-bold text-white">
{user.preferredName || user.name}
</h2>
<p className="text-sm text-gray-500">{user.email}</p>
</div>
</div>
{/* Tier Badge */}
<div
className={`mt-4 flex items-center justify-between ${tc.bg} ${tc.border} border rounded-xl px-4 py-2.5`}
>
<div className="flex items-center gap-2">
<tc.icon size={16} className={tc.text} />
<span className={`text-sm font-semibold ${tc.text}`}>
{tier === "free"
? "Free"
: tier === "premium"
? "Premium"
: "Pro"}
</span>
</div>
<div className="flex items-center gap-1.5">
<Zap size={14} className="text-gray-600" />
<span className="text-sm text-gray-400">
{tier === "pro"
? "Unlimited"
: `${user.dailyMsgCount}/10 AI today`}
</span>
</div>
</div>
{/* FLH Balance */}
<div className="mt-3 flex items-center justify-between bg-[#1a1a24] rounded-xl px-4 py-2.5">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-400">
FLH Balance
</span>
</div>
<span className="text-lg font-bold text-[#D4AF37]">
{user.flhBalance.toLocaleString()}
</span>
</div>
</div>
{/* Edit Profile Form */}
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
<h3 className="text-sm font-semibold text-white mb-4">
Edit Profile
</h3>
{saveMessage && (
<div
className={`mb-4 p-3 rounded-xl text-sm text-center ${
saveMessage === "Profile updated successfully"
? "bg-emerald-900/20 border border-emerald-800/40 text-emerald-400"
: "bg-red-900/20 border border-red-800/40 text-red-400"
}`}
>
{saveMessage}
</div>
)}
<form onSubmit={handleSave} className="space-y-4">
{/* Name */}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider">
Name
</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition"
/>
</div>
{/* Preferred Name */}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider">
Preferred Name
</label>
<input
type="text"
value={preferredName}
onChange={(e) => setPreferredName(e.target.value)}
placeholder="How Nur AI should address you"
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition"
/>
</div>
{/* Coach Persona */}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider">
Coach Persona
</label>
<select
value={coachPersona}
onChange={(e) => setCoachPersona(e.target.value)}
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition appearance-none"
>
<option value="">Default</option>
{PERSONAS.map((p) => (
<option key={p.id} value={p.id}>
{p.icon} {p.name}
</option>
))}
</select>
</div>
{/* Madhab */}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider">
Madhab (School of Thought)
</label>
<select
value={madhab}
onChange={(e) => setMadhab(e.target.value)}
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition appearance-none"
>
<option value="">Not specified</option>
{VALID_MADHABS.map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
</div>
{/* Coaching Goals */}
<div>
<label className="block text-xs font-medium text-gray-500 mb-1.5 uppercase tracking-wider">
Coaching Goals
</label>
<textarea
value={coachingGoals}
onChange={(e) => setCoachingGoals(e.target.value)}
placeholder="What would you like to work on?"
rows={3}
className="w-full bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 text-white placeholder-gray-600 focus:outline-none focus:border-[#D4AF37]/50 focus:ring-1 focus:ring-[#D4AF37]/30 transition resize-none"
/>
</div>
<button
type="submit"
disabled={saving}
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl bg-[#D4AF37] text-[#0a0a0f] font-semibold text-sm active:opacity-80 disabled:opacity-50 transition"
>
{saving ? (
<>
<span className="w-4 h-4 rounded-full border-2 border-[#0a0a0f]/30 border-t-[#0a0a0f] animate-spin" />
Saving...
</>
) : (
<>
<Save size={16} />
Save Changes
</>
)}
</button>
</form>
</div>
{/* Account Info */}
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
<h3 className="text-sm font-semibold text-white mb-3">
Account Details
</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-500">Member since</span>
<span className="text-gray-300">
{new Date(user.createdAt ?? "").toLocaleDateString("en-MY", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
</div>
{user.trialEndsAt && (
<div className="flex justify-between">
<span className="text-gray-500">Trial ends</span>
<span className="text-gray-300">
{new Date(user.trialEndsAt).toLocaleDateString("en-MY", {
year: "numeric",
month: "long",
day: "numeric",
})}
</span>
</div>
)}
{user.madhab && (
<div className="flex justify-between">
<span className="text-gray-500">Madhab</span>
<span className="text-gray-300">{user.madhab}</span>
</div>
)}
</div>
</div>
{/* Logout */}
<button
onClick={handleLogout}
className="w-full flex items-center justify-between py-3.5 px-5 rounded-xl bg-red-900/10 border border-red-800/30 text-red-400 active:bg-red-900/20 transition"
>
<div className="flex items-center gap-2">
<LogOut size={16} />
<span className="text-sm font-medium">Sign Out</span>
</div>
<ChevronRight size={16} />
</button>
<div className="h-4" />
</div>
</div>
);
}