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:
+171
-373
@@ -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>
|
</div>
|
||||||
<button
|
<p className="text-[#999] text-xs truncate">Your Islamic AI Companion</p>
|
||||||
onClick={() => setShowSettings(true)}
|
</div>
|
||||||
className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-100 text-gray-500 active:bg-gray-200 transition"
|
{/* 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"
|
||||||
>
|
>
|
||||||
<Settings size={17} />
|
<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>
|
</button>
|
||||||
</div>
|
</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">
|
||||||
|
<p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-2.5">
|
||||||
|
How are you feeling?
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
{MOODS.map((mood, idx) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowVerse(v => !v)}
|
key={idx}
|
||||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-left"
|
onClick={() => setSelectedMood(idx === selectedMood ? null : idx)}
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center text-2xl active:scale-90 transition-all ${
|
||||||
|
selectedMood === idx
|
||||||
|
? 'border-2 border-[#D4AF37] bg-[#D4AF37]/10 shadow-[0_0_12px_rgba(212,175,55,0.15)]'
|
||||||
|
: 'border-2 border-transparent opacity-60 hover:opacity-100'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<BookMarked size={13} className="text-amber-600 shrink-0" />
|
{mood.emoji}
|
||||||
<span className="text-xs font-semibold text-amber-700">Today's Verse</span>
|
|
||||||
<span className="text-xs text-amber-500 truncate flex-1">{dailyVerse.surah} {dailyVerse.ayah}</span>
|
|
||||||
{showVerse ? <ChevronUp size={13} className="text-amber-400 shrink-0" /> : <ChevronDown size={13} className="text-amber-400 shrink-0" />}
|
|
||||||
</button>
|
</button>
|
||||||
{showVerse && (
|
))}
|
||||||
<div className="px-4 pb-4 space-y-2">
|
</div>
|
||||||
<p className="text-right text-lg leading-loose text-gray-900">{dailyVerse.arabic}</p>
|
|
||||||
<p className="text-xs text-gray-600 italic">“{dailyVerse.translation}”</p>
|
|
||||||
<p className="text-xs text-gray-500 leading-relaxed">{dailyVerse.reflection}</p>
|
|
||||||
</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 */}
|
|
||||||
<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 (
|
return (
|
||||||
<button
|
<button
|
||||||
key={personaId}
|
key={p.id}
|
||||||
onClick={() => {
|
onClick={() => setActivePersona(p.id)}
|
||||||
if (isLocked) { router.push('/upgrade?feature=scholar_personas'); return }
|
className={`shrink-0 rounded-full px-3.5 py-1.5 text-xs font-semibold active:scale-95 transition-all ${
|
||||||
setProfile(p => ({ ...p, coachPersona: personaId }))
|
isActive
|
||||||
}}
|
? 'bg-[#D4AF37]/15 border border-[#D4AF37]/40 text-[#D4AF37]'
|
||||||
className={`relative p-4 rounded-2xl border text-left transition active:scale-[0.97] ${
|
: 'bg-[#111118] border border-[#222] text-[#666]'
|
||||||
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" />}
|
{p.name}
|
||||||
<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>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</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'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
|
|
||||||
onClick={handleSaveProfile}
|
|
||||||
disabled={savingProfile}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{savingProfile ? 'Saving...' : 'Save & Close'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+167
-202
@@ -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 */}
|
||||||
{/* Avatar / Hero Card */}
|
<div className="overflow-y-auto">
|
||||||
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-5 pt-12 pb-8 mb-4">
|
{/* ═══ Profile Header ═══ */}
|
||||||
|
<div className="px-5 pt-14 pb-6">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Avatar circle */}
|
{/* Avatar */}
|
||||||
<div className="w-16 h-16 rounded-full bg-white/20 border-2 border-white flex items-center justify-center shrink-0">
|
<div className="w-[60px] h-[60px] rounded-full bg-[#D4AF37]/20 border-2 border-[#D4AF37]/30 flex items-center justify-center shrink-0">
|
||||||
<User size={28} className="text-white" />
|
<span className="text-xl font-bold text-[#D4AF37]">A</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Name + badges */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h2 className="text-lg font-bold text-white truncate">{user?.name || 'User'}</h2>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-sm text-white/70 truncate">{user?.email || ''}</p>
|
<h2 className="text-white font-bold text-lg truncate">Ahmad</h2>
|
||||||
{/* Tier badge */}
|
<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">
|
||||||
<div className={`inline-flex items-center gap-1.5 mt-2 border rounded-full px-2.5 py-1 ${tierBg}`}>
|
<Pencil size={12} className="text-[#999]" />
|
||||||
<Star size={11} className={tierColor} />
|
</button>
|
||||||
<span className={`text-xs font-semibold ${tierColor}`}>{tier}</span>
|
</div>
|
||||||
|
<p className="text-[#999] text-sm truncate">ahmad@email.com</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
{/* Premium badge */}
|
||||||
|
<span className="text-[10px] font-bold text-white bg-[#D4AF37] rounded-full px-2.5 py-0.5 uppercase tracking-wider">
|
||||||
|
Premium
|
||||||
|
</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>
|
||||||
{/* 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>
|
</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 & 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>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={profile.preferredName}
|
|
||||||
onChange={e => setProfile(p => ({ ...p, preferredName: e.target.value }))}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<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-[#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'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 className="flex-1">
|
|
||||||
<p className="text-sm font-semibold text-gray-900">Halal Monitor</p>
|
|
||||||
<p className="text-xs text-gray-500">Find mosques & halal food near you</p>
|
|
||||||
</div>
|
|
||||||
<ChevronRight size={16} className="text-gray-400" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sign Out */}
|
|
||||||
<div className="mx-4 mb-4">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
localStorage.removeItem('flh_token')
|
|
||||||
localStorage.removeItem('flh_user')
|
|
||||||
}
|
|
||||||
router.push('/login')
|
|
||||||
}}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<LogOut size={16} /> Sign Out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProfileSkeleton() {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24 animate-pulse">
|
<Link
|
||||||
{/* Hero skeleton */}
|
key={action.href}
|
||||||
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-5 pt-12 pb-8 mb-4">
|
href={action.href}
|
||||||
<div className="flex items-center gap-4">
|
className="bg-[#111118] rounded-2xl border border-[#222] p-4 active:scale-[0.96] transition-transform"
|
||||||
<div className="w-16 h-16 rounded-full bg-white/30" />
|
>
|
||||||
<div className="space-y-2 flex-1">
|
<div className="w-10 h-10 rounded-xl bg-[#D4AF37]/10 flex items-center justify-center mb-3">
|
||||||
<div className="h-5 w-32 bg-white/30 rounded-lg" />
|
<Icon size={20} className="text-[#D4AF37]" />
|
||||||
<div className="h-4 w-48 bg-white/20 rounded-lg" />
|
</div>
|
||||||
<div className="h-6 w-20 bg-white/20 rounded-full" />
|
<p className="text-white text-sm font-semibold">{action.label}</p>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ═══ Settings List ═══ */}
|
||||||
|
<div className="mx-5 mb-5">
|
||||||
|
<p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-3">
|
||||||
|
Settings
|
||||||
|
</p>
|
||||||
|
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden divide-y divide-[#222]">
|
||||||
|
{SETTINGS.map((item, idx) => {
|
||||||
|
const Icon = item.icon
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
className="w-full flex items-center gap-3 px-4 py-4 active:bg-[#1a1a24] transition-colors text-left"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
<div className="grid grid-cols-2 gap-3 mx-4 mb-4">
|
<div className="flex-1 min-w-0">
|
||||||
{[...Array(2)].map((_, i) => (
|
<span
|
||||||
<div key={i} className="bg-white rounded-2xl border border-gray-100 p-4 h-24" />
|
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 className="mx-4 bg-white rounded-2xl border border-gray-100 p-5 h-64" />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user