feat: implement Phase 5 Nur screen and Phase 6 Profile screen

Nur screen:
- Header with gold-purple avatar, online status, Home button
- Mood check-in with 5 emojis (gold border on selected)
- Chat with gold gradient user bubbles, purple AI bubbles
- Slash commands: /quran, /hadith, /dua, /prayer
- Input bar with gold-purple send button
- Persona switcher: NurBuddy, Ghazali, Ibn Abbas, Rabi'a

Profile screen:
- Avatar with gold border, name, email, Premium + Lvl 7 badges
- FLH Wallet card with balance display
- 2x2 quick action grid (Souq, Waqf, Forum, Halal Monitor)
- Settings list with streak, notifications, appearance, upgrade
- Sign out with red button
This commit is contained in:
FalahMobile
2026-07-03 11:44:49 +08:00
parent caa3f794b5
commit 9f33038c37
3 changed files with 355 additions and 592 deletions
+182 -384
View File
@@ -1,439 +1,237 @@
'use client' 'use client'
import { useState, useEffect, useRef } from 'react' import { useState, useRef, useEffect } from 'react'
import { Send, Bot, Settings, User, Sparkles, BookOpen, Target, X, Crown, Scroll, Heart, Lock, ChevronDown, ChevronUp, BookMarked, ArrowLeft } from 'lucide-react' import Link from 'next/link'
import { useAuth } from '@/lib/AuthContext' import { House, Settings, ArrowUp } from 'lucide-react'
import { SCHOLAR_PERSONAS, PERSONA_COLORS } from '@/lib/personas'
import type { CoachPersona } from '@/lib/personas'
import { useRouter } from 'next/navigation'
const PERSONA_ICONS: Record<CoachPersona, typeof Bot> = { /* ─── Types & Data ───────────────────────────────────────────── */
nurbuddy: Bot,
ghazali: Scroll,
ibnabbas: Crown,
rabia: Heart,
}
const PREMIUM_PERSONAS: CoachPersona[] = ['ghazali', 'ibnabbas', 'rabia'] const MOODS = [
const FREE_MSG_LIMIT = 10 { emoji: '😊', label: 'Happy' },
{ emoji: '😌', label: 'Peaceful' },
{ emoji: '😢', label: 'Sad' },
{ emoji: '😡', label: 'Angry' },
{ emoji: '😴', label: 'Tired' },
]
interface DailyVerse { const SLASH_COMMANDS = ['/quran', '/hadith', '/dua', '/prayer']
surah: string
ayah: string const PERSONAS = [
arabic: string { id: 'nurbuddy', name: 'NurBuddy' },
translation: string { id: 'ghazali', name: 'Ghazali' },
reflection: string { id: 'ibnabbas', name: 'Ibn Abbas' },
source: string { id: 'rabia', name: "Rabi'a" },
} ]
const EXAMPLE_MESSAGES: { role: 'user' | 'ai'; content: string }[] = [
{
role: 'user',
content: 'Assalamualaikum! Can you tell me about the importance of Fajr prayer?',
},
{
role: 'ai',
content:
"Waalaikumsalam! 🌅 Fajr holds immense blessings. The Prophet ﷺ said: \"Whoever prays the Fajr prayer, they are under Allah's protection.\" (Muslim). It's a time when angels witness your prayer, and starting your day with Fajr brings barakah that carries through the entire day. Would you like a practical tip to help with waking up for Fajr?",
},
{
role: 'user',
content: "Yes please! I struggle with waking up on time.",
},
{
role: 'ai',
content:
'Here are 3 tips that work, by the will of Allah:\n\n1️⃣ Sleep with wudu — the Prophet ﷺ recommended it and it makes waking easier.\n2️⃣ Set your intention (niyyah) before sleeping — tell yourself "I will wake for Fajr."\n3️⃣ Use the Qaylulah (afternoon nap) — a short 20-min rest before Dhuhr helps with the early morning.\n\nStart with one and build the habit. Allah sees your effort! 🤲',
},
]
/* ─── Component ──────────────────────────────────────────────── */
export default function NurPage() { export default function NurPage() {
const { user, token } = useAuth()
const router = useRouter()
const [messages, setMessages] = useState<{ role: string; content: string; time?: string; metadata?: any }[]>([])
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [loading, setLoading] = useState(false) const [selectedMood, setSelectedMood] = useState<number | null>(null)
const [showSettings, setShowSettings] = useState(false) const [messages] = useState(EXAMPLE_MESSAGES)
const [actionItems, setActionItems] = useState<string[]>([]) const [activePersona, setActivePersona] = useState('nurbuddy')
const [dailyMsgCount, setDailyMsgCount] = useState<number | null>(null)
const [limitReached, setLimitReached] = useState(false)
const [showVerse, setShowVerse] = useState(false)
const [dailyVerse, setDailyVerse] = useState<DailyVerse | null>(null)
const [profile, setProfile] = useState({
preferredName: user?.preferredName || '',
experienceLevel: user?.experienceLevel || 'new',
madhab: user?.madhab || 'unspecified',
coachPersona: (user?.coachPersona as CoachPersona) || 'nurbuddy',
})
const [savingProfile, setSavingProfile] = useState(false)
const chatEnd = useRef<HTMLDivElement>(null) const chatEnd = useRef<HTMLDivElement>(null)
const [dailyLoading, setDailyLoading] = useState(false) const scrollRef = useRef<HTMLDivElement>(null)
const dailyChecked = useRef(false)
const currentPersona = SCHOLAR_PERSONAS[profile.coachPersona] || SCHOLAR_PERSONAS.nurbuddy
const PersonaIcon = PERSONA_ICONS[profile.coachPersona] || Bot
const isPremiumUser = user?.isPremium || user?.isPro
// Auto-scroll to bottom on mount
useEffect(() => { useEffect(() => {
if (!user) return chatEnd.current?.scrollIntoView({ behavior: 'auto' })
setProfile(p => ({
...p,
preferredName: user.preferredName || '',
experienceLevel: user.experienceLevel || 'new',
madhab: user.madhab || 'unspecified',
coachPersona: (user.coachPersona as CoachPersona) || 'nurbuddy',
}))
const loadHistory = async () => {
try {
const res = await fetch(`/api/chat/history/${user.id}`, {
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
})
const data = await res.json()
if (data.history?.length > 0) {
setMessages(data.history.map((h: any) => ({
role: h.role,
content: h.content,
time: new Date(h.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
metadata: h.metadata ? JSON.parse(h.metadata) : undefined,
})))
const items: string[] = []
for (const h of data.history) {
if (h.metadata) {
try { const meta = JSON.parse(h.metadata); if (meta.actionItems) items.push(...meta.actionItems) } catch { }
}
}
setActionItems(items.slice(-8))
}
if (!dailyChecked.current) {
dailyChecked.current = true
checkDailyGreeting()
}
} catch { }
}
loadHistory()
}, [user, token])
useEffect(() => {
fetch('/api/daily/verse').then(r => r.json()).then(d => setDailyVerse(d)).catch(() => { })
}, []) }, [])
const checkDailyGreeting = async () => { const handleSend = () => {
if (!user || !token) return if (!input.trim()) return
setDailyLoading(true) // Placeholder — will integrate with OpenCode API later
try { setInput('')
const res = await fetch('/api/chat/daily', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ userId: user.id }),
})
const data = await res.json()
if (data.needsGreeting && data.message) {
setMessages(prev => [...prev, {
role: 'assistant',
content: data.message,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
}])
}
} catch { }
setDailyLoading(false)
}
useEffect(() => { chatEnd.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages])
const handleSend = async () => {
if (!input.trim() || loading || !user) return
const msg = input; setInput('')
setMessages(prev => [...prev, { role: 'user', content: msg, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }])
setLoading(true)
try {
const res = await fetch('/api/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': `Bearer ${token}` } : {}) },
body: JSON.stringify({ userId: user.id, message: msg }),
})
const data = await res.json()
if (res.status === 429 && data.error === 'daily_limit_reached') {
setLimitReached(true); setDailyMsgCount(data.used)
setMessages(prev => prev.slice(0, -1)); setInput(msg)
} else if (res.ok) {
setMessages(prev => [...prev, { role: 'assistant', content: data.response, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), metadata: data.metadata }])
if (data.metadata?.actionItems) setActionItems(prev => [...prev, ...data.metadata.actionItems].slice(-8))
if (data.userContext?.dailyMsgCount !== null && data.userContext?.dailyMsgCount !== undefined) setDailyMsgCount(data.userContext.dailyMsgCount)
} else {
setMessages(prev => [...prev, { role: 'assistant', content: data.error || 'Something went wrong. Please try again.', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }])
}
} catch {
setMessages(prev => [...prev, { role: 'assistant', content: 'I\'m having trouble connecting right now.', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }])
}
setLoading(false)
}
const handleSaveProfile = async () => {
if (!token) return
setSavingProfile(true)
try {
const res = await fetch('/api/auth/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(profile),
})
if (res.status === 403) {
const data = await res.json()
if (data.error === 'premium_required') { router.push('/upgrade?feature=scholar_personas'); return }
}
if (res.ok) {
setShowSettings(false)
const meRes = await fetch('/api/auth/me', { headers: { 'Authorization': `Bearer ${token}` } })
const meData = await meRes.json()
if (meData.user) localStorage.setItem('flh_user', JSON.stringify(meData.user))
}
} catch { }
setSavingProfile(false)
}
if (!user) {
return (
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#f5f4f0] px-6 pb-24">
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center mb-6 shadow-lg">
<Bot size={36} className="text-white" />
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Meet Nur</h2>
<p className="text-gray-500 text-sm text-center mb-8">Your personal Islamic guide. Sign in to start your journey.</p>
<button onClick={() => router.push('/login')}
className="w-full max-w-xs bg-emerald-600 text-white py-4 rounded-2xl font-bold text-base active:scale-[0.97] transition-all">
Sign In
</button>
</div>
)
} }
return ( return (
<div className="flex flex-col bg-[#f5f4f0]" style={{ height: 'calc(100dvh - 64px)' }}> <div className="min-h-dvh bg-[#0a0a0f] flex flex-col pb-32 select-none">
{/* ═══ Header ═══ */}
{/* Header */} <div className="shrink-0 flex items-center gap-3 px-5 pt-14 pb-3">
<div className="shrink-0 flex items-center gap-3 px-4 py-3 border-b border-gray-100 bg-white"> {/* Avatar */}
<div className="w-10 h-10 rounded-2xl flex items-center justify-center bg-violet-50 border border-violet-100"> <div className="w-14 h-14 rounded-full gold-purple-gradient flex items-center justify-center shrink-0 shadow-lg shadow-purple-500/10">
<PersonaIcon size={20} className={PERSONA_COLORS[profile.coachPersona]} /> <span className="text-xl font-bold text-white">N</span>
</div> </div>
{/* Name + online dot */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h1 className="font-bold text-gray-900 text-sm leading-tight truncate">{currentPersona.name}</h1> <div className="flex items-center gap-2">
<p className="text-gray-400 text-xs truncate">{currentPersona.title}</p> <h1 className="text-white font-bold text-base">NurBuddy</h1>
<span className="w-2 h-2 rounded-full bg-[#10B981] shrink-0 shadow-[0_0_6px_rgba(16,185,129,0.6)]" />
<span className="text-[10px] text-[#10B981] font-medium">Online</span>
</div>
<p className="text-[#999] text-xs truncate">Your Islamic AI Companion</p>
</div>
{/* Actions */}
<div className="flex items-center gap-1.5">
<Link
href="/home"
className="w-9 h-9 rounded-xl bg-[#111118] border border-[#222] flex items-center justify-center active:scale-90 transition-transform"
>
<House size={17} className="text-[#D4AF37]" />
</Link>
<button className="w-9 h-9 rounded-xl bg-[#111118] border border-[#222] flex items-center justify-center active:scale-90 transition-transform">
<Settings size={16} className="text-[#666]" />
</button>
</div> </div>
<button
onClick={() => setShowSettings(true)}
className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-100 text-gray-500 active:bg-gray-200 transition"
>
<Settings size={17} />
</button>
</div> </div>
{/* Daily Verse Banner */} {/* ═══ Mood Check-in ═══ */}
{dailyVerse && ( <div className="shrink-0 px-5 pb-3">
<div className="shrink-0 border-b border-amber-100 bg-amber-50"> <div className="bg-[#111118] rounded-2xl border border-[#222] px-4 py-3">
<button <p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-2.5">
onClick={() => setShowVerse(v => !v)} How are you feeling?
className="w-full flex items-center gap-2 px-4 py-2.5 text-left" </p>
> <div className="flex justify-between">
<BookMarked size={13} className="text-amber-600 shrink-0" /> {MOODS.map((mood, idx) => (
<span className="text-xs font-semibold text-amber-700">Today&apos;s Verse</span> <button
<span className="text-xs text-amber-500 truncate flex-1">{dailyVerse.surah} {dailyVerse.ayah}</span> key={idx}
{showVerse ? <ChevronUp size={13} className="text-amber-400 shrink-0" /> : <ChevronDown size={13} className="text-amber-400 shrink-0" />} onClick={() => setSelectedMood(idx === selectedMood ? null : idx)}
</button> className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl active:scale-90 transition-all ${
{showVerse && ( selectedMood === idx
<div className="px-4 pb-4 space-y-2"> ? 'border-2 border-[#D4AF37] bg-[#D4AF37]/10 shadow-[0_0_12px_rgba(212,175,55,0.15)]'
<p className="text-right text-lg leading-loose text-gray-900">{dailyVerse.arabic}</p> : 'border-2 border-transparent opacity-60 hover:opacity-100'
<p className="text-xs text-gray-600 italic">&ldquo;{dailyVerse.translation}&rdquo;</p> }`}
<p className="text-xs text-gray-500 leading-relaxed">{dailyVerse.reflection}</p> >
</div> {mood.emoji}
)} </button>
))}
</div>
</div> </div>
)} </div>
{/* Messages */} {/* ═══ Chat Messages ═══ */}
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3 chat-scroll"> <div
{messages.length === 0 && !dailyLoading ? ( ref={scrollRef}
<div className="flex flex-col items-center justify-center h-full pb-8 space-y-3"> className="flex-1 overflow-y-auto px-5 py-2 space-y-4 chat-scroll"
<div className="w-16 h-16 rounded-3xl bg-violet-50 border border-violet-100 flex items-center justify-center"> >
<PersonaIcon size={28} className={PERSONA_COLORS[profile.coachPersona]} /> {messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full pb-12">
<div className="w-16 h-16 rounded-full gold-purple-gradient flex items-center justify-center mb-4 opacity-60">
<span className="text-2xl">🧠</span>
</div> </div>
<p className="text-gray-500 text-sm text-center max-w-xs px-4">{currentPersona.greeting}</p> <p className="text-[#666] text-sm text-center max-w-xs">
Start a conversation with NurBuddy your AI Islamic companion.
</p>
</div> </div>
) : ( ) : (
messages.map((m, i) => ( messages.map((msg, i) => (
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}> <div
{m.role === 'assistant' && ( key={i}
<div className="w-7 h-7 rounded-xl bg-violet-50 flex items-center justify-center mr-2 shrink-0 mt-0.5"> className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'} items-end gap-2`}
<PersonaIcon size={13} className={PERSONA_COLORS[profile.coachPersona]} /> >
{/* AI avatar */}
{msg.role === 'ai' && (
<div className="w-7 h-7 rounded-full bg-[#1a1a2e] border border-[rgba(167,139,250,0.2)] flex items-center justify-center shrink-0 mb-0.5">
<span className="text-[10px]">🧠</span>
</div> </div>
)} )}
<div className={`max-w-[78%] rounded-2xl px-4 py-3 ${
m.role === 'user' {/* Bubble */}
? 'bg-emerald-600 text-white rounded-br-sm' <div
: 'bg-white text-gray-900 rounded-bl-sm shadow-sm border border-gray-100' className={`max-w-[80%] px-4 py-3 ${
}`}> msg.role === 'user'
<p className="text-sm leading-relaxed whitespace-pre-wrap">{m.content}</p> ? 'gold-gradient text-[#0a0a0f] rounded-2xl rounded-br-sm'
{m.time && <p className={`text-[10px] mt-1 ${m.role === 'user' ? 'text-white/60' : 'text-gray-400'}`}>{m.time}</p>} : 'bg-[#1a1a2e] border border-[rgba(167,139,250,0.2)] text-white rounded-2xl rounded-bl-sm'
}`}
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.content}</p>
</div> </div>
{/* User avatar placeholder */}
{msg.role === 'user' && (
<div className="w-7 h-7 rounded-full bg-[#D4AF37]/20 border border-[#D4AF37]/30 flex items-center justify-center shrink-0 mb-0.5">
<span className="text-[10px] font-bold text-[#D4AF37]">U</span>
</div>
)}
</div> </div>
)) ))
)} )}
{(loading || dailyLoading) && (
<div className="flex justify-start">
<div className="w-7 h-7 rounded-xl bg-violet-50 flex items-center justify-center mr-2 shrink-0 mt-0.5">
<PersonaIcon size={13} className={PERSONA_COLORS[profile.coachPersona]} />
</div>
<div className="bg-white rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm border border-gray-100">
<div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
<div ref={chatEnd} /> <div ref={chatEnd} />
</div> </div>
{/* Limit reached banner */} {/* ═══ Slash Commands ═══ */}
{limitReached && ( <div className="shrink-0 px-5 pb-2">
<div className="shrink-0 mx-4 mb-2 bg-amber-50 border border-amber-200 rounded-2xl px-4 py-3 flex items-center justify-between gap-3"> <div className="flex gap-2 overflow-x-auto scrollbar-none -mx-5 px-5">
<p className="text-xs text-gray-700">Used all {FREE_MSG_LIMIT} free messages today.</p> {SLASH_COMMANDS.map((cmd) => (
<a href="/upgrade" className="shrink-0 bg-emerald-600 text-white px-3 py-1.5 rounded-xl text-xs font-bold"> <button
Upgrade key={cmd}
</a> onClick={() => setInput(cmd + ' ')}
className="shrink-0 bg-[#111118] border border-[#222] rounded-full px-4 py-1.5 active:scale-95 transition-transform"
>
<span className="text-xs text-[#D4AF37] font-medium">{cmd}</span>
</button>
))}
</div> </div>
)} </div>
{/* Input */} {/* ═══ Input Bar (fixed at bottom above nav) ═══ */}
<div className="shrink-0 border-t border-gray-100 px-4 pt-3 pb-3 bg-white"> <div className="shrink-0 px-5 pb-1">
{!isPremiumUser && dailyMsgCount !== null && !limitReached && ( <div className="flex items-center gap-2 bg-[#111118] border border-[#222] rounded-[28px] px-4 py-2">
<p className="text-[11px] text-gray-400 text-right mb-1.5">
{dailyMsgCount} / {FREE_MSG_LIMIT} today
</p>
)}
<div className="flex items-end gap-2">
<input <input
type="text" type="text"
placeholder={limitReached ? 'Upgrade to continue...' : `Ask ${currentPersona.name}...`}
value={input} value={input}
onChange={e => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }} onKeyDown={(e) => {
disabled={limitReached} if (e.key === 'Enter' && !e.shiftKey) {
className="flex-1 bg-gray-50 border border-gray-200 rounded-2xl px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none disabled:opacity-40 transition" e.preventDefault()
handleSend()
}
}}
placeholder="Ask NurBuddy anything..."
className="flex-1 bg-transparent text-sm text-white placeholder-[#666] outline-none border-none"
/> />
<button <button
onClick={handleSend} onClick={handleSend}
disabled={loading || !input.trim() || limitReached} disabled={!input.trim()}
className="w-11 h-11 flex items-center justify-center bg-emerald-600 rounded-2xl disabled:opacity-30 active:scale-95 transition-all shrink-0" className="w-9 h-9 rounded-full gold-purple-gradient flex items-center justify-center disabled:opacity-30 active:scale-90 transition-all shrink-0"
> >
<Send size={18} className="text-white" /> <ArrowUp size={17} className="text-white" />
</button> </button>
</div> </div>
</div> </div>
{/* Settings Drawer (slide-up overlay) */} {/* ═══ Persona Switcher ═══ */}
{showSettings && ( <div className="shrink-0 px-5 pb-2">
<div className="fixed inset-0 z-50 flex flex-col justify-end"> <div className="flex gap-2 overflow-x-auto scrollbar-none -mx-5 px-5">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setShowSettings(false)} /> {PERSONAS.map((p) => {
<div className="relative bg-white rounded-t-3xl max-h-[88dvh] overflow-y-auto"> const isActive = activePersona === p.id
{/* Handle */} return (
<div className="flex justify-center pt-3 pb-1">
<div className="w-10 h-1 bg-gray-200 rounded-full" />
</div>
<div className="px-5 pb-8 space-y-6 pt-3">
<div className="flex items-center justify-between">
<h3 className="font-bold text-gray-900 text-base">Your Spiritual Guide</h3>
<button onClick={() => setShowSettings(false)} className="w-8 h-8 flex items-center justify-center rounded-xl bg-gray-100 text-gray-500">
<X size={16} />
</button>
</div>
{/* Scholar Selector */}
<div className="space-y-3">
{!isPremiumUser && (
<div className="flex items-center gap-2 bg-amber-50 border border-amber-200 rounded-xl px-3 py-2.5">
<Crown size={14} className="text-amber-500 shrink-0" />
<p className="text-xs text-amber-700">Upgrade to unlock scholar personas</p>
</div>
)}
<div className="grid grid-cols-2 gap-3">
{(Object.keys(SCHOLAR_PERSONAS) as CoachPersona[]).map((personaId) => {
const persona = SCHOLAR_PERSONAS[personaId]
const Icon = PERSONA_ICONS[personaId]
const isActive = profile.coachPersona === personaId
const isLocked = PREMIUM_PERSONAS.includes(personaId) && !isPremiumUser
return (
<button
key={personaId}
onClick={() => {
if (isLocked) { router.push('/upgrade?feature=scholar_personas'); return }
setProfile(p => ({ ...p, coachPersona: personaId }))
}}
className={`relative p-4 rounded-2xl border text-left transition active:scale-[0.97] ${
isLocked ? 'border-gray-200 bg-gray-50 opacity-50' :
isActive ? 'border-emerald-400 bg-emerald-50' :
'border-gray-200 bg-gray-50 active:bg-gray-100'
}`}
>
{isLocked && <Lock size={11} className="absolute top-3 right-3 text-gray-400" />}
<Icon size={22} className={`mb-2 ${isActive && !isLocked ? PERSONA_COLORS[personaId] : 'text-gray-400'}`} />
<p className={`text-sm font-semibold leading-tight ${isActive && !isLocked ? 'text-gray-900' : 'text-gray-500'}`}>{persona.name}</p>
<p className="text-[11px] text-gray-400 mt-0.5 leading-tight">{persona.title}</p>
</button>
)
})}
</div>
</div>
{/* Profile Fields */}
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Preferred Name</label>
<input
type="text"
value={profile.preferredName}
onChange={e => setProfile(p => ({ ...p, preferredName: e.target.value }))}
placeholder="How your guide addresses you"
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Experience Level</label>
<select
value={profile.experienceLevel}
onChange={e => setProfile(p => ({ ...p, experienceLevel: e.target.value }))}
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none"
>
<option value="new">New to Islam</option>
<option value="growing">Growing in Faith</option>
<option value="seasoned">Seasoned Muslim</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Madhab</label>
<select
value={profile.madhab}
onChange={e => setProfile(p => ({ ...p, madhab: e.target.value }))}
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none"
>
<option value="unspecified">Madhab Neutral</option>
<option value="hanafi">Hanafi</option>
<option value="shafii">Shafi&apos;i</option>
<option value="maliki">Maliki</option>
<option value="hanbali">Hanbali</option>
</select>
</div>
</div>
{/* Action Items */}
{actionItems.length > 0 && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Target size={13} className="text-emerald-600" />
<span className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Your Commitments</span>
</div>
<div className="flex flex-wrap gap-2">
{actionItems.map((item, i) => (
<span key={i} className="text-xs px-3 py-1.5 rounded-xl bg-emerald-50 text-gray-600 border border-emerald-100">
{item.length > 50 ? item.slice(0, 50) + '…' : item}
</span>
))}
</div>
</div>
)}
<button <button
onClick={handleSaveProfile} key={p.id}
disabled={savingProfile} onClick={() => setActivePersona(p.id)}
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold text-sm disabled:opacity-50 active:scale-[0.98] transition-all" className={`shrink-0 rounded-full px-3.5 py-1.5 text-xs font-semibold active:scale-95 transition-all ${
isActive
? 'bg-[#D4AF37]/15 border border-[#D4AF37]/40 text-[#D4AF37]'
: 'bg-[#111118] border border-[#222] text-[#666]'
}`}
> >
{savingProfile ? 'Saving...' : 'Save & Close'} {p.name}
</button> </button>
</div> )
</div> })}
</div> </div>
)} </div>
</div> </div>
) )
} }
+172 -207
View File
@@ -1,243 +1,208 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState } from 'react'
import { useAuth } from '@/lib/AuthContext'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { User, Crown, Wallet, Sparkles, ShoppingBag, Shield, Save, LogOut, ChevronRight, Star } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import {
Pencil,
ChevronRight,
ShoppingBag,
HandHeart,
MessageSquareText,
MapPin,
Flame,
Bell,
Sun,
Crown,
LogOut,
Wallet,
} from 'lucide-react'
/* ─── Quick Actions ──────────────────────────────────────────── */
const QUICK_ACTIONS = [
{ href: '/souq', icon: ShoppingBag, label: 'Souq' },
{ href: '/waqf', icon: HandHeart, label: 'Waqf' },
{ href: '/forum', icon: MessageSquareText, label: 'Forum' },
{ href: '/halal-monitor', icon: MapPin, label: 'Halal Monitor' },
]
/* ─── Settings Items ─────────────────────────────────────────── */
const SETTINGS = [
{
icon: Flame,
label: 'Spirituality Stats',
value: '124-day streak',
color: 'text-orange-400',
},
{
icon: Bell,
label: 'Notifications',
value: '',
color: 'text-[#999]',
},
{
icon: Sun,
label: 'Appearance',
value: 'Dark Mode',
color: 'text-[#999]',
},
{
icon: Crown,
label: 'Upgrade to Pro',
value: '',
color: 'text-[#D4AF37]',
gold: true,
},
]
/* ─── Component ──────────────────────────────────────────────── */
export default function ProfilePage() { export default function ProfilePage() {
const { token, user, loading: authLoading } = useAuth()
const router = useRouter() const router = useRouter()
const [saving, setSaving] = useState(false)
const [profile, setProfile] = useState({
preferredName: '',
experienceLevel: 'new',
madhab: 'unspecified',
coachPersona: 'nurbuddy',
})
useEffect(() => { const handleSignOut = () => {
if (!authLoading && !token) router.push('/login') if (typeof window !== 'undefined') {
}, [token, authLoading, router]) localStorage.clear()
useEffect(() => {
if (!user) return
const saved = localStorage.getItem('flh_user')
if (saved) {
try {
const u = JSON.parse(saved)
setProfile({
preferredName: u.preferredName || '',
experienceLevel: u.experienceLevel || 'new',
madhab: u.madhab || 'unspecified',
coachPersona: u.coachPersona || 'nurbuddy',
})
} catch { }
} }
}, [user]) router.push('/login')
const handleSave = async () => {
if (!token) return
setSaving(true)
try {
const res = await fetch('/api/auth/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(profile),
})
if (res.ok) {
const meRes = await fetch('/api/auth/me', { headers: { 'Authorization': `Bearer ${token}` } })
const meData = await meRes.json()
if (meData.user) localStorage.setItem('flh_user', JSON.stringify(meData.user))
}
} catch { }
setSaving(false)
} }
const tier = user?.isPro ? 'Pro' : user?.isPremium ? 'Premium' : 'Free'
const tierColor = user?.isPro ? 'text-purple-600' : user?.isPremium ? 'text-[#D4AF37]' : 'text-gray-500'
const tierBg = user?.isPro ? 'bg-purple-50 border-purple-200' : user?.isPremium ? 'bg-amber-50 border-amber-200' : 'bg-gray-100 border-gray-200'
if (authLoading) return <ProfileSkeleton />
if (!token) return null
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24"> <div className="min-h-dvh bg-[#0a0a0f] pb-32 select-none">
{/* Scrollable content */}
<div className="overflow-y-auto">
{/* ═══ Profile Header ═══ */}
<div className="px-5 pt-14 pb-6">
<div className="flex items-center gap-4">
{/* Avatar */}
<div className="w-[60px] h-[60px] rounded-full bg-[#D4AF37]/20 border-2 border-[#D4AF37]/30 flex items-center justify-center shrink-0">
<span className="text-xl font-bold text-[#D4AF37]">A</span>
</div>
{/* Avatar / Hero Card */} {/* Name + badges */}
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-5 pt-12 pb-8 mb-4"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-4"> <div className="flex items-center gap-2">
{/* Avatar circle */} <h2 className="text-white font-bold text-lg truncate">Ahmad</h2>
<div className="w-16 h-16 rounded-full bg-white/20 border-2 border-white flex items-center justify-center shrink-0"> <button className="w-7 h-7 rounded-full bg-[#111118] border border-[#222] flex items-center justify-center active:scale-90 transition-transform shrink-0">
<User size={28} className="text-white" /> <Pencil size={12} className="text-[#999]" />
</div> </button>
<div className="flex-1 min-w-0"> </div>
<h2 className="text-lg font-bold text-white truncate">{user?.name || 'User'}</h2> <p className="text-[#999] text-sm truncate">ahmad@email.com</p>
<p className="text-sm text-white/70 truncate">{user?.email || ''}</p> <div className="flex items-center gap-2 mt-2">
{/* Tier badge */} {/* Premium badge */}
<div className={`inline-flex items-center gap-1.5 mt-2 border rounded-full px-2.5 py-1 ${tierBg}`}> <span className="text-[10px] font-bold text-white bg-[#D4AF37] rounded-full px-2.5 py-0.5 uppercase tracking-wider">
<Star size={11} className={tierColor} /> Premium
<span className={`text-xs font-semibold ${tierColor}`}>{tier}</span> </span>
{/* Level badge */}
<span className="text-[10px] font-bold text-white bg-[#10B981] rounded-full px-2.5 py-0.5 uppercase tracking-wider">
Lvl 7
</span>
</div>
</div> </div>
</div> </div>
{/* Gold premium badge for paid users */}
{(user?.isPremium || user?.isPro) && (
<div className="shrink-0 bg-gradient-to-br from-[#D4AF37] to-amber-500 rounded-xl px-3 py-1.5">
<Crown size={14} className="text-white" />
</div>
)}
</div> </div>
</div>
{/* Stats */} {/* ═══ FLH Wallet Card ═══ */}
<div className="grid grid-cols-2 gap-3 mx-4 mb-4"> <div className="mx-5 mb-5">
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<Wallet size={20} className="text-[#D4AF37] mb-2" />
<p className="text-2xl font-bold text-gray-900">{user?.flhBalance?.toLocaleString() || 0}</p>
<p className="text-xs text-gray-500 mt-0.5">FLH Balance</p>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<ShoppingBag size={20} className="text-emerald-500 mb-2" />
<p className="text-2xl font-bold text-gray-900">0</p>
<p className="text-xs text-gray-500 mt-0.5">Purchases</p>
</div>
</div>
{/* Upgrade CTA — only for free users */}
{!user?.isPremium && !user?.isPro && (
<div className="mx-4 mb-4">
<Link <Link
href="/upgrade" href="/wallet"
className="block bg-gradient-to-r from-amber-50 to-yellow-50 border border-[#D4AF37]/30 rounded-3xl p-5 active:scale-[0.97] transition-all" className="block bg-gradient-to-br from-[#D4AF37]/10 to-[#B8860B]/5 rounded-2xl border border-[#D4AF37]/20 p-5 active:scale-[0.98] transition-transform"
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-[#D4AF37]/15 flex items-center justify-center"> <div className="w-10 h-10 rounded-xl bg-[#D4AF37]/15 flex items-center justify-center">
<Crown size={20} className="text-[#D4AF37]" /> <Wallet size={20} className="text-[#D4AF37]" />
</div> </div>
<div> <div>
<p className="font-bold text-gray-900 text-sm">Go Premium</p> <p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-0.5">
<p className="text-xs text-gray-500">Unlock scholars &amp; unlimited chat</p> FLH Wallet
</p>
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold text-[#D4AF37]">1,250</span>
<span className="text-xs text-[#999]"> .50 USD</span>
</div>
</div> </div>
</div> </div>
<ChevronRight size={18} className="text-[#D4AF37]" /> <ChevronRight size={18} className="text-[#D4AF37]" />
</div> </div>
</Link> </Link>
</div> </div>
)}
{/* Edit Profile Form */} {/* ═══ Quick Action Grid (2x2) ═══ */}
<div className="mx-4 bg-white rounded-2xl shadow-sm border border-gray-100 p-5 mb-4 space-y-4"> <div className="mx-5 mb-5">
<div className="flex items-center gap-2 mb-1"> <p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-3">
<Shield size={15} className="text-emerald-600" /> Quick Actions
<h2 className="font-bold text-gray-900 text-sm">Edit Profile</h2> </p>
</div> <div className="grid grid-cols-2 gap-3">
{QUICK_ACTIONS.map((action) => {
<div className="space-y-1.5"> const Icon = action.icon
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Preferred Name</label> return (
<input <Link
type="text" key={action.href}
value={profile.preferredName} href={action.href}
onChange={e => setProfile(p => ({ ...p, preferredName: e.target.value }))} className="bg-[#111118] rounded-2xl border border-[#222] p-4 active:scale-[0.96] transition-transform"
placeholder="How Nur addresses you" >
className="w-full bg-[#f5f4f0] border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition" <div className="w-10 h-10 rounded-xl bg-[#D4AF37]/10 flex items-center justify-center mb-3">
/> <Icon size={20} className="text-[#D4AF37]" />
</div> </div>
<p className="text-white text-sm font-semibold">{action.label}</p>
<div className="space-y-1.5"> </Link>
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Experience Level</label> )
<select })}
value={profile.experienceLevel}
onChange={e => setProfile(p => ({ ...p, experienceLevel: e.target.value }))}
className="w-full bg-[#f5f4f0] border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none transition"
>
<option value="new">New to Islam</option>
<option value="growing">Growing in Faith</option>
<option value="seasoned">Seasoned Muslim</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Madhab</label>
<select
value={profile.madhab}
onChange={e => setProfile(p => ({ ...p, madhab: e.target.value }))}
className="w-full bg-[#f5f4f0] border border-gray-200 rounded-xl px-4 py-3 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none transition"
>
<option value="unspecified">Madhab Neutral</option>
<option value="hanafi">Hanafi</option>
<option value="shafii">Shafi&apos;i</option>
<option value="maliki">Maliki</option>
<option value="hanbali">Hanbali</option>
</select>
</div>
<button
onClick={handleSave}
disabled={saving}
className="w-full flex items-center justify-center gap-2 bg-emerald-600 text-white py-4 rounded-xl font-bold text-sm disabled:opacity-50 active:scale-[0.97] transition-all"
>
<Save size={16} /> {saving ? 'Saving...' : 'Save Changes'}
</button>
</div>
{/* Halal Monitor link */}
<div className="mx-4 mb-4">
<Link
href="/halal-monitor"
className="flex items-center gap-3 bg-white rounded-2xl shadow-sm border border-gray-100 px-5 py-4 active:scale-[0.97] transition-all"
>
<div className="w-9 h-9 rounded-xl bg-emerald-50 flex items-center justify-center">
<Sparkles size={17} className="text-emerald-600" />
</div> </div>
<div className="flex-1"> </div>
<p className="text-sm font-semibold text-gray-900">Halal Monitor</p>
<p className="text-xs text-gray-500">Find mosques &amp; halal food near you</p>
</div>
<ChevronRight size={16} className="text-gray-400" />
</Link>
</div>
{/* Sign Out */} {/* ═══ Settings List ═══ */}
<div className="mx-4 mb-4"> <div className="mx-5 mb-5">
<button <p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-3">
onClick={() => { Settings
if (typeof window !== 'undefined') { </p>
localStorage.removeItem('flh_token') <div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden divide-y divide-[#222]">
localStorage.removeItem('flh_user') {SETTINGS.map((item, idx) => {
} const Icon = item.icon
router.push('/login') return (
}} <button
className="w-full flex items-center justify-center gap-2 border border-red-200 text-red-500 bg-red-50 rounded-2xl py-4 text-sm font-semibold active:scale-[0.97] transition-all" key={idx}
> className="w-full flex items-center gap-3 px-4 py-4 active:bg-[#1a1a24] transition-colors text-left"
<LogOut size={16} /> Sign Out >
</button> <div className="w-8 h-8 rounded-lg bg-[#0a0a0f] border border-[#222] flex items-center justify-center shrink-0">
<Icon size={15} className={item.color} />
</div>
<div className="flex-1 min-w-0">
<span
className={`text-sm font-medium ${
item.gold ? 'text-[#D4AF37]' : 'text-white'
}`}
>
{item.label}
</span>
{item.value && (
<span className="text-[11px] text-[#999] ml-2">{item.value}</span>
)}
</div>
<ChevronRight size={15} className="text-[#666] shrink-0" />
</button>
)
})}
</div>
</div>
{/* ═══ Sign Out ═══ */}
<div className="mx-5 mb-6">
<button
onClick={handleSignOut}
className="w-full flex items-center justify-center gap-2 py-4 rounded-2xl border border-red-900/40 bg-red-950/20 active:bg-red-950/40 transition-colors"
>
<LogOut size={16} className="text-red-400" />
<span className="text-sm font-semibold text-red-400">Sign Out</span>
</button>
</div>
{/* Bottom spacer */}
<div className="h-4" />
</div> </div>
</div> </div>
) )
} }
function ProfileSkeleton() {
return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24 animate-pulse">
{/* Hero skeleton */}
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-5 pt-12 pb-8 mb-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full bg-white/30" />
<div className="space-y-2 flex-1">
<div className="h-5 w-32 bg-white/30 rounded-lg" />
<div className="h-4 w-48 bg-white/20 rounded-lg" />
<div className="h-6 w-20 bg-white/20 rounded-full" />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3 mx-4 mb-4">
{[...Array(2)].map((_, i) => (
<div key={i} className="bg-white rounded-2xl border border-gray-100 p-4 h-24" />
))}
</div>
<div className="mx-4 bg-white rounded-2xl border border-gray-100 p-5 h-64" />
</div>
)
}
+1 -1
View File
File diff suppressed because one or more lines are too long