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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user