Redesign all pages to light theme (Qalby-inspired mobile UI)
Complete UI overhaul replacing dark gold theme with a clean mobile-first light theme: gradient emerald headers, white card components, light backgrounds, emerald/amber CTAs, and a new home dashboard with feature grid navigation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+51
@@ -0,0 +1,51 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.opencode/memories.md
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
.next/
|
||||
.netlify/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Secrets (DO NOT COMMIT)
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.sql
|
||||
!infrastructure/postgres/init.sql
|
||||
|
||||
# Claude (workspace config, not project code)
|
||||
.claude/*
|
||||
!.claude/commands/
|
||||
|
||||
# Netlify
|
||||
.functions/
|
||||
@@ -0,0 +1,13 @@
|
||||
import { dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
const eslintConfig = [...compat.extends("next/core-web-vitals")];
|
||||
export default eslintConfig;
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
@@ -0,0 +1,6 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
}
|
||||
export default config
|
||||
+74
-49
@@ -109,14 +109,14 @@ export default function DhikrPage() {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#0a0a0f] px-6 pb-20">
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#f5f4f0] px-6 pb-20">
|
||||
<div className="text-5xl mb-6">📿</div>
|
||||
<h2 className="text-xl font-bold text-white mb-2">Daily Dhikr</h2>
|
||||
<h2 className="text-xl font-bold text-gray-900 mb-2">Daily Dhikr</h2>
|
||||
<p className="text-gray-500 text-sm text-center mb-8">
|
||||
Build a daily habit of remembrance. Sign in to track your streak.
|
||||
</p>
|
||||
<button onClick={() => router.push('/login')}
|
||||
className="w-full max-w-xs bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold active:scale-[0.98] transition-all">
|
||||
className="w-full max-w-xs bg-gradient-to-r from-emerald-700 to-teal-600 text-white py-4 rounded-2xl font-bold active:scale-[0.97] transition-all">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
@@ -125,53 +125,76 @@ export default function DhikrPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-dvh flex items-center justify-center bg-[#0a0a0f] pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
||||
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-emerald-700/30 border-t-emerald-700 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col pb-24 select-none">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] flex flex-col pb-24 select-none">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-2 flex items-center justify-between">
|
||||
{/* Header gradient */}
|
||||
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 rounded-b-3xl px-5 pt-10 pb-8 flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Daily Dhikr</h1>
|
||||
<p className="text-[11px] text-gray-700 mt-0.5">100 remembrances · after prayer</p>
|
||||
<p className="text-[11px] text-white/60 mt-0.5">100 remembrances · after prayer</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 bg-orange-500/10 border border-orange-500/20 rounded-xl px-3 py-2">
|
||||
<Flame size={14} className="text-orange-400" />
|
||||
<span className="text-sm font-bold text-orange-400">{streak}</span>
|
||||
{/* Streak badge inside header */}
|
||||
<div className="flex items-center gap-1.5 bg-white/15 border border-white/20 rounded-xl px-3 py-2">
|
||||
<Flame size={14} className="text-orange-300" />
|
||||
<span className="text-sm font-bold text-white">{streak}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phase progress dots */}
|
||||
<div className="flex gap-2 px-5 mt-4 mb-8">
|
||||
{DHIKR_SEQUENCE.map((d, i) => (
|
||||
{/* Phase tabs — below header */}
|
||||
<div className="flex gap-2 px-5 mt-4 mb-2">
|
||||
{DHIKR_SEQUENCE.map((d, i) => {
|
||||
const isActive = i === phase
|
||||
const isDone = i < phase
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 h-1 rounded-full transition-all duration-500"
|
||||
className={`flex-1 py-1.5 rounded-xl text-center transition-all duration-300 ${
|
||||
isActive
|
||||
? 'bg-white shadow-sm border border-[#D4AF37]/40'
|
||||
: isDone
|
||||
? 'bg-white/60 border border-gray-100'
|
||||
: 'bg-white/40 border border-gray-100'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="text-[10px] font-semibold truncate px-1"
|
||||
style={{ color: isActive ? '#D4AF37' : isDone ? d.ring : '#9ca3af' }}
|
||||
>
|
||||
{d.transliteration}
|
||||
</div>
|
||||
<div
|
||||
className="mx-3 mt-1 h-0.5 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
background: i < phase ? d.ring : i === phase ? `${d.ring}99` : '#1a1a26',
|
||||
background: isDone ? d.ring : isActive ? `${d.ring}99` : '#e5e7eb',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Completed today (not just-finished) */}
|
||||
{todayCompleted && !justFinished ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center">
|
||||
<div className="w-28 h-28 rounded-full border-2 border-[#D4AF37]/30 bg-[#D4AF37]/8 flex items-center justify-center mb-6">
|
||||
<CheckCircle2 size={48} className="text-[#D4AF37]" />
|
||||
<div className="bg-white rounded-3xl shadow-md border border-gray-100 p-10 w-full max-w-xs mx-auto flex flex-col items-center">
|
||||
<div className="w-24 h-24 rounded-full border-2 border-[#D4AF37]/30 bg-amber-50 flex items-center justify-center mb-5">
|
||||
<CheckCircle2 size={44} className="text-[#D4AF37]" />
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-2">MashaAllah!</h2>
|
||||
<p className="text-gray-500 text-sm mb-5">You've completed your daily dhikr</p>
|
||||
<div className="flex items-center gap-2 bg-orange-500/10 border border-orange-500/20 rounded-2xl px-5 py-3">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-1">MashaAllah!</h2>
|
||||
<p className="text-gray-500 text-sm mb-5">You've completed your daily dhikr</p>
|
||||
<div className="flex items-center gap-2 bg-orange-50 border border-orange-100 rounded-2xl px-5 py-3">
|
||||
<Flame size={18} className="text-orange-400" />
|
||||
<span className="text-lg font-bold text-orange-400">{streak} day streak</span>
|
||||
<span className="text-lg font-bold text-orange-500">{streak} day streak</span>
|
||||
</div>
|
||||
<button onClick={handleReset} className="mt-10 flex items-center gap-2 text-gray-600 text-sm">
|
||||
</div>
|
||||
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-400 text-sm active:scale-[0.97] transition-transform">
|
||||
<RotateCcw size={13} /> Repeat again
|
||||
</button>
|
||||
</div>
|
||||
@@ -180,27 +203,28 @@ export default function DhikrPage() {
|
||||
/* Completion screen */
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center">
|
||||
<div className="text-6xl mb-5">✨</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-1">SubhanAllah!</h2>
|
||||
<p className="text-gray-400 text-sm mb-6">100 remembrances complete</p>
|
||||
<div className="flex items-center gap-2 bg-orange-500/10 border border-orange-500/20 rounded-2xl px-6 py-4 mb-8">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-1">SubhanAllah!</h2>
|
||||
<p className="text-gray-500 text-sm mb-6">100 remembrances complete</p>
|
||||
<div className="flex items-center gap-2 bg-orange-50 border border-orange-100 rounded-2xl px-6 py-4 mb-6">
|
||||
<Flame size={22} className="text-orange-400" />
|
||||
<span className="text-2xl font-bold text-orange-400">{streak} day streak</span>
|
||||
<span className="text-2xl font-bold text-orange-500">{streak} day streak</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/60 border border-gray-800 rounded-2xl px-5 py-4 max-w-xs">
|
||||
<div className="bg-white border border-gray-100 rounded-2xl shadow-sm px-5 py-4 max-w-xs">
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
“Whoever says SubhanAllah 33 times, Alhamdulillah 33 times, and Allahu Akbar 34 times after each prayer…”
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-700 mt-2">— Sahih Muslim</p>
|
||||
<p className="text-[11px] text-gray-400 mt-2">— Sahih Muslim</p>
|
||||
</div>
|
||||
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-600 text-sm">
|
||||
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-400 text-sm active:scale-[0.97] transition-transform">
|
||||
<RotateCcw size={13} /> Repeat again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
/* Main counter */
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6">
|
||||
{/* Ring counter */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-5">
|
||||
{/* Ring counter — white card */}
|
||||
<div className="bg-white rounded-3xl shadow-md border border-gray-100 mx-5 py-8 px-6 flex flex-col items-center w-full">
|
||||
<button
|
||||
onClick={handleTap}
|
||||
className="relative flex items-center justify-center active:scale-[0.96] transition-transform"
|
||||
@@ -213,14 +237,14 @@ export default function DhikrPage() {
|
||||
style={{
|
||||
width: 216, height: 216,
|
||||
background: `radial-gradient(circle, ${current.glow} 0%, transparent 70%)`,
|
||||
opacity: ripple ? 1 : 0.4,
|
||||
opacity: ripple ? 1 : 0.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* SVG progress ring */}
|
||||
<svg width="200" height="200" className="-rotate-90" style={{ filter: `drop-shadow(0 0 8px ${current.glow})` }}>
|
||||
{/* Track */}
|
||||
<circle cx="100" cy="100" r={R} fill="none" stroke="#1a1a26" strokeWidth="8" />
|
||||
<circle cx="100" cy="100" r={R} fill="none" stroke="#e5e7eb" strokeWidth="8" />
|
||||
{/* Progress */}
|
||||
<circle
|
||||
cx="100" cy="100" r={R} fill="none"
|
||||
@@ -235,36 +259,37 @@ export default function DhikrPage() {
|
||||
|
||||
{/* Center content */}
|
||||
<div
|
||||
className="absolute rounded-full flex flex-col items-center justify-center border border-white/5"
|
||||
className="absolute rounded-full flex flex-col items-center justify-center"
|
||||
style={{
|
||||
inset: 28,
|
||||
background: `radial-gradient(circle at 40% 30%, ${current.bg}, #0d0d18)`,
|
||||
background: `radial-gradient(circle at 40% 30%, ${current.bg}, #ffffff)`,
|
||||
}}
|
||||
>
|
||||
<span className="text-5xl font-bold text-white tabular-nums leading-none">{count}</span>
|
||||
<span className="text-[11px] text-gray-600 mt-1">of {current.target}</span>
|
||||
<span className="text-5xl font-bold text-gray-900 tabular-nums leading-none">{count}</span>
|
||||
<span className="text-[11px] text-gray-400 mt-1">of {current.target}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Dhikr text */}
|
||||
<div className="text-center mt-10 space-y-2">
|
||||
<p className="text-3xl text-white leading-relaxed font-light">{current.arabic}</p>
|
||||
{/* Dhikr text below ring, inside card */}
|
||||
<div className="text-center mt-6 space-y-1.5">
|
||||
<p className="text-3xl text-gray-900 leading-relaxed font-light">{current.arabic}</p>
|
||||
<p className="font-bold text-base" style={{ color: current.ring }}>{current.transliteration}</p>
|
||||
<p className="text-gray-600 text-sm">{current.meaning}</p>
|
||||
<p className="text-gray-500 text-sm">{current.meaning}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-800 text-xs mt-10">Tap to count</p>
|
||||
<p className="text-gray-400 text-xs mt-6">Tap to count</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom progress */}
|
||||
{/* Bottom progress bar */}
|
||||
{!justFinished && (
|
||||
<div className="px-5 pb-2">
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-700 mb-2">
|
||||
<div className="px-5 pb-2 mt-4">
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-400 mb-2">
|
||||
<span>Total today</span>
|
||||
<span className="tabular-nums">{Math.min(totalCount, TOTAL)} / {TOTAL}</span>
|
||||
</div>
|
||||
<div className="h-1 bg-gray-900 rounded-full overflow-hidden">
|
||||
<div className="h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
|
||||
+36
-36
@@ -39,19 +39,19 @@ export default function ForumPage() {
|
||||
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) }
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-4 flex items-center justify-between">
|
||||
<div className="px-5 pt-8 pb-4 bg-white flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
{view === 'threads' && (
|
||||
<button onClick={() => { setView('categories'); setSelectedCat(null) }} className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-800/60 active:opacity-60">
|
||||
<ArrowLeft size={18} className="text-gray-400" />
|
||||
<button onClick={() => { setView('categories'); setSelectedCat(null) }} className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-100 active:opacity-60">
|
||||
<ArrowLeft size={18} className="text-gray-500" />
|
||||
</button>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
|
||||
<p className="text-[11px] text-gray-600 mt-0.5">
|
||||
<h1 className="text-xl font-bold text-gray-900">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
|
||||
<p className="text-[11px] text-gray-400 mt-0.5">
|
||||
{view === 'categories' ? 'Community discussions' : `${threads.length} threads`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ export default function ForumPage() {
|
||||
{token && (
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-1.5 bg-[#D4AF37] text-[#0a0a0f] px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.96] transition-transform"
|
||||
className="flex items-center gap-1.5 bg-emerald-600 text-white px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<Plus size={16} /> Post
|
||||
</button>
|
||||
@@ -68,36 +68,36 @@ export default function ForumPage() {
|
||||
|
||||
{/* Loading */}
|
||||
{loading && (
|
||||
<div className="px-5 space-y-3">
|
||||
<div className="px-5 pt-4 space-y-3">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="h-20 rounded-2xl bg-gray-900/60 animate-pulse" />
|
||||
<div key={i} className="h-20 rounded-2xl bg-gray-200 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categories view */}
|
||||
{!loading && view === 'categories' && (
|
||||
<div className="px-5 space-y-3">
|
||||
<div className="px-5 pt-4 space-y-3">
|
||||
{categories.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="text-4xl mb-3">🕌</div>
|
||||
<p className="text-gray-500 text-sm">No categories yet</p>
|
||||
<p className="text-gray-400 text-sm">No categories yet</p>
|
||||
</div>
|
||||
) : categories.map(c => (
|
||||
<button
|
||||
key={c.id}
|
||||
onClick={() => { setSelectedCat(c); setView('threads'); loadThreads(c.id) }}
|
||||
className="w-full text-left bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition-transform"
|
||||
className="w-full text-left bg-white rounded-2xl shadow-sm border border-gray-100 p-4 active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl w-10 h-10 flex items-center justify-center bg-gray-800/60 rounded-xl shrink-0">
|
||||
<span className="text-2xl w-10 h-10 flex items-center justify-center bg-emerald-50 rounded-xl shrink-0">
|
||||
{c.icon || '💬'}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-white text-sm">{c.name}</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5 line-clamp-1">{c.description}</p>
|
||||
<h3 className="font-semibold text-gray-900 text-sm">{c.name}</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">{c.description}</p>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-700 shrink-0" />
|
||||
<ChevronRight size={16} className="text-gray-300 shrink-0" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -106,25 +106,25 @@ export default function ForumPage() {
|
||||
|
||||
{/* Threads view */}
|
||||
{!loading && view === 'threads' && (
|
||||
<div className="px-5 space-y-3">
|
||||
<div className="px-5 pt-4 space-y-3">
|
||||
{threads.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="text-4xl mb-3">💬</div>
|
||||
<p className="text-white font-semibold mb-1">No threads yet</p>
|
||||
<p className="text-gray-500 text-sm">Start the first discussion</p>
|
||||
<p className="text-gray-900 font-semibold mb-1">No threads yet</p>
|
||||
<p className="text-gray-400 text-sm">Start the first discussion</p>
|
||||
</div>
|
||||
) : threads.map(t => (
|
||||
<div key={t.id} className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4">
|
||||
<h3 className="font-semibold text-white text-sm leading-snug mb-1">{t.title}</h3>
|
||||
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{t.content}</p>
|
||||
<div key={t.id} className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
|
||||
<h3 className="font-semibold text-gray-900 text-sm leading-snug mb-1">{t.title}</h3>
|
||||
<p className="text-xs text-gray-400 line-clamp-2 mb-3">{t.content}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-5 h-5 rounded-full bg-[#D4AF37]/15 flex items-center justify-center">
|
||||
<span className="text-[9px] text-[#D4AF37] font-bold">{t.author.name[0]}</span>
|
||||
<div className="w-5 h-5 rounded-full bg-emerald-100 flex items-center justify-center">
|
||||
<span className="text-[9px] text-emerald-700 font-bold">{t.author.name[0]}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-600">{t.author.name}</span>
|
||||
<span className="text-[11px] text-gray-400">{t.author.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-[11px] text-gray-700">
|
||||
<div className="flex items-center gap-1 text-[11px] text-gray-400">
|
||||
<MessageCircle size={12} />
|
||||
{t._count.posts}
|
||||
</div>
|
||||
@@ -136,7 +136,7 @@ export default function ForumPage() {
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-gray-900 text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
@@ -182,38 +182,38 @@ function CreateThreadSheet({ token, categories, onClose, onCreated, showToast }:
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 space-y-4"
|
||||
className="relative bg-white rounded-t-3xl p-6 pb-10 space-y-4"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto absolute left-1/2 -translate-x-1/2 top-3" />
|
||||
<h2 className="text-lg font-bold text-white mt-2">New Thread</h2>
|
||||
<button type="button" onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-800 mt-2">
|
||||
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto absolute left-1/2 -translate-x-1/2 top-3" />
|
||||
<h2 className="text-lg font-bold text-gray-900 mt-2">New Thread</h2>
|
||||
<button type="button" onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 mt-2">
|
||||
<X size={15} className="text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={categoryId} onChange={e => setCategoryId(e.target.value)}
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3.5 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none"
|
||||
>
|
||||
{categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)}
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="text" placeholder="Thread title" value={title} onChange={e => setTitle(e.target.value)} required
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Share your thoughts…" value={content} onChange={e => setContent(e.target.value)} required rows={4}
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none resize-none"
|
||||
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 resize-none"
|
||||
/>
|
||||
<button
|
||||
type="submit" disabled={loading}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.98] transition-transform"
|
||||
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform"
|
||||
>
|
||||
{loading ? 'Posting…' : 'Post Thread'}
|
||||
</button>
|
||||
|
||||
+9
-6
@@ -3,16 +3,16 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #0a0a0f;
|
||||
--foreground: #ededed;
|
||||
--background: #f5f4f0;
|
||||
--foreground: #111827;
|
||||
--gold: #D4AF37;
|
||||
--gold-dark: #C9A84C;
|
||||
--emerald: #059669;
|
||||
--nav-height: 64px;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
color-scheme: dark;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -37,13 +37,16 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Prevent iOS zoom on input focus — font-size must be >= 16px */
|
||||
/* Prevent iOS zoom on input focus */
|
||||
input, select, textarea {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Smooth scrolling in chat areas */
|
||||
.chat-scroll {
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Hide scrollbar on category strips */
|
||||
.scrollbar-none::-webkit-scrollbar { display: none; }
|
||||
.scrollbar-none { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAuth } from '@/lib/AuthContext'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Bell, ChevronRight, Flame, BookOpen } from 'lucide-react'
|
||||
|
||||
type Timings = { Fajr: string; Sunrise: string; Dhuhr: string; Asr: string; Maghrib: string; Isha: string }
|
||||
const PRAYERS = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha'] as const
|
||||
type PrayerName = typeof PRAYERS[number]
|
||||
|
||||
function toMins(t: string) { const [h, m] = t.replace(/\s*(AM|PM)$/i, '').split(':').map(Number); return h * 60 + m }
|
||||
function fmt12(t: string) { const [h, m] = t.replace(/\s*(AM|PM)$/i, '').split(':').map(Number); return `${h % 12 || 12}:${String(m).padStart(2,'0')} ${h < 12 ? 'AM' : 'PM'}` }
|
||||
function fmtCountdown(mins: number) {
|
||||
const h = Math.floor(mins / 60), m = mins % 60
|
||||
if (h === 0) return `${m}m`
|
||||
return m === 0 ? `${h}h` : `${h}h ${m}m`
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{ href: '/nur', emoji: '🤖', label: 'Nur AI', bg: 'from-violet-500 to-purple-600' },
|
||||
{ href: '/prayer', emoji: '🕌', label: 'Prayer', bg: 'from-emerald-500 to-teal-600' },
|
||||
{ href: '/dhikr', emoji: '📿', label: 'Dhikr', bg: 'from-teal-500 to-cyan-600' },
|
||||
{ href: '/souq', emoji: '🛍️', label: 'Souq', bg: 'from-amber-500 to-orange-500' },
|
||||
{ href: '/halal-monitor', emoji: '🗺️', label: 'Halal Map', bg: 'from-green-500 to-emerald-600' },
|
||||
{ href: '/forum', emoji: '💬', label: 'Forum', bg: 'from-blue-500 to-indigo-600' },
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, token } = useAuth()
|
||||
const router = useRouter()
|
||||
const [timings, setTimings] = useState<Timings | null>(null)
|
||||
const [next, setNext] = useState<{ name: PrayerName; minsLeft: number } | null>(null)
|
||||
const [city, setCity] = useState('')
|
||||
const [now, setNow] = useState(new Date())
|
||||
const [verse, setVerse] = useState<{ surah: string; translation: string; arabic: string } | null>(null)
|
||||
const [streak, setStreak] = useState(0)
|
||||
|
||||
useEffect(() => { const id = setInterval(() => setNow(new Date()), 60000); return () => clearInterval(id) }, [])
|
||||
useEffect(() => { fetch('/api/daily/verse').then(r => r.json()).then(setVerse).catch(() => {}) }, [])
|
||||
useEffect(() => {
|
||||
if (!token) return
|
||||
fetch('/api/dhikr/status', { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(r => r.json()).then(d => setStreak(d.streak ?? 0)).catch(() => {})
|
||||
}, [token])
|
||||
|
||||
const fetchPrayer = useCallback((lat: number, lng: number) => {
|
||||
fetch(`/api/prayer/times?lat=${lat}&lng=${lng}`)
|
||||
.then(r => r.json()).then(d => {
|
||||
if (d.timings) {
|
||||
setTimings(d.timings)
|
||||
const nowMins = now.getHours() * 60 + now.getMinutes()
|
||||
for (const p of PRAYERS) {
|
||||
const pm = toMins(d.timings[p])
|
||||
if (pm > nowMins) { setNext({ name: p, minsLeft: pm - nowMins }); return }
|
||||
}
|
||||
setNext({ name: 'Fajr', minsLeft: (24 * 60 - nowMins) + toMins(d.timings.Fajr) })
|
||||
}
|
||||
}).catch(() => {})
|
||||
}, [now])
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.geolocation) return
|
||||
navigator.geolocation.getCurrentPosition(pos => {
|
||||
const { latitude: lat, longitude: lng } = pos.coords
|
||||
fetchPrayer(lat, lng)
|
||||
fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`, { headers: { 'Accept-Language': 'en' } })
|
||||
.then(r => r.json()).then(d => setCity(d.address?.city || d.address?.town || '')).catch(() => {})
|
||||
}, () => {}, { timeout: 8000 })
|
||||
}, [fetchPrayer])
|
||||
|
||||
const greeting = (() => {
|
||||
const h = now.getHours()
|
||||
if (h < 12) return 'Good Morning'
|
||||
if (h < 17) return 'Good Afternoon'
|
||||
return 'Good Evening'
|
||||
})()
|
||||
|
||||
const name = user?.preferredName || user?.name?.split(' ')[0] || 'Guest'
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24 select-none">
|
||||
|
||||
{/* Header gradient */}
|
||||
<div className="bg-gradient-to-br from-emerald-700 via-teal-600 to-emerald-800 px-5 pt-12 pb-6 rounded-b-[32px]">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div>
|
||||
<p className="text-emerald-200 text-xs font-medium mb-0.5">{greeting}</p>
|
||||
<h1 className="text-2xl font-bold text-white">Assalamualaikum</h1>
|
||||
<p className="text-white/80 text-sm mt-0.5">{name}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{streak > 0 && (
|
||||
<div className="flex items-center gap-1 bg-white/15 rounded-xl px-2.5 py-1.5">
|
||||
<Flame size={13} className="text-orange-300" />
|
||||
<span className="text-xs font-bold text-white">{streak}</span>
|
||||
</div>
|
||||
)}
|
||||
<button className="w-9 h-9 bg-white/15 rounded-xl flex items-center justify-center">
|
||||
<Bell size={18} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prayer time card inside header */}
|
||||
<div
|
||||
className="bg-white/15 backdrop-blur-sm rounded-2xl p-4 border border-white/20"
|
||||
onClick={() => router.push('/prayer')}
|
||||
>
|
||||
{next && timings ? (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-emerald-200 text-[11px] font-medium uppercase tracking-wider mb-1">Next Prayer</p>
|
||||
<p className="text-white text-xl font-bold">{next.name}</p>
|
||||
<p className="text-white/70 text-sm">{fmt12(timings[next.name])}</p>
|
||||
{city && <p className="text-white/50 text-[11px] mt-1">{city}</p>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-3xl font-bold text-white tabular-nums">{fmtCountdown(next.minsLeft)}</p>
|
||||
<p className="text-white/60 text-xs mt-1">remaining</p>
|
||||
<div className="flex items-center gap-1 justify-end mt-2">
|
||||
<span className="text-white/50 text-[11px]">View all</span>
|
||||
<ChevronRight size={11} className="text-white/50" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-emerald-200 text-[11px] font-medium uppercase tracking-wider mb-2">Prayer Times</p>
|
||||
<p className="text-white/70 text-sm">Tap to enable location</p>
|
||||
</div>
|
||||
<ChevronRight size={18} className="text-white/50" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features grid */}
|
||||
<div className="px-5 mt-6 mb-6">
|
||||
<h2 className="text-sm font-bold text-gray-900 mb-3">Quick Access</h2>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{FEATURES.map(f => (
|
||||
<Link
|
||||
key={f.href}
|
||||
href={f.href}
|
||||
className="flex flex-col items-center gap-2 active:scale-[0.94] transition-transform"
|
||||
>
|
||||
<div className={`w-16 h-16 rounded-2xl bg-gradient-to-br ${f.bg} flex items-center justify-center shadow-sm`}>
|
||||
<span className="text-2xl">{f.emoji}</span>
|
||||
</div>
|
||||
<span className="text-xs font-semibold text-gray-700">{f.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Daily verse */}
|
||||
{verse && (
|
||||
<div className="mx-5 mb-5 bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div className="bg-gradient-to-r from-amber-500 to-yellow-500 px-4 py-2.5 flex items-center gap-2">
|
||||
<BookOpen size={14} className="text-white" />
|
||||
<span className="text-white text-xs font-bold uppercase tracking-wider">Daily Verse</span>
|
||||
<span className="text-white/70 text-xs ml-auto">{verse.surah}</span>
|
||||
</div>
|
||||
<div className="px-4 py-4">
|
||||
<p className="text-right text-xl text-gray-800 leading-loose mb-3 font-light">{verse.arabic}</p>
|
||||
<p className="text-gray-600 text-sm leading-relaxed italic">“{verse.translation}”</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nur AI promo */}
|
||||
<div className="mx-5 mb-5">
|
||||
<Link href="/nur" className="block bg-gradient-to-r from-violet-600 to-purple-700 rounded-2xl p-4 active:scale-[0.98] transition-transform">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white font-bold text-base">Ask Nur AI</p>
|
||||
<p className="text-white/70 text-xs mt-0.5">Your personal Islamic guide</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<span className="text-xl">🤖</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Sign in prompt for guests */}
|
||||
{!user && (
|
||||
<div className="mx-5 mb-5 bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
|
||||
<p className="text-gray-900 font-semibold text-sm mb-1">Sign in to track your progress</p>
|
||||
<p className="text-gray-500 text-xs mb-3">Streaks, history, and personalization</p>
|
||||
<Link href="/login" className="block w-full text-center bg-emerald-600 text-white py-3 rounded-xl font-semibold text-sm active:scale-[0.98] transition-transform">
|
||||
Sign In
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+12
-12
@@ -30,13 +30,13 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col bg-[#0a0a0f] px-6">
|
||||
<div className="min-h-dvh flex flex-col bg-white px-6">
|
||||
{/* Brand */}
|
||||
<div className="flex flex-col items-center pt-16 pb-10">
|
||||
<div className="w-20 h-20 rounded-3xl bg-[#D4AF37]/10 border border-[#D4AF37]/25 flex items-center justify-center mb-5">
|
||||
<span className="text-3xl font-bold text-[#D4AF37]">ف</span>
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-600 to-teal-500 flex items-center justify-center mb-5 shadow-lg shadow-emerald-200">
|
||||
<span className="text-3xl font-bold text-white">ف</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight">Welcome back</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Welcome back</h1>
|
||||
<p className="text-gray-500 text-sm mt-1">Sign in to your Falah account</p>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function LoginPage() {
|
||||
required
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl px-4 py-4 text-white placeholder-gray-700 focus:border-[#D4AF37]/60 focus:outline-none transition"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -66,12 +66,12 @@ export default function LoginPage() {
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl px-4 py-4 pr-14 text-white placeholder-gray-700 focus:border-[#D4AF37]/60 focus:outline-none transition"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 pr-14 text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPass(v => !v)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-600 p-1"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 p-1"
|
||||
>
|
||||
{showPass ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
@@ -79,15 +79,15 @@ export default function LoginPage() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl px-4 py-3">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.98] transition-all mt-2"
|
||||
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.97] transition-all mt-2 shadow-sm shadow-emerald-200"
|
||||
>
|
||||
{loading ? 'Signing in...' : 'Sign In'}
|
||||
</button>
|
||||
@@ -96,11 +96,11 @@ export default function LoginPage() {
|
||||
<div className="py-8 space-y-4 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
No account?{' '}
|
||||
<Link href="/register" className="text-[#D4AF37] font-semibold">
|
||||
<Link href="/register" className="text-emerald-600 font-semibold">
|
||||
Create one free
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-xs text-gray-800">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
|
||||
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+59
-59
@@ -173,14 +173,14 @@ export default function NurPage() {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#0a0a0f] px-6 pb-20">
|
||||
<div className="w-20 h-20 rounded-3xl bg-[#D4AF37]/10 border border-[#D4AF37]/20 flex items-center justify-center mb-6">
|
||||
<Bot size={36} className="text-[#D4AF37]" />
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#f5f4f0] px-6 pb-20">
|
||||
<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-white mb-2">Meet Nur</h2>
|
||||
<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-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-base active:scale-[0.98] transition-all">
|
||||
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>
|
||||
@@ -188,20 +188,20 @@ export default function NurPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-[#0a0a0f]" style={{ height: 'calc(100dvh - 64px)' }}>
|
||||
<div className="flex flex-col bg-[#f5f4f0]" style={{ height: 'calc(100dvh - 64px)' }}>
|
||||
|
||||
{/* Header */}
|
||||
<div className="shrink-0 flex items-center gap-3 px-4 py-3 border-b border-gray-800/60 bg-[#0a0a0f]">
|
||||
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center bg-[#D4AF37]/10 border border-[#D4AF37]/20`}>
|
||||
<div className="shrink-0 flex items-center gap-3 px-4 py-3 border-b border-gray-100 bg-white">
|
||||
<div className="w-10 h-10 rounded-2xl flex items-center justify-center bg-violet-50 border border-violet-100">
|
||||
<PersonaIcon size={20} className={PERSONA_COLORS[profile.coachPersona]} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h1 className="font-bold text-white text-sm leading-tight truncate">{currentPersona.name}</h1>
|
||||
<p className="text-gray-500 text-xs truncate">{currentPersona.title}</p>
|
||||
<h1 className="font-bold text-gray-900 text-sm leading-tight truncate">{currentPersona.name}</h1>
|
||||
<p className="text-gray-400 text-xs truncate">{currentPersona.title}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-900 text-gray-400 active:bg-gray-800 transition"
|
||||
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>
|
||||
@@ -209,20 +209,20 @@ export default function NurPage() {
|
||||
|
||||
{/* Daily Verse Banner */}
|
||||
{dailyVerse && (
|
||||
<div className="shrink-0 border-b border-gray-800/60">
|
||||
<div className="shrink-0 border-b border-amber-100 bg-amber-50">
|
||||
<button
|
||||
onClick={() => setShowVerse(v => !v)}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-left"
|
||||
>
|
||||
<BookMarked size={13} className="text-[#D4AF37] shrink-0" />
|
||||
<span className="text-xs font-semibold text-[#D4AF37]">Today's Verse</span>
|
||||
<span className="text-xs text-gray-600 truncate flex-1">{dailyVerse.surah} {dailyVerse.ayah}</span>
|
||||
{showVerse ? <ChevronUp size={13} className="text-gray-600 shrink-0" /> : <ChevronDown size={13} className="text-gray-600 shrink-0" />}
|
||||
<BookMarked size={13} className="text-amber-600 shrink-0" />
|
||||
<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>
|
||||
{showVerse && (
|
||||
<div className="px-4 pb-4 space-y-2 bg-[#0d0d14]">
|
||||
<p className="text-right text-lg leading-loose text-gray-200">{dailyVerse.arabic}</p>
|
||||
<p className="text-xs text-gray-400 italic">“{dailyVerse.translation}”</p>
|
||||
<div className="px-4 pb-4 space-y-2">
|
||||
<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>
|
||||
)}
|
||||
@@ -233,7 +233,7 @@ export default function NurPage() {
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4 space-y-3 chat-scroll">
|
||||
{messages.length === 0 && !dailyLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-full pb-8 space-y-3">
|
||||
<div className="w-16 h-16 rounded-3xl bg-[#D4AF37]/10 border border-[#D4AF37]/15 flex items-center justify-center">
|
||||
<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]} />
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm text-center max-w-xs px-4">{currentPersona.greeting}</p>
|
||||
@@ -242,31 +242,31 @@ export default function NurPage() {
|
||||
messages.map((m, i) => (
|
||||
<div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{m.role === 'assistant' && (
|
||||
<div className="w-7 h-7 rounded-xl bg-[#D4AF37]/10 flex items-center justify-center mr-2 shrink-0 mt-0.5">
|
||||
<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={`max-w-[78%] rounded-2xl px-4 py-3 ${
|
||||
m.role === 'user'
|
||||
? 'bg-[#D4AF37] text-[#0a0a0f] rounded-br-sm'
|
||||
: 'bg-[#1a1a26] text-gray-100 rounded-bl-sm'
|
||||
? 'bg-emerald-600 text-white rounded-br-sm'
|
||||
: 'bg-white text-gray-900 rounded-bl-sm shadow-sm border border-gray-100'
|
||||
}`}>
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{m.content}</p>
|
||||
{m.time && <p className={`text-[10px] mt-1 ${m.role === 'user' ? 'text-[#0a0a0f]/50' : 'text-gray-600'}`}>{m.time}</p>}
|
||||
{m.time && <p className={`text-[10px] mt-1 ${m.role === 'user' ? 'text-white/60' : 'text-gray-400'}`}>{m.time}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{(loading || dailyLoading) && (
|
||||
<div className="flex justify-start">
|
||||
<div className="w-7 h-7 rounded-xl bg-[#D4AF37]/10 flex items-center justify-center mr-2 shrink-0 mt-0.5">
|
||||
<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-[#1a1a26] rounded-2xl rounded-bl-sm px-4 py-3">
|
||||
<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-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
<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>
|
||||
@@ -276,18 +276,18 @@ export default function NurPage() {
|
||||
|
||||
{/* Limit reached banner */}
|
||||
{limitReached && (
|
||||
<div className="shrink-0 mx-4 mb-2 bg-[#1a1a26] border border-[#D4AF37]/25 rounded-2xl px-4 py-3 flex items-center justify-between gap-3">
|
||||
<p className="text-sm text-gray-300 text-xs">Used all {FREE_MSG_LIMIT} free messages today.</p>
|
||||
<a href="/upgrade" className="shrink-0 bg-[#D4AF37] text-[#0a0a0f] px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||
<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">
|
||||
<p className="text-xs text-gray-700">Used all {FREE_MSG_LIMIT} free messages today.</p>
|
||||
<a href="/upgrade" className="shrink-0 bg-emerald-600 text-white px-3 py-1.5 rounded-xl text-xs font-bold">
|
||||
Upgrade
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input */}
|
||||
<div className="shrink-0 border-t border-gray-800/60 px-4 pt-3 pb-3 bg-[#0a0a0f]">
|
||||
<div className="shrink-0 border-t border-gray-100 px-4 pt-3 pb-3 bg-white">
|
||||
{!isPremiumUser && dailyMsgCount !== null && !limitReached && (
|
||||
<p className="text-[11px] text-gray-700 text-right mb-1.5">
|
||||
<p className="text-[11px] text-gray-400 text-right mb-1.5">
|
||||
{dailyMsgCount} / {FREE_MSG_LIMIT} today
|
||||
</p>
|
||||
)}
|
||||
@@ -299,14 +299,14 @@ export default function NurPage() {
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }}
|
||||
disabled={limitReached}
|
||||
className="flex-1 bg-[#1a1a26] border border-gray-800 rounded-2xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none disabled:opacity-40 transition"
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={loading || !input.trim() || limitReached}
|
||||
className="w-11 h-11 flex items-center justify-center bg-[#D4AF37] rounded-2xl disabled:opacity-30 active:scale-95 transition-all shrink-0"
|
||||
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"
|
||||
>
|
||||
<Send size={18} className="text-[#0a0a0f]" />
|
||||
<Send size={18} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -314,17 +314,17 @@ export default function NurPage() {
|
||||
{/* Settings Drawer (slide-up overlay) */}
|
||||
{showSettings && (
|
||||
<div className="fixed inset-0 z-50 flex flex-col justify-end">
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowSettings(false)} />
|
||||
<div className="relative bg-[#111118] rounded-t-3xl border-t border-gray-800/60 max-h-[88dvh] overflow-y-auto">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setShowSettings(false)} />
|
||||
<div className="relative bg-white rounded-t-3xl max-h-[88dvh] overflow-y-auto">
|
||||
{/* Handle */}
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<div className="w-10 h-1 bg-gray-700 rounded-full" />
|
||||
<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-white text-base">Your Spiritual Guide</h3>
|
||||
<button onClick={() => setShowSettings(false)} className="w-8 h-8 flex items-center justify-center rounded-xl bg-gray-800 text-gray-400">
|
||||
<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>
|
||||
@@ -332,9 +332,9 @@ export default function NurPage() {
|
||||
{/* Scholar Selector */}
|
||||
<div className="space-y-3">
|
||||
{!isPremiumUser && (
|
||||
<div className="flex items-center gap-2 bg-[#D4AF37]/8 border border-[#D4AF37]/20 rounded-xl px-3 py-2.5">
|
||||
<Crown size={14} className="text-[#D4AF37] shrink-0" />
|
||||
<p className="text-xs text-[#D4AF37]">Upgrade to unlock scholar personas</p>
|
||||
<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">
|
||||
@@ -351,15 +351,15 @@ export default function NurPage() {
|
||||
setProfile(p => ({ ...p, coachPersona: personaId }))
|
||||
}}
|
||||
className={`relative p-4 rounded-2xl border text-left transition active:scale-[0.97] ${
|
||||
isLocked ? 'border-gray-800 bg-[#0d0d14] opacity-50' :
|
||||
isActive ? 'border-[#D4AF37]/50 bg-[#D4AF37]/5' :
|
||||
'border-gray-800 bg-[#0d0d14] active:bg-gray-800'
|
||||
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-600" />}
|
||||
<Icon size={22} className={`mb-2 ${isActive && !isLocked ? PERSONA_COLORS[personaId] : 'text-gray-600'}`} />
|
||||
<p className={`text-sm font-semibold leading-tight ${isActive && !isLocked ? 'text-white' : 'text-gray-400'}`}>{persona.name}</p>
|
||||
<p className="text-[11px] text-gray-600 mt-0.5 leading-tight">{persona.title}</p>
|
||||
{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>
|
||||
)
|
||||
})}
|
||||
@@ -375,7 +375,7 @@ export default function NurPage() {
|
||||
value={profile.preferredName}
|
||||
onChange={e => setProfile(p => ({ ...p, preferredName: e.target.value }))}
|
||||
placeholder="How your guide addresses you"
|
||||
className="w-full bg-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-700 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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">
|
||||
@@ -383,7 +383,7 @@ export default function NurPage() {
|
||||
<select
|
||||
value={profile.experienceLevel}
|
||||
onChange={e => setProfile(p => ({ ...p, experienceLevel: e.target.value }))}
|
||||
className="w-full bg-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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>
|
||||
@@ -395,7 +395,7 @@ export default function NurPage() {
|
||||
<select
|
||||
value={profile.madhab}
|
||||
onChange={e => setProfile(p => ({ ...p, madhab: e.target.value }))}
|
||||
className="w-full bg-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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>
|
||||
@@ -410,12 +410,12 @@ export default function NurPage() {
|
||||
{actionItems.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target size={13} className="text-[#D4AF37]" />
|
||||
<span className="text-xs font-semibold text-gray-400 uppercase tracking-widest">Your Commitments</span>
|
||||
<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-[#D4AF37]/5 text-gray-400 border border-[#D4AF37]/10">
|
||||
<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>
|
||||
))}
|
||||
@@ -426,7 +426,7 @@ export default function NurPage() {
|
||||
<button
|
||||
onClick={handleSaveProfile}
|
||||
disabled={savingProfile}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-sm disabled:opacity-50 active:scale-[0.98] transition-all"
|
||||
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>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function Home() {
|
||||
redirect('/prayer')
|
||||
redirect('/home')
|
||||
}
|
||||
|
||||
+85
-68
@@ -139,26 +139,27 @@ export default function PrayerPage() {
|
||||
const C = 2 * Math.PI * R
|
||||
const strokeOffset = C * (1 - arcPercent)
|
||||
|
||||
const currentTime = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col pb-24 select-none">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] flex flex-col pb-24 select-none">
|
||||
|
||||
{/* Top bar */}
|
||||
<div className="flex items-center justify-between px-5 pt-6 pb-2">
|
||||
{/* Header gradient card */}
|
||||
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 rounded-b-3xl px-5 pt-10 pb-8">
|
||||
|
||||
{/* Top bar inside header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{city ? (
|
||||
<>
|
||||
<MapPin size={12} className="text-gray-600" />
|
||||
<span className="text-xs text-gray-500">{city}</span>
|
||||
<MapPin size={12} className="text-white/70" />
|
||||
<span className="text-xs text-white/80">{city}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-xs text-gray-700">Detecting location…</span>
|
||||
<span className="text-xs text-white/60">Detecting location…</span>
|
||||
)}
|
||||
</div>
|
||||
{hijri && (
|
||||
<p className="text-[11px] text-[#D4AF37]/60 mt-0.5">
|
||||
<p className="text-[11px] text-white/60 mt-0.5">
|
||||
{hijri.date} {hijri.month.en} {hijri.year} AH
|
||||
</p>
|
||||
)}
|
||||
@@ -166,52 +167,20 @@ export default function PrayerPage() {
|
||||
<button
|
||||
onClick={requestLocation}
|
||||
disabled={loading}
|
||||
className="w-9 h-9 rounded-xl bg-gray-800/60 flex items-center justify-center active:scale-90 transition-transform"
|
||||
className="w-9 h-9 rounded-xl bg-white/10 flex items-center justify-center active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<RefreshCw size={15} className={`text-gray-400 ${loading ? 'animate-spin' : ''}`} />
|
||||
<RefreshCw size={15} className={`text-white/80 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Location denied */}
|
||||
{locationDenied && (
|
||||
<div className="mx-5 mt-4 rounded-3xl bg-gray-900 border border-gray-800 p-8 text-center">
|
||||
<div className="text-4xl mb-4">🕌</div>
|
||||
<p className="text-white font-bold text-lg mb-1">Enable Location</p>
|
||||
<p className="text-gray-500 text-sm mb-6">Prayer times require your location to be accurate</p>
|
||||
<button
|
||||
onClick={requestLocation}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] font-bold py-4 rounded-2xl active:scale-[0.98] transition-transform"
|
||||
>
|
||||
Allow Location Access
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && !locationDenied && (
|
||||
<div className="mx-5 mt-4 bg-red-500/10 border border-red-500/20 rounded-2xl px-4 py-3 text-center">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading state */}
|
||||
{loading && !timings && (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 pt-8">
|
||||
<div className="w-10 h-10 rounded-full border-2 border-[#D4AF37]/20 border-t-[#D4AF37] animate-spin" />
|
||||
<p className="text-xs text-gray-600">Fetching prayer times…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{/* Next prayer arc + countdown inside header */}
|
||||
{timings && next && countdown && (
|
||||
<>
|
||||
{/* Hero — next prayer */}
|
||||
<div className="flex flex-col items-center pt-6 pb-8 px-5">
|
||||
<div className="flex flex-col items-center">
|
||||
{/* Arc clock */}
|
||||
<div className="relative flex items-center justify-center mb-6">
|
||||
<div className="relative flex items-center justify-center mb-5">
|
||||
<svg width="160" height="160" className="-rotate-90">
|
||||
{/* Background ring */}
|
||||
<circle cx="80" cy="80" r={R} fill="none" stroke="#1c1c2a" strokeWidth="6" />
|
||||
<circle cx="80" cy="80" r={R} fill="none" stroke="rgba(255,255,255,0.15)" strokeWidth="6" />
|
||||
{/* Progress arc */}
|
||||
<circle
|
||||
cx="80" cy="80" r={R} fill="none"
|
||||
@@ -222,10 +191,10 @@ export default function PrayerPage() {
|
||||
strokeDashoffset={strokeOffset}
|
||||
className="transition-all duration-1000"
|
||||
/>
|
||||
{/* Glow dots at end of arc */}
|
||||
{/* Glow dot at end of arc */}
|
||||
<circle
|
||||
cx="80" cy="80" r={R} fill="none"
|
||||
stroke="#D4AF37"
|
||||
stroke="rgba(255,255,255,0.4)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray="1"
|
||||
@@ -236,29 +205,60 @@ export default function PrayerPage() {
|
||||
{/* Center content */}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<span className="text-3xl mb-0.5">{PRAYER_DATA[next.name].emoji}</span>
|
||||
<span className="text-[10px] text-gray-600 uppercase tracking-widest">Next Prayer</span>
|
||||
<span className="text-[10px] text-white/60 uppercase tracking-widest">Next Prayer</span>
|
||||
<span className="text-base font-bold text-white mt-0.5">{next.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Countdown */}
|
||||
<div className="text-center">
|
||||
<p className="text-5xl font-bold text-[#D4AF37] tabular-nums leading-none tracking-tight">
|
||||
<p className="text-5xl font-bold text-white tabular-nums leading-none tracking-tight">
|
||||
{countdown.value}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 mt-1.5">{countdown.unit}</p>
|
||||
<p className="text-xs text-gray-700 mt-1">
|
||||
<p className="text-sm text-white/60 mt-1.5">{countdown.unit}</p>
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
{next.name} at {fmt12(timings[next.name])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mx-5 h-px bg-gray-800/60 mb-5" />
|
||||
{/* Loading skeleton inside header */}
|
||||
{loading && !timings && (
|
||||
<div className="flex flex-col items-center gap-3 py-4">
|
||||
<div className="w-40 h-40 rounded-full bg-white/10 animate-pulse" />
|
||||
<div className="w-24 h-8 rounded-xl bg-white/10 animate-pulse mt-2" />
|
||||
<div className="w-32 h-4 rounded-lg bg-white/10 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Location denied */}
|
||||
{locationDenied && (
|
||||
<div className="mx-5 mt-5 bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center">
|
||||
<div className="text-4xl mb-4">🕌</div>
|
||||
<p className="text-gray-900 font-bold text-lg mb-1">Enable Location</p>
|
||||
<p className="text-gray-500 text-sm mb-6">Prayer times require your location to be accurate</p>
|
||||
<button
|
||||
onClick={requestLocation}
|
||||
className="w-full bg-gradient-to-r from-emerald-700 to-teal-600 text-white font-bold py-4 rounded-2xl active:scale-[0.97] transition-transform"
|
||||
>
|
||||
Allow Location Access
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{error && !locationDenied && (
|
||||
<div className="mx-5 mt-4 bg-red-50 border border-red-100 rounded-2xl px-4 py-3 text-center">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prayer list */}
|
||||
<div className="px-4 space-y-1.5">
|
||||
{PRAYERS.map((prayer, i) => {
|
||||
{timings && next && (
|
||||
<div className="px-4 mt-5 space-y-2">
|
||||
{PRAYERS.map((prayer) => {
|
||||
const pMins = toMins(timings[prayer])
|
||||
const isNext = prayer === next.name
|
||||
const isPast = pMins < nowMins && !isNext
|
||||
@@ -268,10 +268,10 @@ export default function PrayerPage() {
|
||||
key={prayer}
|
||||
className={`flex items-center gap-3 px-4 py-3.5 rounded-2xl transition-all ${
|
||||
isNext
|
||||
? 'bg-[#D4AF37]/10 border border-[#D4AF37]/25'
|
||||
? 'bg-amber-50 border border-[#D4AF37]'
|
||||
: isPast
|
||||
? 'opacity-35'
|
||||
: 'bg-gray-900/50'
|
||||
? 'bg-white border border-gray-100 shadow-sm opacity-50'
|
||||
: 'bg-white border border-gray-100 shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{/* Emoji */}
|
||||
@@ -280,7 +280,7 @@ export default function PrayerPage() {
|
||||
{/* Names */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`font-semibold text-sm ${isNext ? 'text-[#D4AF37]' : isPast ? 'text-gray-600' : 'text-white'}`}>
|
||||
<span className={`font-semibold text-sm ${isNext ? 'text-emerald-700' : 'text-gray-900'}`}>
|
||||
{prayer}
|
||||
</span>
|
||||
{isNext && (
|
||||
@@ -289,11 +289,11 @@ export default function PrayerPage() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-700">{PRAYER_DATA[prayer].arabic}</span>
|
||||
<span className="text-[11px] text-gray-500">{PRAYER_DATA[prayer].arabic}</span>
|
||||
</div>
|
||||
|
||||
{/* Time */}
|
||||
<span className={`text-sm font-bold tabular-nums ${isNext ? 'text-[#D4AF37]' : isPast ? 'text-gray-700' : 'text-gray-300'}`}>
|
||||
<span className={`text-sm font-bold tabular-nums ${isNext ? 'text-[#D4AF37]' : 'text-gray-500'}`}>
|
||||
{fmt12(timings[prayer])}
|
||||
</span>
|
||||
</div>
|
||||
@@ -301,18 +301,35 @@ export default function PrayerPage() {
|
||||
})}
|
||||
|
||||
{/* Sunrise */}
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 opacity-25">
|
||||
<div className="flex items-center gap-3 px-4 py-2.5 bg-white rounded-2xl border border-gray-100 shadow-sm opacity-40">
|
||||
<span className="text-lg w-7 text-center">🌄</span>
|
||||
<span className="flex-1 text-xs text-gray-600">Sunrise</span>
|
||||
<span className="text-xs text-gray-600 tabular-nums">{fmt12(timings.Sunrise)}</span>
|
||||
<span className="flex-1 text-xs text-gray-500">Sunrise</span>
|
||||
<span className="text-xs text-gray-500 tabular-nums">{fmt12(timings.Sunrise)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading skeleton for prayer list */}
|
||||
{loading && !timings && (
|
||||
<div className="px-4 mt-5 space-y-2">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="bg-white rounded-2xl shadow-sm border border-gray-100 px-4 py-3.5 flex items-center gap-3">
|
||||
<div className="w-7 h-7 rounded-full bg-gray-200 animate-pulse" />
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="h-3.5 w-20 rounded bg-gray-200 animate-pulse" />
|
||||
<div className="h-2.5 w-12 rounded bg-gray-200 animate-pulse" />
|
||||
</div>
|
||||
<div className="h-3.5 w-16 rounded bg-gray-200 animate-pulse" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer note */}
|
||||
<p className="text-center text-[10px] text-gray-800 mt-8 mb-2">
|
||||
{timings && (
|
||||
<p className="text-center text-[10px] text-gray-400 mt-8 mb-2">
|
||||
Muslim World League (MWL) · AlAdhan
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+60
-49
@@ -56,63 +56,69 @@ export default function ProfilePage() {
|
||||
}
|
||||
|
||||
const tier = user?.isPro ? 'Pro' : user?.isPremium ? 'Premium' : 'Free'
|
||||
const tierColor = user?.isPro ? 'text-purple-400' : user?.isPremium ? 'text-[#D4AF37]' : 'text-gray-500'
|
||||
const tierBg = user?.isPro ? 'bg-purple-400/10 border-purple-400/20' : user?.isPremium ? 'bg-[#D4AF37]/10 border-[#D4AF37]/20' : 'bg-gray-800/50 border-gray-700/50'
|
||||
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 (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-5">
|
||||
<h1 className="text-xl font-bold text-white">Profile</h1>
|
||||
</div>
|
||||
|
||||
{/* Avatar + info card */}
|
||||
<div className="mx-4 bg-[#111118] border border-gray-800/60 rounded-3xl p-5 mb-4">
|
||||
{/* Avatar / Hero Card */}
|
||||
<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-2xl bg-[#D4AF37]/10 border border-[#D4AF37]/20 flex items-center justify-center shrink-0">
|
||||
<User size={28} className="text-[#D4AF37]" />
|
||||
{/* Avatar circle */}
|
||||
<div className="w-16 h-16 rounded-full bg-white/20 border-2 border-white flex items-center justify-center shrink-0">
|
||||
<User size={28} className="text-white" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-bold text-white truncate">{user?.name || 'User'}</h2>
|
||||
<p className="text-sm text-gray-500 truncate">{user?.email || ''}</p>
|
||||
<p className="text-sm text-white/70 truncate">{user?.email || ''}</p>
|
||||
{/* Tier badge */}
|
||||
<div className={`inline-flex items-center gap-1.5 mt-2 border rounded-full px-2.5 py-1 ${tierBg}`}>
|
||||
<Star size={11} className={tierColor} />
|
||||
<span className={`text-xs font-semibold ${tierColor}`}>{tier}</span>
|
||||
</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>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 gap-3 mx-4 mb-4">
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4">
|
||||
<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-white">{user?.flhBalance?.toLocaleString() || 0}</p>
|
||||
<p className="text-xs text-gray-600 mt-0.5">FLH Balance</p>
|
||||
<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-[#111118] border border-gray-800/60 rounded-2xl p-4">
|
||||
<ShoppingBag size={20} className="text-blue-400 mb-2" />
|
||||
<p className="text-2xl font-bold text-white">0</p>
|
||||
<p className="text-xs text-gray-600 mt-0.5">Purchases</p>
|
||||
<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 href="/upgrade" className="block bg-gradient-to-r from-[#D4AF37]/15 to-[#D4AF37]/5 border border-[#D4AF37]/25 rounded-3xl p-5">
|
||||
<Link
|
||||
href="/upgrade"
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-2xl bg-[#D4AF37]/15 flex items-center justify-center">
|
||||
<Crown size={20} className="text-[#D4AF37]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-[#D4AF37] text-sm">Go Premium</p>
|
||||
<p className="text-xs text-gray-500">Unlock scholars & unlimited chat</p>
|
||||
<p className="font-bold text-gray-900 text-sm">Go Premium</p>
|
||||
<p className="text-xs text-gray-500">Unlock scholars & unlimited chat</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={18} className="text-[#D4AF37]" />
|
||||
@@ -122,29 +128,29 @@ export default function ProfilePage() {
|
||||
)}
|
||||
|
||||
{/* Edit Profile Form */}
|
||||
<div className="mx-4 bg-[#111118] border border-gray-800/60 rounded-3xl p-5 mb-4 space-y-4">
|
||||
<div className="mx-4 bg-white rounded-2xl shadow-sm border border-gray-100 p-5 mb-4 space-y-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Shield size={15} className="text-[#D4AF37]" />
|
||||
<h2 className="font-bold text-white text-sm">Edit Profile</h2>
|
||||
<Shield size={15} className="text-emerald-600" />
|
||||
<h2 className="font-bold text-gray-900 text-sm">Edit Profile</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-widest">Preferred Name</label>
|
||||
<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-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-700 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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-600 uppercase tracking-widest">Experience Level</label>
|
||||
<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-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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>
|
||||
@@ -153,11 +159,11 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-semibold text-gray-600 uppercase tracking-widest">Madhab</label>
|
||||
<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-[#0d0d14] border border-gray-800 rounded-xl px-4 py-3 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
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>
|
||||
@@ -170,7 +176,7 @@ export default function ProfilePage() {
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="w-full flex items-center justify-center gap-2 bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-xl font-bold text-sm disabled:opacity-50 active:scale-[0.98] transition-all"
|
||||
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>
|
||||
@@ -178,15 +184,18 @@ export default function ProfilePage() {
|
||||
|
||||
{/* Halal Monitor link */}
|
||||
<div className="mx-4 mb-4">
|
||||
<Link href="/halal-monitor" className="flex items-center gap-3 bg-[#111118] border border-gray-800/60 rounded-2xl px-5 py-4">
|
||||
<div className="w-9 h-9 rounded-xl bg-emerald-500/10 flex items-center justify-center">
|
||||
<Sparkles size={17} className="text-emerald-400" />
|
||||
<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-white">Halal Monitor</p>
|
||||
<p className="text-xs text-gray-600">Find mosques & halal food near you</p>
|
||||
<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-600" />
|
||||
<ChevronRight size={16} className="text-gray-400" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -200,7 +209,7 @@ export default function ProfilePage() {
|
||||
}
|
||||
router.push('/login')
|
||||
}}
|
||||
className="w-full flex items-center justify-center gap-2 border border-red-900/50 text-red-500 bg-red-500/5 rounded-2xl py-4 text-sm font-semibold active:bg-red-500/10 transition"
|
||||
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>
|
||||
@@ -211,22 +220,24 @@ export default function ProfilePage() {
|
||||
|
||||
function ProfileSkeleton() {
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24 animate-pulse">
|
||||
<div className="px-5 pt-6 pb-5"><div className="h-7 w-20 bg-gray-800 rounded-xl" /></div>
|
||||
<div className="mx-4 bg-[#111118] border border-gray-800/60 rounded-3xl p-5 mb-4">
|
||||
<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-2xl bg-gray-800" />
|
||||
<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-gray-800 rounded-lg" />
|
||||
<div className="h-4 w-48 bg-gray-800 rounded-lg" />
|
||||
<div className="h-6 w-20 bg-gray-800 rounded-full" />
|
||||
<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-[#111118] border border-gray-800/60 rounded-2xl p-4 h-24" />)}
|
||||
{[...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-[#111118] border border-gray-800/60 rounded-3xl p-5 h-64" />
|
||||
<div className="mx-4 bg-white rounded-2xl border border-gray-100 p-5 h-64" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+16
-16
@@ -31,17 +31,17 @@ export default function RegisterPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col bg-[#0a0a0f] px-6">
|
||||
<div className="min-h-dvh flex flex-col bg-white px-6">
|
||||
{/* Brand */}
|
||||
<div className="flex flex-col items-center pt-12 pb-8">
|
||||
<div className="w-20 h-20 rounded-3xl bg-[#D4AF37]/10 border border-[#D4AF37]/25 flex items-center justify-center mb-5">
|
||||
<span className="text-3xl font-bold text-[#D4AF37]">ف</span>
|
||||
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-emerald-600 to-teal-500 flex items-center justify-center mb-5 shadow-lg shadow-emerald-200">
|
||||
<span className="text-3xl font-bold text-white">ف</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white tracking-tight">Join Falah</h1>
|
||||
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Join Falah</h1>
|
||||
<p className="text-gray-500 text-sm mt-1">Your Islamic lifestyle companion</p>
|
||||
<div className="flex items-center gap-1.5 mt-4 bg-[#D4AF37]/8 border border-[#D4AF37]/20 rounded-full px-3.5 py-2">
|
||||
<Sparkles size={13} className="text-[#D4AF37]" />
|
||||
<span className="text-xs text-[#D4AF37] font-semibold">7-day Premium trial — free</span>
|
||||
<div className="flex items-center gap-1.5 mt-4 bg-emerald-50 border border-emerald-200 rounded-full px-3.5 py-2">
|
||||
<Sparkles size={13} className="text-emerald-600" />
|
||||
<span className="text-xs text-emerald-700 font-semibold">7-day Premium trial — free</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function RegisterPage() {
|
||||
onChange={e => setName(e.target.value)}
|
||||
required
|
||||
autoComplete="name"
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl px-4 py-4 text-white placeholder-gray-700 focus:border-[#D4AF37]/60 focus:outline-none transition"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function RegisterPage() {
|
||||
required
|
||||
autoComplete="email"
|
||||
inputMode="email"
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl px-4 py-4 text-white placeholder-gray-700 focus:border-[#D4AF37]/60 focus:outline-none transition"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -85,12 +85,12 @@ export default function RegisterPage() {
|
||||
required
|
||||
minLength={8}
|
||||
autoComplete="new-password"
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl px-4 py-4 pr-14 text-white placeholder-gray-700 focus:border-[#D4AF37]/60 focus:outline-none transition"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 pr-14 text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none transition"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPass(v => !v)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-600 p-1"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 p-1"
|
||||
>
|
||||
{showPass ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
@@ -98,15 +98,15 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 rounded-2xl px-4 py-3">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3">
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.98] transition-all mt-1"
|
||||
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.97] transition-all mt-1 shadow-sm shadow-emerald-200"
|
||||
>
|
||||
{loading ? 'Creating account...' : 'Create Free Account'}
|
||||
</button>
|
||||
@@ -115,11 +115,11 @@ export default function RegisterPage() {
|
||||
<div className="py-8 space-y-3 text-center">
|
||||
<p className="text-sm text-gray-500">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-[#D4AF37] font-semibold">
|
||||
<Link href="/login" className="text-emerald-600 font-semibold">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-xs text-gray-800">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
|
||||
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+45
-45
@@ -61,19 +61,19 @@ export default function SouqPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24 select-none">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24 select-none">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-4">
|
||||
<div className="px-5 pt-8 pb-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">Souq</h1>
|
||||
<p className="text-[11px] text-gray-600 mt-0.5">Islamic digital marketplace</p>
|
||||
<h1 className="text-xl font-bold text-gray-900">Souq</h1>
|
||||
<p className="text-[11px] text-gray-400 mt-0.5">Islamic digital marketplace</p>
|
||||
</div>
|
||||
{token && (
|
||||
<button
|
||||
onClick={() => setShowCreate(true)}
|
||||
className="flex items-center gap-1.5 bg-[#D4AF37] text-[#0a0a0f] px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.96] transition-transform"
|
||||
className="flex items-center gap-1.5 bg-emerald-600 text-white px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<Plus size={16} /> Sell
|
||||
</button>
|
||||
@@ -82,27 +82,27 @@ export default function SouqPage() {
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-600" />
|
||||
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search products…"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="w-full bg-[#111118] border border-gray-800 rounded-2xl pl-11 pr-4 py-3.5 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl pl-11 pr-4 py-4 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category chips */}
|
||||
<div className="flex gap-2 px-5 pb-4 overflow-x-auto scrollbar-none" style={{ scrollbarWidth: 'none' }}>
|
||||
<div className="flex gap-2 px-5 py-4 overflow-x-auto scrollbar-none bg-white border-b border-gray-100" style={{ scrollbarWidth: 'none' }}>
|
||||
{CATEGORIES.map(c => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCategory(c)}
|
||||
className={`flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-xs font-semibold whitespace-nowrap transition-all active:scale-95 ${
|
||||
category === c
|
||||
? 'bg-[#D4AF37] text-[#0a0a0f]'
|
||||
: 'bg-gray-900 text-gray-500 border border-gray-800'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-white text-gray-500 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<span>{CATEGORY_ICONS[c]}</span>
|
||||
@@ -113,9 +113,9 @@ export default function SouqPage() {
|
||||
|
||||
{/* Loading */}
|
||||
{loading && (
|
||||
<div className="px-5 space-y-3">
|
||||
<div className="px-5 pt-4 space-y-3">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div key={i} className="h-28 rounded-2xl bg-gray-900/60 animate-pulse" />
|
||||
<div key={i} className="h-28 rounded-2xl bg-gray-200 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -124,37 +124,37 @@ export default function SouqPage() {
|
||||
{!loading && filtered.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center pt-20 px-6 text-center">
|
||||
<div className="text-5xl mb-4">🏪</div>
|
||||
<p className="text-white font-semibold mb-1">Nothing here yet</p>
|
||||
<p className="text-gray-600 text-sm">Be the first to list a product</p>
|
||||
<p className="text-gray-900 font-semibold mb-1">Nothing here yet</p>
|
||||
<p className="text-gray-400 text-sm">Be the first to list a product</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Listings */}
|
||||
{!loading && filtered.length > 0 && (
|
||||
<div className="px-5 space-y-3">
|
||||
<div className="px-5 pt-4 space-y-3">
|
||||
{filtered.map(l => (
|
||||
<button
|
||||
key={l.id}
|
||||
onClick={() => setSelectedListing(l)}
|
||||
className="w-full text-left bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition-transform"
|
||||
className="w-full text-left bg-white rounded-2xl shadow-sm border border-gray-100 p-4 active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon block */}
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-800/60 flex items-center justify-center shrink-0 text-xl">
|
||||
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center shrink-0 text-xl">
|
||||
{CATEGORY_ICONS[l.category] || '📦'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold text-white text-sm leading-tight line-clamp-1">{l.title}</h3>
|
||||
<h3 className="font-semibold text-gray-900 text-sm leading-tight line-clamp-1">{l.title}</h3>
|
||||
{l.featured && <Zap size={13} className="text-[#D4AF37] shrink-0 mt-0.5" />}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-0.5 line-clamp-1">{l.description}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">{l.description}</p>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-[#D4AF37] font-bold text-sm">{l.price_flh.toLocaleString()} FLH</span>
|
||||
<span className="text-[11px] text-gray-700">{l.seller}</span>
|
||||
<span className="text-[11px] text-gray-400">{l.seller}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-gray-700 shrink-0 mt-1" />
|
||||
<ChevronRight size={16} className="text-gray-300 shrink-0 mt-1" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -163,7 +163,7 @@ export default function SouqPage() {
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-gray-900 text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
@@ -171,46 +171,46 @@ export default function SouqPage() {
|
||||
{/* Detail bottom sheet */}
|
||||
{selectedListing && (
|
||||
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={() => setSelectedListing(null)}>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
<div
|
||||
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 max-h-[80dvh] overflow-y-auto"
|
||||
className="relative bg-white rounded-t-3xl p-6 pb-10 max-h-[80dvh] overflow-y-auto"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* Handle */}
|
||||
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto mb-5" />
|
||||
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-5" />
|
||||
|
||||
<div className="flex items-start gap-3 mb-4">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gray-800/60 flex items-center justify-center text-2xl shrink-0">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gray-100 flex items-center justify-center text-2xl shrink-0">
|
||||
{CATEGORY_ICONS[selectedListing.category] || '📦'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="text-lg font-bold text-white leading-tight">{selectedListing.title}</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">by {selectedListing.seller}</p>
|
||||
<h2 className="text-lg font-bold text-gray-900 leading-tight">{selectedListing.title}</h2>
|
||||
<p className="text-xs text-gray-400 mt-0.5">by {selectedListing.seller}</p>
|
||||
</div>
|
||||
<button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-800">
|
||||
<button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100">
|
||||
<X size={15} className="text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="inline-block text-[11px] bg-gray-800 text-gray-400 px-3 py-1 rounded-full mb-3">{selectedListing.category}</span>
|
||||
<p className="text-sm text-gray-400 leading-relaxed mb-5">{selectedListing.description}</p>
|
||||
<span className="inline-block text-[11px] bg-gray-100 text-gray-500 px-3 py-1 rounded-full mb-3">{selectedListing.category}</span>
|
||||
<p className="text-sm text-gray-500 leading-relaxed mb-5">{selectedListing.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<p className="text-2xl font-bold text-[#D4AF37]">{selectedListing.price_flh.toLocaleString()} FLH</p>
|
||||
{user && <p className="text-xs text-gray-600">Your balance: {user.flhBalance?.toLocaleString() || 0} FLH</p>}
|
||||
{user && <p className="text-xs text-gray-400">Your balance: {user.flhBalance?.toLocaleString() || 0} FLH</p>}
|
||||
</div>
|
||||
|
||||
{token && selectedListing.sellerId !== user?.id ? (
|
||||
<button
|
||||
onClick={() => handlePurchase(selectedListing.id)}
|
||||
className="w-full flex items-center justify-center gap-2 bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-base active:scale-[0.98] transition-transform"
|
||||
className="w-full flex items-center justify-center gap-2 bg-emerald-600 text-white py-4 rounded-2xl font-bold text-base active:scale-[0.97] transition-transform"
|
||||
>
|
||||
<ShoppingCart size={18} /> Purchase Now
|
||||
</button>
|
||||
) : selectedListing.sellerId === user?.id ? (
|
||||
<div className="w-full text-center py-4 rounded-2xl bg-gray-900 text-gray-500 text-sm">Your listing</div>
|
||||
<div className="w-full text-center py-4 rounded-2xl border border-gray-200 text-gray-400 text-sm">Your listing</div>
|
||||
) : (
|
||||
<button onClick={() => setSelectedListing(null)} className="w-full text-center py-4 rounded-2xl bg-gray-900 text-gray-400 text-sm">
|
||||
<button onClick={() => setSelectedListing(null)} className="w-full text-center py-4 rounded-2xl border border-gray-200 text-gray-400 text-sm">
|
||||
Sign in to purchase
|
||||
</button>
|
||||
)}
|
||||
@@ -258,41 +258,41 @@ function CreateListingSheet({ token, onClose, onCreated, showToast }: {
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
|
||||
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative bg-[#0f0f18] border border-gray-800/60 rounded-t-3xl p-6 pb-10 space-y-4"
|
||||
className="relative bg-white rounded-t-3xl p-6 pb-10 space-y-4"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="w-10 h-1 bg-gray-700 rounded-full mx-auto mb-2" />
|
||||
<h2 className="text-lg font-bold text-white">New Listing</h2>
|
||||
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2" />
|
||||
<h2 className="text-lg font-bold text-gray-900">New Listing</h2>
|
||||
|
||||
<input
|
||||
type="text" placeholder="Product title" value={title} onChange={e => setTitle(e.target.value)} required
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} required rows={3}
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-3 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none resize-none"
|
||||
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 resize-none"
|
||||
/>
|
||||
<select
|
||||
value={category} onChange={e => setCategory(e.target.value)}
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-3.5 text-sm text-gray-900 focus:border-emerald-400 focus:outline-none"
|
||||
>
|
||||
{['E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other'].map(c => <option key={c}>{c}</option>)}
|
||||
</select>
|
||||
<input
|
||||
type="number" placeholder="Price in FLH" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1}
|
||||
inputMode="numeric"
|
||||
className="w-full bg-[#0a0a0f] border border-gray-800 rounded-2xl px-4 py-4 text-sm text-white placeholder-gray-600 focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-sm text-gray-900 placeholder-gray-400 focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="submit" disabled={loading}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.98] transition-transform"
|
||||
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform"
|
||||
>
|
||||
{loading ? 'Creating…' : 'Create Listing'}
|
||||
</button>
|
||||
<button type="button" onClick={onClose} className="w-full text-gray-600 text-sm py-2">
|
||||
<button type="button" onClick={onClose} className="w-full text-gray-400 text-sm py-2">
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
|
||||
+35
-32
@@ -21,7 +21,7 @@ const TIERS = [
|
||||
cta: 'Continue Free',
|
||||
highlighted: false,
|
||||
icon: Zap,
|
||||
iconColor: 'text-gray-500',
|
||||
iconColor: 'text-gray-400',
|
||||
badge: null,
|
||||
},
|
||||
{
|
||||
@@ -55,7 +55,7 @@ const TIERS = [
|
||||
cta: 'Go Pro',
|
||||
highlighted: false,
|
||||
icon: Crown,
|
||||
iconColor: 'text-purple-400',
|
||||
iconColor: 'text-purple-500',
|
||||
badge: null,
|
||||
},
|
||||
]
|
||||
@@ -101,60 +101,63 @@ function UpgradeContent() {
|
||||
}
|
||||
|
||||
if (authLoading) return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
||||
<div className="min-h-dvh bg-[#f5f4f0] flex items-center justify-center pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-emerald-300 border-t-emerald-600 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
if (!token) return null
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-3 px-4 pt-6 pb-4">
|
||||
<button onClick={() => router.back()} className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-900 text-gray-400">
|
||||
{/* Gradient header */}
|
||||
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-4 pt-12 pb-8">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="w-9 h-9 flex items-center justify-center rounded-xl bg-white/20 text-white mb-4 active:scale-[0.97] transition-all"
|
||||
>
|
||||
<ChevronLeft size={20} />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">Choose your plan</h1>
|
||||
<p className="text-xs text-gray-500">Cancel anytime</p>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-white">Choose your plan</h1>
|
||||
<p className="text-sm text-white/70 mt-1">Cancel anytime, no hidden fees</p>
|
||||
</div>
|
||||
|
||||
{/* Trial reminder */}
|
||||
<div className="mx-4 mb-5 bg-[#D4AF37]/8 border border-[#D4AF37]/20 rounded-2xl px-4 py-3">
|
||||
<p className="text-xs text-[#D4AF37] font-medium">✨ New accounts include a 7-day Premium trial — free</p>
|
||||
<div className="mx-4 -mt-4 mb-5 bg-white border border-emerald-100 rounded-2xl shadow-sm px-4 py-3">
|
||||
<p className="text-xs text-emerald-700 font-medium">New accounts include a 7-day Premium trial — free</p>
|
||||
</div>
|
||||
|
||||
{/* Tiers */}
|
||||
<div className="px-4 space-y-3">
|
||||
<div className="px-4 space-y-4">
|
||||
{TIERS.map((tier) => {
|
||||
const Icon = tier.icon
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className={`relative rounded-3xl border p-5 ${
|
||||
className={`relative bg-white rounded-3xl shadow-md border p-5 ${
|
||||
tier.highlighted
|
||||
? 'bg-gradient-to-b from-[#D4AF37]/8 to-[#111118] border-[#D4AF37]/40'
|
||||
: 'bg-[#111118] border-gray-800/60'
|
||||
? 'border-emerald-400'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{tier.badge && (
|
||||
<div className="absolute -top-3 left-5 bg-[#D4AF37] text-[#0a0a0f] text-[10px] font-bold px-3 py-1 rounded-full tracking-widest">
|
||||
<div className="absolute -top-3 left-5 bg-gradient-to-r from-[#D4AF37] to-amber-500 text-white text-[10px] font-bold px-3 py-1 rounded-full tracking-widest">
|
||||
{tier.badge}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${tier.highlighted ? 'bg-[#D4AF37]/15' : 'bg-gray-800/60'}`}>
|
||||
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${
|
||||
tier.highlighted ? 'bg-amber-50' : 'bg-gray-50'
|
||||
}`}>
|
||||
<Icon size={20} className={tier.iconColor} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-white">{tier.name}</h2>
|
||||
<h2 className="font-bold text-gray-900">{tier.name}</h2>
|
||||
<div className="flex items-baseline gap-0.5">
|
||||
<span className="text-xl font-bold text-white">{tier.price}</span>
|
||||
<span className="text-gray-600 text-xs">{tier.period}</span>
|
||||
<span className="text-xl font-bold text-gray-900">{tier.price}</span>
|
||||
<span className="text-gray-400 text-xs">{tier.period}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,8 +166,8 @@ function UpgradeContent() {
|
||||
<ul className="space-y-2.5 mb-5">
|
||||
{tier.features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2.5">
|
||||
<Check size={14} className="text-[#D4AF37] mt-0.5 shrink-0" />
|
||||
<span className="text-sm text-gray-400">{f}</span>
|
||||
<Check size={14} className="text-emerald-500 mt-0.5 shrink-0" />
|
||||
<span className="text-sm text-gray-600">{f}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -172,10 +175,10 @@ function UpgradeContent() {
|
||||
{tier.priceId ? (
|
||||
<button
|
||||
onClick={() => handleSubscribe(tier.priceId!)}
|
||||
className={`w-full py-4 rounded-2xl font-bold text-sm active:scale-[0.98] transition-all ${
|
||||
className={`w-full py-4 rounded-2xl font-bold text-sm active:scale-[0.97] transition-all ${
|
||||
tier.highlighted
|
||||
? 'bg-[#D4AF37] text-[#0a0a0f]'
|
||||
: 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-purple-50 text-purple-600 border border-purple-200'
|
||||
}`}
|
||||
>
|
||||
{tier.cta}
|
||||
@@ -183,7 +186,7 @@ function UpgradeContent() {
|
||||
) : (
|
||||
<button
|
||||
onClick={() => router.push(tier.href ?? '/')}
|
||||
className="w-full py-4 rounded-2xl font-bold text-sm border border-gray-700 text-gray-400 active:bg-gray-800 transition"
|
||||
className="w-full py-4 rounded-2xl font-bold text-sm border border-gray-200 text-gray-500 bg-gray-50 active:scale-[0.97] transition-all"
|
||||
>
|
||||
{tier.cta}
|
||||
</button>
|
||||
@@ -193,7 +196,7 @@ function UpgradeContent() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-xs text-gray-700 mt-6 px-8">
|
||||
<p className="text-center text-xs text-gray-400 mt-6 px-8">
|
||||
Secure payments via Polar. Cancel anytime from your account settings.
|
||||
</p>
|
||||
</div>
|
||||
@@ -203,8 +206,8 @@ function UpgradeContent() {
|
||||
export default function UpgradePage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
||||
<div className="min-h-dvh bg-[#f5f4f0] flex items-center justify-center pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-emerald-300 border-t-emerald-600 animate-spin" />
|
||||
</div>
|
||||
}>
|
||||
<UpgradeContent />
|
||||
|
||||
+33
-33
@@ -37,8 +37,8 @@ export default function WalletPage() {
|
||||
}
|
||||
|
||||
if (authLoading) return (
|
||||
<div className="min-h-dvh flex items-center justify-center bg-[#0a0a0f] pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
|
||||
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-20">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-emerald-200 border-t-emerald-600 animate-spin" />
|
||||
</div>
|
||||
)
|
||||
if (!token) return null
|
||||
@@ -46,38 +46,38 @@ export default function WalletPage() {
|
||||
const fiatValue = amount ? ((parseInt(amount) || 0) * 0.008).toFixed(2) : '0.00'
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-5">
|
||||
<h1 className="text-xl font-bold text-white">Wallet</h1>
|
||||
<p className="text-[11px] text-gray-600 mt-0.5">Manage your FLH balance</p>
|
||||
<div className="px-5 pt-8 pb-4 bg-white">
|
||||
<h1 className="text-xl font-bold text-gray-900">Wallet</h1>
|
||||
<p className="text-[11px] text-gray-400 mt-0.5">Manage your FLH balance</p>
|
||||
</div>
|
||||
|
||||
{/* Balance card */}
|
||||
<div className="mx-5 mb-5 rounded-3xl overflow-hidden" style={{ background: 'linear-gradient(135deg, #1a1508 0%, #0f0f18 100%)', border: '1px solid rgba(212,175,55,0.2)' }}>
|
||||
<div className="px-6 pt-6 pb-5">
|
||||
<div className="mx-5 mt-5 mb-5 rounded-2xl overflow-hidden shadow-sm bg-gradient-to-br from-emerald-600 to-teal-600">
|
||||
<div className="px-6 pt-6 pb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Wallet size={16} className="text-[#D4AF37]" />
|
||||
<span className="text-xs text-[#D4AF37]/70 font-medium uppercase tracking-wider">FLH Balance</span>
|
||||
<Wallet size={16} className="text-white/70" />
|
||||
<span className="text-xs text-white/70 font-medium uppercase tracking-wider">FLH Balance</span>
|
||||
</div>
|
||||
<p className="text-5xl font-bold text-white tabular-nums leading-none">
|
||||
{(user?.flhBalance || 0).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-[#D4AF37] font-semibold mt-1 text-base">FLH Tokens</p>
|
||||
<p className="text-xs text-gray-600 mt-3">
|
||||
<p className="text-white/80 font-semibold mt-1 text-base">FLH Tokens</p>
|
||||
<p className="text-xs text-white/50 mt-3">
|
||||
≈ £{((user?.flhBalance || 0) * 0.008).toFixed(2)} GBP at current rate
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cash Out */}
|
||||
<div className="mx-5 mb-5 bg-[#111118] border border-gray-800/60 rounded-3xl p-5">
|
||||
<div className="mx-5 mb-5 bg-white border border-gray-100 rounded-2xl shadow-sm p-5">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ArrowDownLeft size={16} className="text-[#D4AF37]" />
|
||||
<h2 className="font-bold text-white text-sm">Cash Out</h2>
|
||||
<ArrowDownLeft size={16} className="text-emerald-600" />
|
||||
<h2 className="font-bold text-gray-900 text-sm">Cash Out</h2>
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 mb-4">100 FLH = £0.80 · Minimum 100 FLH</p>
|
||||
<p className="text-[11px] text-gray-400 mb-4">100 FLH = £0.80 · Minimum 100 FLH</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
@@ -87,18 +87,18 @@ export default function WalletPage() {
|
||||
onChange={e => setAmount(e.target.value)}
|
||||
min={100}
|
||||
inputMode="numeric"
|
||||
className="w-full bg-[#0d0d14] border border-gray-800 rounded-2xl px-4 py-4 text-white placeholder-gray-700 text-sm focus:border-[#D4AF37]/50 focus:outline-none"
|
||||
className="w-full bg-gray-50 border border-gray-200 rounded-xl px-4 py-4 text-gray-900 placeholder-gray-400 text-sm focus:border-emerald-400 focus:outline-none"
|
||||
/>
|
||||
{amount && parseInt(amount) > 0 && (
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<span className="text-xs text-gray-600">{parseInt(amount).toLocaleString()} FLH</span>
|
||||
<span className="text-xs text-[#D4AF37] font-semibold">£{fiatValue} GBP</span>
|
||||
<span className="text-xs text-gray-400">{parseInt(amount).toLocaleString()} FLH</span>
|
||||
<span className="text-xs text-emerald-600 font-semibold">£{fiatValue} GBP</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleCashout}
|
||||
disabled={loading || !amount || parseInt(amount) < 100}
|
||||
className="w-full bg-[#D4AF37] text-[#0a0a0f] py-4 rounded-2xl font-bold text-sm disabled:opacity-40 active:scale-[0.98] transition-all"
|
||||
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold text-sm disabled:opacity-40 active:scale-[0.97] transition-all"
|
||||
>
|
||||
{loading ? 'Submitting…' : 'Request Cash Out'}
|
||||
</button>
|
||||
@@ -109,32 +109,32 @@ export default function WalletPage() {
|
||||
</div>
|
||||
|
||||
{/* History */}
|
||||
<div className="mx-5 bg-[#111118] border border-gray-800/60 rounded-3xl p-5">
|
||||
<h2 className="font-bold text-white text-sm mb-4">History</h2>
|
||||
<div className="mx-5 bg-white border border-gray-100 rounded-2xl shadow-sm p-5">
|
||||
<h2 className="font-bold text-gray-900 text-sm mb-4">History</h2>
|
||||
{cashouts.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-gray-600 text-sm">No cashout requests yet</p>
|
||||
<p className="text-gray-400 text-sm">No cashout requests yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{cashouts.map(c => (
|
||||
<div key={c.id} className="flex items-center gap-3 py-2">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-800 flex items-center justify-center shrink-0">
|
||||
<div className="w-9 h-9 rounded-full bg-gray-100 flex items-center justify-center shrink-0">
|
||||
{c.status === 'pending'
|
||||
? <Clock size={16} className="text-yellow-400" />
|
||||
? <Clock size={16} className="text-yellow-500" />
|
||||
: c.status === 'approved'
|
||||
? <CheckCircle2 size={16} className="text-emerald-400" />
|
||||
: <XCircle size={16} className="text-red-400" />
|
||||
? <CheckCircle2 size={16} className="text-emerald-600" />
|
||||
: <XCircle size={16} className="text-red-500" />
|
||||
}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-white font-medium">{c.amountFlh.toLocaleString()} FLH</p>
|
||||
<p className="text-xs text-gray-600">£{c.fiatAmount.toFixed(2)} GBP</p>
|
||||
<p className="text-sm text-gray-900 font-medium">{c.amountFlh.toLocaleString()} FLH</p>
|
||||
<p className="text-xs text-gray-400">£{c.fiatAmount.toFixed(2)} GBP</p>
|
||||
</div>
|
||||
<span className={`text-xs font-semibold capitalize px-2.5 py-1 rounded-full ${
|
||||
c.status === 'pending' ? 'bg-yellow-400/10 text-yellow-400'
|
||||
: c.status === 'approved' ? 'bg-emerald-400/10 text-emerald-400'
|
||||
: 'bg-red-400/10 text-red-400'
|
||||
c.status === 'pending' ? 'bg-yellow-50 text-yellow-700'
|
||||
: c.status === 'approved' ? 'bg-emerald-50 text-emerald-700'
|
||||
: 'bg-red-50 text-red-600'
|
||||
}`}>{c.status}</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -144,7 +144,7 @@ export default function WalletPage() {
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-[#1a1a26] border border-gray-700 rounded-2xl px-4 py-3 text-sm text-white text-center z-50 shadow-xl">
|
||||
<div className="fixed bottom-24 left-5 right-5 bg-gray-900 text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
|
||||
{toast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,27 +2,47 @@
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Bot, ShoppingBag, Moon, User, Repeat2 } from 'lucide-react'
|
||||
import { Home, Moon, Bot, Repeat2, User } from 'lucide-react'
|
||||
|
||||
const tabs = [
|
||||
{ href: '/nur', icon: Bot, label: 'Nur' },
|
||||
{ href: '/home', icon: Home, label: 'Home' },
|
||||
{ href: '/prayer', icon: Moon, label: 'Prayer' },
|
||||
{ href: '/nur', icon: Bot, label: 'Nur', center: true },
|
||||
{ href: '/dhikr', icon: Repeat2, label: 'Dhikr' },
|
||||
{ href: '/souq', icon: ShoppingBag, label: 'Souq' },
|
||||
{ href: '/profile',icon: User, label: 'Profile' },
|
||||
]
|
||||
|
||||
export default function BottomNav() {
|
||||
const pathname = usePathname()
|
||||
|
||||
if (pathname === '/login' || pathname === '/register') return null
|
||||
|
||||
return (
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-[#0a0a0f]/95 backdrop-blur-md border-t border-gray-800/60"
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)', height: 'calc(64px + env(safe-area-inset-bottom, 0px))' }}>
|
||||
<div className="flex items-stretch h-16">
|
||||
{tabs.map(({ href, icon: Icon, label }) => {
|
||||
const isActive = pathname === href || pathname.startsWith(href + '/')
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-100"
|
||||
style={{ paddingBottom: 'env(safe-area-inset-bottom, 0px)', height: 'calc(64px + env(safe-area-inset-bottom, 0px))' }}
|
||||
>
|
||||
<div className="flex items-stretch h-16 px-2">
|
||||
{tabs.map(({ href, icon: Icon, label, center }) => {
|
||||
const isActive = pathname === href || (href !== '/home' && pathname.startsWith(href + '/'))
|
||||
|
||||
if (center) {
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className="flex-1 flex flex-col items-center justify-center gap-0.5"
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center shadow-sm transition-all active:scale-95 ${
|
||||
isActive
|
||||
? 'bg-gradient-to-br from-violet-500 to-purple-600'
|
||||
: 'bg-gradient-to-br from-emerald-500 to-teal-600'
|
||||
}`}>
|
||||
<Icon size={22} className="text-white" strokeWidth={2} />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
@@ -31,10 +51,10 @@ export default function BottomNav() {
|
||||
>
|
||||
<Icon
|
||||
size={22}
|
||||
className={isActive ? 'text-[#D4AF37]' : 'text-gray-600'}
|
||||
className={isActive ? 'text-emerald-600' : 'text-gray-400'}
|
||||
strokeWidth={isActive ? 2.5 : 1.8}
|
||||
/>
|
||||
<span className={`text-[10px] font-medium ${isActive ? 'text-[#D4AF37]' : 'text-gray-600'}`}>
|
||||
<span className={`text-[10px] font-medium ${isActive ? 'text-emerald-600' : 'text-gray-400'}`}>
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user