feat: implement Phase 4 Home screen with all 5 sections
- Prayer countdown ring (SVG circular progress, Aladhan API + fallback) - AI morning briefing (gold gradient card, greeting by time of day) - Verse of the day (Ayatul Kursi with Arabic, translation, gold header) - Quick Dhikr (3 counters: SubhanAllah 33, Alhamdulillah 33, Allahu Akbar 34) - Community feed (3 Instagram-style posts with like/comment/share actions)
This commit is contained in:
+488
-175
@@ -1,201 +1,514 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAuth } from '@/lib/AuthContext'
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Bell, ChevronRight, Flame, BookOpen } from 'lucide-react'
|
||||
import { Heart, MessageCircle, Share2, RotateCcw, Bell, Clock } 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]
|
||||
/* ─── Types & Constants ─────────────────────────────────────── */
|
||||
|
||||
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`
|
||||
type PrayerName = 'Fajr' | 'Dhuhr' | 'Asr' | 'Maghrib' | 'Isha'
|
||||
const PRAYERS: PrayerName[] = ['Fajr', 'Dhuhr', 'Asr', 'Maghrib', 'Isha']
|
||||
|
||||
interface Timings {
|
||||
Fajr: string
|
||||
Sunrise: string
|
||||
Dhuhr: string
|
||||
Asr: string
|
||||
Maghrib: string
|
||||
Isha: string
|
||||
}
|
||||
|
||||
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' },
|
||||
const AYATUL_KURSI = {
|
||||
arabic:
|
||||
'ٱللَّهُ لَآ إِلَـٰهَ إِلَّا هُوَ ٱلْحَىُّ ٱلْقَيُّومُ ۚ لَا تَأْخُذُهُۥ سِنَةٌ وَلَا نَوْمٌ ۚ لَّهُۥ مَا فِى ٱلسَّمَـٰوَٰتِ وَمَا فِى ٱلْأَرْضِ ۗ مَن ذَا ٱلَّذِى يَشْفَعُ عِندَهُۥٓ إِلَّا بِإِذْنِهِۦ ۚ يَعْلَمُ مَا بَيْنَ أَيْدِيهِمْ وَمَا خَلْفَهُمْ ۖ وَلَا يُحِيطُونَ بِشَىْءٍ مِّنْ عِلْمِهِۦٓ إِلَّا بِمَا شَآءَ ۚ وَسِعَ كُرْسِيُّهُ ٱلسَّمَـٰوَٰتِ وَٱلْأَرْضَ ۖ وَلَا يَـُٔودُهُۥ حِفْظُهُمَا ۚ وَهُوَ ٱلْعَلِىُّ ٱلْعَظِيمُ',
|
||||
translation:
|
||||
'Allah! There is no god but Him, the Ever-Living, the Sustainer of existence. Neither drowsiness nor sleep overtakes Him. To Him belongs whatever is in the heavens and whatever is on the earth. Who is it that can intercede with Him except by His permission? He knows what is before them and what will be after them, and they encompass nothing of His knowledge except what He wills. His Kursi extends over the heavens and the earth, and their preservation tires Him not. And He is the Most High, the Most Great.',
|
||||
surah: 'Surah Al-Baqarah (2:255)',
|
||||
}
|
||||
|
||||
const DHIKR_ITEMS = [
|
||||
{ arabic: 'سُبْحَانَ ٱللَّٰهِ', latin: 'SubhanAllah', target: 33, color: '#D4AF37' },
|
||||
{ arabic: 'ٱلْحَمْدُ لِلَّٰهِ', latin: 'Alhamdulillah', target: 33, color: '#10B981' },
|
||||
{ arabic: 'ٱللَّٰهُ أَكْبَرُ', latin: 'Allahu Akbar', target: 34, color: '#D4AF37' },
|
||||
]
|
||||
|
||||
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)
|
||||
const POSTS = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Ahmad R.',
|
||||
avatar: 'AR',
|
||||
content:
|
||||
'Alhamdulillah, completed my first full month of Fajr prayer on time! ☀️ The early morning blessings are real.',
|
||||
likes: 24,
|
||||
comments: 5,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Fatima Z.',
|
||||
avatar: 'FZ',
|
||||
content:
|
||||
'Just finished reading Surah Al-Kahf. The story of the people of the cave never gets old. SubhanAllah how Allah protects the believers! 📖',
|
||||
likes: 42,
|
||||
comments: 8,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Omar H.',
|
||||
avatar: 'OH',
|
||||
content:
|
||||
'Anyone looking for a good tafsir of Juz Amma? I highly recommend "The Clear Quran" by Dr. Mustafa Khattab. Very accessible English. 🤲',
|
||||
likes: 18,
|
||||
comments: 12,
|
||||
},
|
||||
]
|
||||
|
||||
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])
|
||||
/* ─── Helpers ────────────────────────────────────────────────── */
|
||||
|
||||
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])
|
||||
const RING_R = 40
|
||||
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_R
|
||||
|
||||
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 DHIKR_R = 22
|
||||
const DHIKR_CIRCUMFERENCE = 2 * Math.PI * DHIKR_R
|
||||
|
||||
const greeting = (() => {
|
||||
const h = now.getHours()
|
||||
function toMins(t: string): number {
|
||||
const [h, m] = t.split(':').map(Number)
|
||||
return h * 60 + m
|
||||
}
|
||||
|
||||
function fmtCountdown(mins: number): string {
|
||||
if (mins <= 0) return 'Now'
|
||||
const h = Math.floor(mins / 60)
|
||||
const m = mins % 60
|
||||
if (h === 0) return `${m}m`
|
||||
if (m === 0) return `${h}h`
|
||||
return `${h}h ${m}m`
|
||||
}
|
||||
|
||||
function getGreeting(h: number): string {
|
||||
if (h < 5) return 'Late Night Reflection'
|
||||
if (h < 12) return 'Good Morning'
|
||||
if (h < 17) return 'Good Afternoon'
|
||||
return 'Good Evening'
|
||||
})()
|
||||
if (h < 20) return 'Good Evening'
|
||||
return 'Night Reflection'
|
||||
}
|
||||
|
||||
const name = user?.preferredName || user?.name?.split(' ')[0] || 'Guest'
|
||||
/* ─── Sub-components ─────────────────────────────────────────── */
|
||||
|
||||
function DhikrCard({
|
||||
item,
|
||||
count,
|
||||
onTap,
|
||||
onReset,
|
||||
}: {
|
||||
item: (typeof DHIKR_ITEMS)[number]
|
||||
count: number
|
||||
onTap: () => void
|
||||
onReset: () => void
|
||||
}) {
|
||||
const progress = Math.min(count / item.target, 1)
|
||||
const dashoffset = DHIKR_CIRCUMFERENCE * (1 - progress)
|
||||
const isComplete = count >= item.target
|
||||
|
||||
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" />
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{/* Circular ring + count display */}
|
||||
<button
|
||||
onClick={onTap}
|
||||
className="relative w-20 h-20 flex items-center justify-center active:scale-90 transition-transform"
|
||||
>
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" className="absolute inset-0">
|
||||
<circle
|
||||
cx="40" cy="40" r={DHIKR_R}
|
||||
fill="none"
|
||||
stroke="rgba(255,255,255,0.06)"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<circle
|
||||
cx="40" cy="40" r={DHIKR_R}
|
||||
fill="none"
|
||||
stroke={item.color}
|
||||
strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={DHIKR_CIRCUMFERENCE}
|
||||
strokeDashoffset={dashoffset}
|
||||
className="transition-all duration-500 ease-out"
|
||||
style={{ transform: 'rotate(-90deg)', transformOrigin: 'center' }}
|
||||
/>
|
||||
</svg>
|
||||
<span className={`text-xl font-bold ${isComplete ? 'text-[#D4AF37]' : 'text-white'}`}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<div className="flex flex-col items-center gap-0.5">
|
||||
<span className="text-[13px] font-arabic leading-tight text-white/90">{item.arabic}</span>
|
||||
<span className="text-[10px] text-[#999]">{item.latin}</span>
|
||||
<span className="text-[10px] text-[#666]">/ {item.target}</span>
|
||||
</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')}
|
||||
{/* Reset */}
|
||||
{count > 0 && (
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="text-[#666] hover:text-[#999] transition-colors p-0.5"
|
||||
title={`Reset ${item.latin}`}
|
||||
>
|
||||
{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>
|
||||
<RotateCcw size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PostCard({ post }: { post: (typeof POSTS)[number] }) {
|
||||
const [liked, setLiked] = useState(false)
|
||||
const [likeCount, setLikeCount] = useState(post.likes)
|
||||
|
||||
const handleLike = () => {
|
||||
setLiked((p) => !p)
|
||||
setLikeCount((p) => (liked ? p - 1 : p + 1))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden">
|
||||
{/* Author row */}
|
||||
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
||||
<div className="w-9 h-9 rounded-full bg-[#D4AF37]/20 flex items-center justify-center shrink-0">
|
||||
<span className="text-xs font-bold text-[#D4AF37]">{post.avatar}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-white truncate">{post.name}</p>
|
||||
<p className="text-[11px] text-[#666]">Just now</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="px-4 py-2">
|
||||
<p className="text-sm text-[#ccc] leading-relaxed">{post.content}</p>
|
||||
</div>
|
||||
|
||||
{/* Action bar */}
|
||||
<div className="flex items-center gap-5 px-4 py-3 border-t border-[#222]">
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className="flex items-center gap-1.5 active:scale-90 transition-transform"
|
||||
>
|
||||
<Heart
|
||||
size={17}
|
||||
className={liked ? 'fill-[#D4AF37] text-[#D4AF37]' : 'text-[#666]'}
|
||||
/>
|
||||
<span className={`text-xs ${liked ? 'text-[#D4AF37]' : 'text-[#666]'}`}>
|
||||
{likeCount}
|
||||
</span>
|
||||
</button>
|
||||
<button className="flex items-center gap-1.5 active:scale-90 transition-transform">
|
||||
<MessageCircle size={17} className="text-[#666]" />
|
||||
<span className="text-xs text-[#666]">{post.comments}</span>
|
||||
</button>
|
||||
<button className="ml-auto active:scale-90 transition-transform">
|
||||
<Share2 size={16} className="text-[#666]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ─── Home Page ──────────────────────────────────────────────── */
|
||||
|
||||
export default function HomePage() {
|
||||
const [now, setNow] = useState(new Date())
|
||||
const [timings, setTimings] = useState<Timings | null>(null)
|
||||
const [nextPrayer, setNextPrayer] = useState<{
|
||||
name: PrayerName
|
||||
time: string
|
||||
minsLeft: number
|
||||
progress: number
|
||||
} | null>(null)
|
||||
|
||||
// Dhikr counters
|
||||
const [dhikrCounts, setDhikrCounts] = useState([0, 0, 0])
|
||||
|
||||
// Tick every 30s
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(new Date()), 30000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
// Fetch prayer times
|
||||
useEffect(() => {
|
||||
fetch(
|
||||
'https://api.aladhan.com/v1/timingsByCity?city=Makkah&country=SaudiArabia&method=4'
|
||||
)
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
if (d.data?.timings) {
|
||||
setTimings(d.data.timings as Timings)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Hardcoded fallback — approximate Makkah times for the demo
|
||||
setTimings({
|
||||
Fajr: '04:30',
|
||||
Sunrise: '05:45',
|
||||
Dhuhr: '12:20',
|
||||
Asr: '15:45',
|
||||
Maghrib: '18:55',
|
||||
Isha: '20:15',
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Compute next prayer + ring progress
|
||||
useEffect(() => {
|
||||
if (!timings) return
|
||||
|
||||
const nowMins = now.getHours() * 60 + now.getMinutes()
|
||||
|
||||
for (let i = 0; i < PRAYERS.length; i++) {
|
||||
const pm = toMins(timings[PRAYERS[i]])
|
||||
if (pm > nowMins) {
|
||||
// Found the next prayer
|
||||
const prevPrayer = i > 0 ? PRAYERS[i - 1] : PRAYERS[PRAYERS.length - 1]
|
||||
const prevMins = toMins(timings[prevPrayer])
|
||||
const windowMinutes = pm - prevMins
|
||||
const elapsed = nowMins - prevMins
|
||||
const progress = Math.min(Math.max(elapsed / windowMinutes, 0), 1)
|
||||
|
||||
setNextPrayer({
|
||||
name: PRAYERS[i],
|
||||
time: timings[PRAYERS[i]],
|
||||
minsLeft: pm - nowMins,
|
||||
progress,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// After Isha — next is Fajr (next day)
|
||||
const fajrMins = toMins(timings.Fajr) + 24 * 60
|
||||
const ishaMins = toMins(timings.Isha)
|
||||
const windowMinutes = fajrMins - ishaMins
|
||||
const elapsed = nowMins - ishaMins
|
||||
const progress = Math.min(Math.max(elapsed / windowMinutes, 0), 1)
|
||||
|
||||
setNextPrayer({
|
||||
name: 'Fajr',
|
||||
time: timings.Fajr,
|
||||
minsLeft: fajrMins - nowMins,
|
||||
progress,
|
||||
})
|
||||
}, [timings, now])
|
||||
|
||||
const ringDashoffset = nextPrayer
|
||||
? RING_CIRCUMFERENCE * (1 - nextPrayer.progress)
|
||||
: RING_CIRCUMFERENCE
|
||||
|
||||
const greeting = useMemo(() => getGreeting(now.getHours()), [now])
|
||||
|
||||
// Dhikr handlers
|
||||
const handleDhikrTap = useCallback((idx: number) => {
|
||||
setDhikrCounts((prev) => {
|
||||
const copy = [...prev]
|
||||
if (copy[idx] < DHIKR_ITEMS[idx].target) {
|
||||
copy[idx] = copy[idx] + 1
|
||||
}
|
||||
return copy
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleDhikrReset = useCallback((idx: number) => {
|
||||
setDhikrCounts((prev) => {
|
||||
const copy = [...prev]
|
||||
copy[idx] = 0
|
||||
return copy
|
||||
})
|
||||
}, [])
|
||||
|
||||
const allDhikrComplete = dhikrCounts.every(
|
||||
(c, i) => c >= DHIKR_ITEMS[i].target
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-32 select-none">
|
||||
{/* ═══ Scrollable content ═══ */}
|
||||
<div className="overflow-y-auto">
|
||||
{/* ── Header ── */}
|
||||
<div className="px-5 pt-14 pb-2">
|
||||
<div className="flex items-start justify-between mb-1">
|
||||
<div>
|
||||
<p className="text-[#999] text-xs font-medium mb-0.5">{greeting}</p>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Assalamualaikum
|
||||
</h1>
|
||||
<p className="text-[#999] text-sm mt-0.5">Welcome to Falah</p>
|
||||
</div>
|
||||
<button className="w-9 h-9 bg-[#111118] rounded-xl flex items-center justify-center border border-[#222] active:scale-90 transition-transform">
|
||||
<Bell size={17} className="text-[#999]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 1. Prayer Countdown Ring ─── */}
|
||||
<div className="mx-5 mb-5">
|
||||
<div className="bg-[#111118] rounded-2xl border border-[#222] p-5">
|
||||
<div className="flex items-center gap-5">
|
||||
{/* SVG Ring */}
|
||||
<div className="relative shrink-0">
|
||||
<svg width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle
|
||||
cx="50" cy="50" r={RING_R}
|
||||
className="prayer-ring-bg"
|
||||
/>
|
||||
<circle
|
||||
cx="50" cy="50" r={RING_R}
|
||||
className="prayer-ring-fg"
|
||||
strokeDasharray={RING_CIRCUMFERENCE}
|
||||
strokeDashoffset={ringDashoffset}
|
||||
/>
|
||||
</svg>
|
||||
{/* Center emoji icon */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="text-2xl">🕌</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next prayer info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[11px] text-[#666] font-medium uppercase tracking-wider mb-1">
|
||||
Next Prayer
|
||||
</p>
|
||||
{nextPrayer ? (
|
||||
<>
|
||||
<p className="text-xl font-bold text-white">{nextPrayer.name}</p>
|
||||
<p className="text-sm text-[#D4AF37] mt-0.5">
|
||||
{nextPrayer.time}
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 mt-2">
|
||||
<Clock size={13} className="text-[#D4AF37]" />
|
||||
<span className="text-lg font-bold text-[#D4AF37] tabular-nums">
|
||||
{fmtCountdown(nextPrayer.minsLeft)}
|
||||
</span>
|
||||
<span className="text-[11px] text-[#666]">remaining</span>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-[#999]">Loading prayer times...</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 2. AI Morning Briefing ─── */}
|
||||
<div className="mx-5 mb-5">
|
||||
<div className="bg-gradient-to-br from-[#D4AF37]/10 to-[#B8860B]/5 rounded-2xl border border-[#D4AF37]/20 p-5 relative overflow-hidden">
|
||||
{/* Decorative glow */}
|
||||
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#D4AF37]/10 rounded-full blur-3xl" />
|
||||
<div className="relative z-10">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-lg">🌙</span>
|
||||
<span className="text-xs font-semibold text-[#D4AF37] uppercase tracking-widest">
|
||||
Morning Briefing
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-white text-base font-semibold leading-relaxed mb-2">
|
||||
Assalamualaikum! 🌙 Your spiritual briefing for today...
|
||||
</p>
|
||||
<p className="text-[#999] text-sm leading-relaxed">
|
||||
Today is a new opportunity to grow closer to Allah. Remember that
|
||||
every good deed, no matter how small, is multiplied by the Most
|
||||
Generous. Start your day with intention, pray Fajr on time, and
|
||||
carry that light through the hours ahead.
|
||||
</p>
|
||||
<Link
|
||||
href="/nur"
|
||||
className="inline-block mt-4 text-xs font-semibold text-[#D4AF37] border border-[#D4AF37]/30 rounded-full px-4 py-1.5 active:scale-95 transition-transform"
|
||||
>
|
||||
Chat with NurBuddy →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 3. Verse of the Day ─── */}
|
||||
<div className="mx-5 mb-5">
|
||||
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden">
|
||||
{/* Gold header bar */}
|
||||
<div className="gold-gradient px-4 py-2.5 flex items-center gap-2">
|
||||
<span className="text-white text-xs font-bold uppercase tracking-wider">
|
||||
Verse of the Day
|
||||
</span>
|
||||
<span className="text-white/70 text-[10px] ml-auto">
|
||||
{AYATUL_KURSI.surah}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="px-5 py-5">
|
||||
<p
|
||||
className="text-right text-lg text-white leading-[2] mb-4 font-light"
|
||||
style={{ fontFamily: '"Noto Naskh Arabic", "Traditional Arabic", serif' }}
|
||||
>
|
||||
{AYATUL_KURSI.arabic}
|
||||
</p>
|
||||
<div className="w-8 h-px bg-[#D4AF37]/40 mx-auto mb-4" />
|
||||
<p className="text-[#bbb] text-sm leading-relaxed italic">
|
||||
“{AYATUL_KURSI.translation}”
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 4. Quick Dhikr ─── */}
|
||||
<div className="mx-5 mb-5">
|
||||
<div className="bg-[#111118] rounded-2xl border border-[#222] p-5">
|
||||
<div className="flex items-center justify-between mb-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base">📿</span>
|
||||
<span className="text-xs font-semibold text-[#D4AF37] uppercase tracking-widest">
|
||||
Quick Dhikr
|
||||
</span>
|
||||
</div>
|
||||
{allDhikrComplete && (
|
||||
<span className="text-[10px] text-emerald-400 font-semibold">
|
||||
✅ Completed
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-around gap-3">
|
||||
{DHIKR_ITEMS.map((item, idx) => (
|
||||
<DhikrCard
|
||||
key={idx}
|
||||
item={item}
|
||||
count={dhikrCounts[idx]}
|
||||
onTap={() => handleDhikrTap(idx)}
|
||||
onReset={() => handleDhikrReset(idx)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ─── 5. Community Feed ─── */}
|
||||
<div className="mx-5 mb-5">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-base">💬</span>
|
||||
<span className="text-xs font-semibold text-[#999] uppercase tracking-widest">
|
||||
Community
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{POSTS.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom spacer */}
|
||||
<div className="h-4" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user