Add Prayer Times, Dhikr counter, Quran tracker with mobile UI overhaul
- Prayer times page: arc progress ring, next-prayer countdown, clean prayer list - Dhikr counter: SVG ring with glow, 3-phase sequence (SubhanAllah/Alhamdulillah/AllahuAkbar), streak tracking - Daily Quran reading tracker API routes (GET/POST) - Bottom nav updated: Prayer + Dhikr tabs, Repeat2 icon for dhikr, Moon for prayer - Home page redirects to /prayer as primary entry point - Dockerfile: openssl added to builder stage, explicit prisma schema path fixes build - Schema: dhikrStreak, quranStreak on User; DhikrSession and QuranReading models - Stub wallet top-up checkout to fix TypeScript build error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAuth } from '@/lib/AuthContext'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Flame, CheckCircle2, RotateCcw } from 'lucide-react'
|
||||
|
||||
const DHIKR_SEQUENCE = [
|
||||
{
|
||||
arabic: 'سُبْحَانَ اللَّهِ',
|
||||
transliteration: 'SubhanAllah',
|
||||
meaning: 'Glory be to Allah',
|
||||
target: 33,
|
||||
ring: '#10b981', // emerald
|
||||
glow: 'rgba(16,185,129,0.25)',
|
||||
bg: 'rgba(16,185,129,0.08)',
|
||||
},
|
||||
{
|
||||
arabic: 'الْحَمْدُ لِلَّهِ',
|
||||
transliteration: 'Alhamdulillah',
|
||||
meaning: 'All praise is for Allah',
|
||||
target: 33,
|
||||
ring: '#3b82f6', // blue
|
||||
glow: 'rgba(59,130,246,0.25)',
|
||||
bg: 'rgba(59,130,246,0.08)',
|
||||
},
|
||||
{
|
||||
arabic: 'اللَّهُ أَكْبَرُ',
|
||||
transliteration: 'Allahu Akbar',
|
||||
meaning: 'Allah is the Greatest',
|
||||
target: 34,
|
||||
ring: '#D4AF37', // gold
|
||||
glow: 'rgba(212,175,55,0.3)',
|
||||
bg: 'rgba(212,175,55,0.08)',
|
||||
},
|
||||
]
|
||||
|
||||
const TOTAL = DHIKR_SEQUENCE.reduce((s, d) => s + d.target, 0)
|
||||
|
||||
export default function DhikrPage() {
|
||||
const { user, token } = useAuth()
|
||||
const router = useRouter()
|
||||
const [phase, setPhase] = useState(0)
|
||||
const [count, setCount] = useState(0)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [streak, setStreak] = useState(0)
|
||||
const [todayCompleted, setTodayCompleted] = useState(false)
|
||||
const [justFinished, setJustFinished] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [ripple, setRipple] = useState(false)
|
||||
|
||||
const current = DHIKR_SEQUENCE[phase]
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) { setLoading(false); return }
|
||||
fetch('/api/dhikr/status', { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
setStreak(d.streak ?? 0)
|
||||
setTodayCompleted(d.todayCompleted ?? false)
|
||||
if (d.todayCompleted) setTotalCount(TOTAL)
|
||||
})
|
||||
.catch(() => { })
|
||||
.finally(() => setLoading(false))
|
||||
}, [token])
|
||||
|
||||
const handleTap = useCallback(async () => {
|
||||
if (todayCompleted || justFinished) return
|
||||
if (typeof navigator !== 'undefined' && 'vibrate' in navigator) navigator.vibrate(10)
|
||||
|
||||
setRipple(true)
|
||||
setTimeout(() => setRipple(false), 300)
|
||||
|
||||
const newCount = count + 1
|
||||
const newTotal = totalCount + 1
|
||||
setCount(newCount)
|
||||
setTotalCount(newTotal)
|
||||
|
||||
if (newCount >= current.target) {
|
||||
if (phase < DHIKR_SEQUENCE.length - 1) {
|
||||
setTimeout(() => { setPhase(p => p + 1); setCount(0) }, 350)
|
||||
} else {
|
||||
setJustFinished(true)
|
||||
setTodayCompleted(true)
|
||||
if (token) {
|
||||
try {
|
||||
const res = await fetch('/api/dhikr/complete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ count: TOTAL }),
|
||||
})
|
||||
const data = await res.json()
|
||||
setStreak(data.streak ?? streak + 1)
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [count, totalCount, phase, current, token, todayCompleted, justFinished, streak])
|
||||
|
||||
const handleReset = () => {
|
||||
setPhase(0); setCount(0); setTotalCount(0)
|
||||
setJustFinished(false); setTodayCompleted(false)
|
||||
}
|
||||
|
||||
const progress = current ? count / current.target : 1
|
||||
const R = 58
|
||||
const C = 2 * Math.PI * R
|
||||
const strokeDashoffset = C * (1 - progress)
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#0a0a0f] px-6 pb-20">
|
||||
<div className="text-5xl mb-6">📿</div>
|
||||
<h2 className="text-xl font-bold text-white 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">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col pb-24 select-none">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-2 flex items-center 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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Phase progress dots */}
|
||||
<div className="flex gap-2 px-5 mt-4 mb-8">
|
||||
{DHIKR_SEQUENCE.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-1 h-1 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
background: i < phase ? d.ring : i === phase ? `${d.ring}99` : '#1a1a26',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
<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">
|
||||
<Flame size={18} className="text-orange-400" />
|
||||
<span className="text-lg font-bold text-orange-400">{streak} day streak</span>
|
||||
</div>
|
||||
<button onClick={handleReset} className="mt-10 flex items-center gap-2 text-gray-600 text-sm">
|
||||
<RotateCcw size={13} /> Repeat again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
) : justFinished ? (
|
||||
/* 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">
|
||||
<Flame size={22} className="text-orange-400" />
|
||||
<span className="text-2xl font-bold text-orange-400">{streak} day streak</span>
|
||||
</div>
|
||||
<div className="bg-gray-900/60 border border-gray-800 rounded-2xl 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>
|
||||
</div>
|
||||
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-600 text-sm">
|
||||
<RotateCcw size={13} /> Repeat again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
/* Main counter */
|
||||
<div className="flex-1 flex flex-col items-center justify-center px-6">
|
||||
{/* Ring counter */}
|
||||
<button
|
||||
onClick={handleTap}
|
||||
className="relative flex items-center justify-center active:scale-[0.96] transition-transform"
|
||||
style={{ WebkitTapHighlightColor: 'transparent' }}
|
||||
aria-label={`Tap to count ${current.transliteration}`}
|
||||
>
|
||||
{/* Outer glow ring */}
|
||||
<div
|
||||
className="absolute rounded-full transition-opacity duration-300"
|
||||
style={{
|
||||
width: 216, height: 216,
|
||||
background: `radial-gradient(circle, ${current.glow} 0%, transparent 70%)`,
|
||||
opacity: ripple ? 1 : 0.4,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 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" />
|
||||
{/* Progress */}
|
||||
<circle
|
||||
cx="100" cy="100" r={R} fill="none"
|
||||
stroke={current.ring}
|
||||
strokeWidth="8"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={C}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
className="transition-all duration-200"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Center content */}
|
||||
<div
|
||||
className="absolute rounded-full flex flex-col items-center justify-center border border-white/5"
|
||||
style={{
|
||||
inset: 28,
|
||||
background: `radial-gradient(circle at 40% 30%, ${current.bg}, #0d0d18)`,
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
<p className="font-bold text-base" style={{ color: current.ring }}>{current.transliteration}</p>
|
||||
<p className="text-gray-600 text-sm">{current.meaning}</p>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-800 text-xs mt-10">Tap to count</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom progress */}
|
||||
{!justFinished && (
|
||||
<div className="px-5 pb-2">
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-700 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-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min((totalCount / TOTAL) * 100, 100)}%`,
|
||||
background: 'linear-gradient(to right, #10b981, #3b82f6, #D4AF37)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user