Files
falah-mobile/src/app/home/page.tsx
T
FalahMobile caa3f794b5 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)
2026-07-03 11:34:05 +08:00

515 lines
18 KiB
TypeScript

'use client'
import { useState, useEffect, useMemo, useCallback } from 'react'
import Link from 'next/link'
import { Heart, MessageCircle, Share2, RotateCcw, Bell, Clock } from 'lucide-react'
/* ─── Types & Constants ─────────────────────────────────────── */
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 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' },
]
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,
},
]
/* ─── Helpers ────────────────────────────────────────────────── */
const RING_R = 40
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_R
const DHIKR_R = 22
const DHIKR_CIRCUMFERENCE = 2 * Math.PI * DHIKR_R
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'
if (h < 20) return 'Good Evening'
return 'Night Reflection'
}
/* ─── 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="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>
{/* 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>
{/* Reset */}
{count > 0 && (
<button
onClick={onReset}
className="text-[#666] hover:text-[#999] transition-colors p-0.5"
title={`Reset ${item.latin}`}
>
<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">
&ldquo;{AYATUL_KURSI.translation}&rdquo;
</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>
)
}