feat(falahmobile): retheme app to premium dark spatial UI, implement waqf module, and resolve testing suite config

This commit is contained in:
FalahMobile
2026-07-08 18:07:07 +08:00
parent 3eb89a5665
commit 0cacc4fa1a
22 changed files with 693 additions and 355 deletions
+22 -14
View File
@@ -2,12 +2,16 @@ import { test, expect } from '@playwright/test';
test.describe('Authentication Flow', () => { test.describe('Authentication Flow', () => {
test('should handle login, registration, and logout', async ({ page }) => { test('should handle login, registration, and logout', async ({ page }) => {
// Visit site → home page loads → see navbar // Visit site → redirects to /home → see bottom nav
await page.goto('/'); await page.goto('/');
await expect(page.locator('nav')).toBeVisible(); await expect(page.locator('nav')).toBeVisible();
// Click Profile tab to find Login button
await page.click('text=Profile');
await expect(page).toHaveURL(/\/profile/);
// Click Login → see login form // Click Login → see login form
await page.click('text=Login'); await page.click('a[href="/login"]');
await expect(page.locator('form')).toBeVisible(); await expect(page.locator('form')).toBeVisible();
// Try invalid login → see error message // Try invalid login → see error message
@@ -16,23 +20,27 @@ test.describe('Authentication Flow', () => {
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
await expect(page.locator('text=Invalid credentials')).toBeVisible(); await expect(page.locator('text=Invalid credentials')).toBeVisible();
// Navigate to registration (assuming a link exists on login page) // Navigate to registration
await page.click('text=Register'); await page.click('a[href="/register"]');
// Fill registration form → submit → redirect to home // Fill registration form → submit → redirect to home
await page.fill('input[name="name"]', 'Test User'); const randomEmail = `newuser_${Date.now()}_${Math.floor(Math.random() * 1000)}@example.com`;
await page.fill('input[name="email"]', 'newuser@example.com'); await page.fill('input[type="text"]', 'Test User');
await page.fill('input[name="password"]', 'securepassword123'); await page.fill('input[type="email"]', randomEmail);
await page.fill('input[placeholder="Min. 8 characters"]', 'securepassword123');
await page.click('button[type="submit"]'); await page.click('button[type="submit"]');
// Check redirect and authentication state // Check redirect and authentication state
await expect(page).toHaveURL('/'); await expect(page).toHaveURL(/\/nur/);
// See user menu or similar in navbar when authenticated
await expect(page.locator('nav')).toContainText('Logout');
// Click logout → redirect to home → see login button // Go to Profile tab and check authenticated state
await page.click('text=Logout'); await page.click('text=Profile');
await expect(page).toHaveURL('/'); await expect(page.locator('nav')).toContainText('Profile');
await expect(page.locator('text=Login')).toBeVisible(); await expect(page.locator('button:has-text("Logout")')).toBeVisible();
// Click logout → redirect to login → see login button
await page.click('button:has-text("Logout")');
await expect(page).toHaveURL(/\/login/);
await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
}); });
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineConfig({
}, },
], ],
webServer: { webServer: {
command: 'npm run dev', command: 'node .next/standalone/server.js',
url: 'http://localhost:3000', url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
+4 -4
View File
@@ -7,10 +7,10 @@ export async function POST() {
const password = await bcrypt.hash('password123', 10) const password = await bcrypt.hash('password123', 10)
const userData = [ const userData = [
{ email: 'ghazali@falahos.my', name: 'Abu Hamid Al-Ghazali', password, flhBalance: 5000 }, { email: 'ghazali@falahos.my', name: 'Abu Hamid Al-Ghazali', passwordHash: password, flhBalance: 5000 },
{ email: 'ibnabbas@falahos.my', name: 'Abdullah Ibn Abbas', password, flhBalance: 3000 }, { email: 'ibnabbas@falahos.my', name: 'Abdullah Ibn Abbas', passwordHash: password, flhBalance: 3000 },
{ email: 'rabia@falahos.my', name: "Rabi'a Al-Adawiyya", password, flhBalance: 2000 }, { email: 'rabia@falahos.my', name: "Rabi'a Al-Adawiyya", passwordHash: password, flhBalance: 2000 },
{ email: 'demo@falahos.my', name: 'Demo User', password, flhBalance: 500 }, { email: 'demo@falahos.my', name: 'Demo User', passwordHash: password, flhBalance: 500 },
] ]
const users: any[] = [] const users: any[] = []
+46
View File
@@ -0,0 +1,46 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { verifyJWT } from '@/lib/auth'
export async function POST(req: NextRequest) {
try {
const auth = req.headers.get('authorization')
if (!auth?.startsWith('Bearer ')) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const payload = await verifyJWT(auth.slice(7))
if (!payload) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 })
}
const { projectId, amountFlh } = await req.json()
if (!projectId || !amountFlh || amountFlh <= 0) {
return NextResponse.json({ error: 'Invalid project or amount' }, { status: 400 })
}
const user = await prisma.user.findUnique({ where: { id: payload.id } })
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
if (user.flhBalance < amountFlh) {
return NextResponse.json({ error: 'Insufficient FLH balance' }, { status: 400 })
}
// Decrement the user's balance
const updatedUser = await prisma.user.update({
where: { id: payload.id },
data: { flhBalance: { decrement: amountFlh } },
})
return NextResponse.json({
success: true,
newBalance: updatedUser.flhBalance,
message: `Successfully contributed ${amountFlh} FLH to the project.`
})
} catch (e) {
console.error(e)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
+48 -48
View File
@@ -109,14 +109,14 @@ export default function DhikrPage() {
if (!user) { if (!user) {
return ( return (
<div className="min-h-dvh flex flex-col items-center justify-center bg-[#f5f4f0] px-6 pb-24"> <div className="min-h-dvh flex flex-col items-center justify-center bg-bg-deep px-6 pb-24 text-white">
<div className="text-5xl mb-6">📿</div> <div className="text-5xl mb-6">📿</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">Daily Dhikr</h2> <h2 className="text-xl font-bold text-white mb-2">Daily Dhikr</h2>
<p className="text-gray-500 text-sm text-center mb-8"> <p className="text-text-secondary text-sm text-center mb-8">
Build a daily habit of remembrance. Sign in to track your streak. Build a daily habit of remembrance. Sign in to track your streak.
</p> </p>
<button onClick={() => router.push('/login')} <button onClick={() => router.push('/login')}
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"> className="w-full max-w-xs gold-gradient text-bg-deep py-4 rounded-2xl font-bold active:scale-[0.97] transition-all cursor-pointer shadow-sm shadow-gold/10">
Sign In Sign In
</button> </button>
</div> </div>
@@ -125,24 +125,24 @@ export default function DhikrPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-24"> <div className="min-h-dvh flex items-center justify-center bg-bg-deep pb-24">
<div className="w-8 h-8 rounded-full border-2 border-emerald-700/30 border-t-emerald-700 animate-spin" /> <div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div> </div>
) )
} }
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] flex flex-col pb-24 select-none"> <div className="min-h-dvh bg-bg-deep flex flex-col pb-32 select-none text-white">
{/* Header gradient */} {/* 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 className="bg-gradient-to-br from-emerald-950/40 via-bg-card to-bg-deep border-b border-border/80 rounded-b-3xl px-5 pt-14 pb-8 flex items-start justify-between">
<div> <div>
<h1 className="text-xl font-bold text-white">Daily Dhikr</h1> <h1 className="text-xl font-bold text-white">Daily Dhikr</h1>
<p className="text-[11px] text-white/60 mt-0.5">100 remembrances · after prayer</p> <p className="text-[11px] text-text-secondary mt-0.5">100 remembrances · after prayer</p>
</div> </div>
{/* Streak badge inside header */} {/* 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"> <div className="flex items-center gap-1.5 bg-bg-card/80 border border-border rounded-xl px-3 py-2">
<Flame size={14} className="text-orange-300" /> <Flame size={14} className="text-orange-400 animate-pulse" />
<span className="text-sm font-bold text-white">{streak}</span> <span className="text-sm font-bold text-white">{streak}</span>
</div> </div>
</div> </div>
@@ -157,22 +157,22 @@ export default function DhikrPage() {
key={i} key={i}
className={`flex-1 py-1.5 rounded-xl text-center transition-all duration-300 ${ className={`flex-1 py-1.5 rounded-xl text-center transition-all duration-300 ${
isActive isActive
? 'bg-white shadow-sm border border-[#D4AF37]/40' ? 'bg-bg-card shadow-lg border border-gold/40'
: isDone : isDone
? 'bg-white/60 border border-gray-100' ? 'bg-bg-card/60 border border-border/60'
: 'bg-white/40 border border-gray-100' : 'bg-bg-card/30 border border-border/30'
}`} }`}
> >
<div <div
className="text-[10px] font-semibold truncate px-1" className="text-[10px] font-semibold truncate px-1"
style={{ color: isActive ? '#D4AF37' : isDone ? d.ring : '#9ca3af' }} style={{ color: isActive ? '#D4AF37' : isDone ? d.ring : '#666' }}
> >
{d.transliteration} {d.transliteration}
</div> </div>
<div <div
className="mx-3 mt-1 h-0.5 rounded-full transition-all duration-500" className="mx-3 mt-1 h-0.5 rounded-full transition-all duration-500"
style={{ style={{
background: isDone ? d.ring : isActive ? `${d.ring}99` : '#e5e7eb', background: isDone ? d.ring : isActive ? `${d.ring}99` : 'var(--color-border)',
}} }}
/> />
</div> </div>
@@ -183,18 +183,18 @@ export default function DhikrPage() {
{/* Completed today (not just-finished) */} {/* Completed today (not just-finished) */}
{todayCompleted && !justFinished ? ( {todayCompleted && !justFinished ? (
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center"> <div className="flex-1 flex flex-col items-center justify-center px-6 text-center">
<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="bg-bg-card rounded-3xl border border-border 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"> <div className="w-24 h-24 rounded-full border-2 border-gold/30 bg-gold/5 flex items-center justify-center mb-5">
<CheckCircle2 size={44} className="text-[#D4AF37]" /> <CheckCircle2 size={44} className="text-gold" />
</div> </div>
<h2 className="text-2xl font-bold text-gray-900 mb-1">MashaAllah!</h2> <h2 className="text-2xl font-bold text-white mb-1">MashaAllah!</h2>
<p className="text-gray-500 text-sm mb-5">You&apos;ve completed your daily dhikr</p> <p className="text-text-secondary text-sm mb-5">You&apos;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"> <div className="flex items-center gap-2 bg-orange-950/20 border border-orange-900/40 rounded-2xl px-5 py-3 text-orange-400">
<Flame size={18} className="text-orange-400" /> <Flame size={18} className="text-orange-400 animate-pulse" />
<span className="text-lg font-bold text-orange-500">{streak} day streak</span> <span className="text-lg font-bold">{streak} day streak</span>
</div> </div>
</div> </div>
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-400 text-sm active:scale-[0.97] transition-transform"> <button onClick={handleReset} className="mt-8 flex items-center gap-2 text-text-muted text-sm active:scale-[0.97] transition-transform cursor-pointer hover:text-text-secondary">
<RotateCcw size={13} /> Repeat again <RotateCcw size={13} /> Repeat again
</button> </button>
</div> </div>
@@ -202,20 +202,20 @@ export default function DhikrPage() {
) : justFinished ? ( ) : justFinished ? (
/* Completion screen */ /* Completion screen */
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center"> <div className="flex-1 flex flex-col items-center justify-center px-6 text-center">
<div className="text-6xl mb-5"></div> <div className="text-6xl mb-5 animate-bounce"></div>
<h2 className="text-3xl font-bold text-gray-900 mb-1">SubhanAllah!</h2> <h2 className="text-3xl font-bold text-white mb-1">SubhanAllah!</h2>
<p className="text-gray-500 text-sm mb-6">100 remembrances complete</p> <p className="text-text-secondary 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"> <div className="flex items-center gap-2 bg-orange-950/20 border border-orange-900/40 rounded-2xl px-6 py-4 mb-6 text-orange-400 animate-pulse">
<Flame size={22} className="text-orange-400" /> <Flame size={22} className="text-orange-400" />
<span className="text-2xl font-bold text-orange-500">{streak} day streak</span> <span className="text-2xl font-bold">{streak} day streak</span>
</div> </div>
<div className="bg-white border border-gray-100 rounded-2xl shadow-sm px-5 py-4 max-w-xs"> <div className="bg-bg-card border border-border rounded-2xl px-5 py-4 max-w-xs">
<p className="text-xs text-gray-500 leading-relaxed"> <p className="text-xs text-text-secondary leading-relaxed">
&ldquo;Whoever says SubhanAllah 33 times, Alhamdulillah 33 times, and Allahu Akbar 34 times after each prayer&rdquo; &ldquo;Whoever says SubhanAllah 33 times, Alhamdulillah 33 times, and Allahu Akbar 34 times after each prayer&rdquo;
</p> </p>
<p className="text-[11px] text-gray-400 mt-2"> Sahih Muslim</p> <p className="text-[11px] text-text-muted mt-2"> Sahih Muslim</p>
</div> </div>
<button onClick={handleReset} className="mt-8 flex items-center gap-2 text-gray-400 text-sm active:scale-[0.97] transition-transform"> <button onClick={handleReset} className="mt-8 flex items-center gap-2 text-text-muted text-sm active:scale-[0.97] transition-transform cursor-pointer hover:text-text-secondary">
<RotateCcw size={13} /> Repeat again <RotateCcw size={13} /> Repeat again
</button> </button>
</div> </div>
@@ -223,11 +223,11 @@ export default function DhikrPage() {
) : ( ) : (
/* Main counter */ /* Main counter */
<div className="flex-1 flex flex-col items-center justify-center px-5"> <div className="flex-1 flex flex-col items-center justify-center px-5">
{/* Ring counter — white card */} {/* Ring counter — 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"> <div className="bg-bg-card rounded-3xl border border-border mx-5 py-8 px-6 flex flex-col items-center w-full">
<button <button
onClick={handleTap} onClick={handleTap}
className="relative flex items-center justify-center active:scale-[0.96] transition-transform" className="relative flex items-center justify-center active:scale-[0.96] transition-transform cursor-pointer"
style={{ WebkitTapHighlightColor: 'transparent' }} style={{ WebkitTapHighlightColor: 'transparent' }}
aria-label={`Tap to count ${current.transliteration}`} aria-label={`Tap to count ${current.transliteration}`}
> >
@@ -244,7 +244,7 @@ export default function DhikrPage() {
{/* SVG progress ring */} {/* SVG progress ring */}
<svg width="200" height="200" className="-rotate-90" style={{ filter: `drop-shadow(0 0 8px ${current.glow})` }}> <svg width="200" height="200" className="-rotate-90" style={{ filter: `drop-shadow(0 0 8px ${current.glow})` }}>
{/* Track */} {/* Track */}
<circle cx="100" cy="100" r={R} fill="none" stroke="#e5e7eb" strokeWidth="8" /> <circle cx="100" cy="100" r={R} fill="none" stroke="var(--color-border)" strokeWidth="8" />
{/* Progress */} {/* Progress */}
<circle <circle
cx="100" cy="100" r={R} fill="none" cx="100" cy="100" r={R} fill="none"
@@ -259,37 +259,37 @@ export default function DhikrPage() {
{/* Center content */} {/* Center content */}
<div <div
className="absolute rounded-full flex flex-col items-center justify-center" className="absolute rounded-full flex flex-col items-center justify-center border border-border/60"
style={{ style={{
inset: 28, inset: 28,
background: `radial-gradient(circle at 40% 30%, ${current.bg}, #ffffff)`, background: `radial-gradient(circle at 40% 30%, var(--color-bg-card), var(--color-bg-deep))`,
}} }}
> >
<span className="text-5xl font-bold text-gray-900 tabular-nums leading-none">{count}</span> <span className="text-5xl font-extrabold text-white tabular-nums leading-none">{count}</span>
<span className="text-[11px] text-gray-400 mt-1">of {current.target}</span> <span className="text-[11px] text-text-secondary mt-1">of {current.target}</span>
</div> </div>
</button> </button>
{/* Dhikr text below ring, inside card */} {/* Dhikr text below ring, inside card */}
<div className="text-center mt-6 space-y-1.5"> <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="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="font-bold text-base" style={{ color: current.ring }}>{current.transliteration}</p>
<p className="text-gray-500 text-sm">{current.meaning}</p> <p className="text-text-secondary text-sm">{current.meaning}</p>
</div> </div>
</div> </div>
<p className="text-gray-400 text-xs mt-6">Tap to count</p> <p className="text-text-muted text-xs mt-6">Tap to count</p>
</div> </div>
)} )}
{/* Bottom progress bar */} {/* Bottom progress bar */}
{!justFinished && ( {!justFinished && (
<div className="px-5 pb-2 mt-4"> <div className="px-5 pb-8 mt-4">
<div className="flex items-center justify-between text-[11px] text-gray-400 mb-2"> <div className="flex items-center justify-between text-[11px] text-text-secondary mb-2">
<span>Total today</span> <span>Total today</span>
<span className="tabular-nums">{Math.min(totalCount, TOTAL)} / {TOTAL}</span> <span className="tabular-nums">{Math.min(totalCount, TOTAL)} / {TOTAL}</span>
</div> </div>
<div className="h-1.5 bg-gray-200 rounded-full overflow-hidden"> <div className="h-1.5 bg-bg-card border border-border rounded-full overflow-hidden">
<div <div
className="h-full rounded-full transition-all duration-300" className="h-full rounded-full transition-all duration-300"
style={{ style={{
+35 -35
View File
@@ -39,19 +39,19 @@ export default function ForumPage() {
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) } const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) }
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24"> <div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* Header */} {/* Header */}
<div className="px-5 pt-8 pb-4 bg-white flex items-center justify-between"> <div className="px-5 pt-14 pb-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{view === 'threads' && ( {view === 'threads' && (
<button onClick={() => { setView('categories'); setSelectedCat(null) }} className="w-9 h-9 flex items-center justify-center rounded-xl bg-gray-100 active:opacity-60"> <button onClick={() => { setView('categories'); setSelectedCat(null) }} className="w-9 h-9 flex items-center justify-center rounded-xl bg-bg-card border border-border active:opacity-60 cursor-pointer">
<ArrowLeft size={18} className="text-gray-500" /> <ArrowLeft size={18} className="text-text-secondary" />
</button> </button>
)} )}
<div> <div>
<h1 className="text-xl font-bold text-gray-900">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1> <h1 className="text-xl font-bold text-white">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
<p className="text-[11px] text-gray-400 mt-0.5"> <p className="text-[11px] text-text-secondary mt-0.5">
{view === 'categories' ? 'Community discussions' : `${threads.length} threads`} {view === 'categories' ? 'Community discussions' : `${threads.length} threads`}
</p> </p>
</div> </div>
@@ -59,7 +59,7 @@ export default function ForumPage() {
{token && ( {token && (
<button <button
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
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" className="flex items-center gap-1.5 gold-gradient text-bg-deep px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.97] transition-transform cursor-pointer shadow-sm shadow-gold/10"
> >
<Plus size={16} /> Post <Plus size={16} /> Post
</button> </button>
@@ -70,7 +70,7 @@ export default function ForumPage() {
{loading && ( {loading && (
<div className="px-5 pt-4 space-y-3"> <div className="px-5 pt-4 space-y-3">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div key={i} className="h-20 rounded-2xl bg-gray-200 animate-pulse" /> <div key={i} className="h-20 rounded-2xl bg-bg-card border border-border/50 animate-pulse" />
))} ))}
</div> </div>
)} )}
@@ -81,23 +81,23 @@ export default function ForumPage() {
{categories.length === 0 ? ( {categories.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
<div className="text-4xl mb-3">🕌</div> <div className="text-4xl mb-3">🕌</div>
<p className="text-gray-400 text-sm">No categories yet</p> <p className="text-text-secondary text-sm">No categories yet</p>
</div> </div>
) : categories.map(c => ( ) : categories.map(c => (
<button <button
key={c.id} key={c.id}
onClick={() => { setSelectedCat(c); setView('threads'); loadThreads(c.id) }} onClick={() => { setSelectedCat(c); setView('threads'); loadThreads(c.id) }}
className="w-full text-left bg-white rounded-2xl shadow-sm border border-gray-100 p-4 active:scale-[0.97] transition-transform" className="w-full text-left bg-bg-card rounded-2xl border border-border p-4 active:scale-[0.97] transition-transform hover:border-gold/30 cursor-pointer"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-2xl w-10 h-10 flex items-center justify-center bg-emerald-50 rounded-xl shrink-0"> <span className="text-2xl w-10 h-10 flex items-center justify-center bg-bg-deep border border-border rounded-xl shrink-0">
{c.icon || '💬'} {c.icon || '💬'}
</span> </span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 text-sm">{c.name}</h3> <h3 className="font-semibold text-white text-sm">{c.name}</h3>
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">{c.description}</p> <p className="text-xs text-text-secondary mt-0.5 line-clamp-1">{c.description}</p>
</div> </div>
<ChevronRight size={16} className="text-gray-300 shrink-0" /> <ChevronRight size={16} className="text-text-muted shrink-0" />
</div> </div>
</button> </button>
))} ))}
@@ -110,21 +110,21 @@ export default function ForumPage() {
{threads.length === 0 ? ( {threads.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
<div className="text-4xl mb-3">💬</div> <div className="text-4xl mb-3">💬</div>
<p className="text-gray-900 font-semibold mb-1">No threads yet</p> <p className="text-white font-semibold mb-1">No threads yet</p>
<p className="text-gray-400 text-sm">Start the first discussion</p> <p className="text-text-secondary text-sm">Start the first discussion</p>
</div> </div>
) : threads.map(t => ( ) : threads.map(t => (
<div key={t.id} className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4"> <div key={t.id} className="bg-bg-card rounded-2xl border border-border p-4">
<h3 className="font-semibold text-gray-900 text-sm leading-snug mb-1">{t.title}</h3> <h3 className="font-semibold text-white text-sm leading-snug mb-1">{t.title}</h3>
<p className="text-xs text-gray-400 line-clamp-2 mb-3">{t.content}</p> <p className="text-xs text-text-secondary line-clamp-2 mb-3">{t.content}</p>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<div className="w-5 h-5 rounded-full bg-emerald-100 flex items-center justify-center"> <div className="w-5 h-5 rounded-full bg-bg-deep border border-border flex items-center justify-center">
<span className="text-[9px] text-emerald-700 font-bold">{t.author.name[0]}</span> <span className="text-[9px] text-[#D4AF37] font-bold">{t.author.name[0]}</span>
</div> </div>
<span className="text-[11px] text-gray-400">{t.author.name}</span> <span className="text-[11px] text-text-secondary">{t.author.name}</span>
</div> </div>
<div className="flex items-center gap-1 text-[11px] text-gray-400"> <div className="flex items-center gap-1 text-[11px] text-text-secondary">
<MessageCircle size={12} /> <MessageCircle size={12} />
{t._count.posts} {t._count.posts}
</div> </div>
@@ -136,7 +136,7 @@ export default function ForumPage() {
{/* Toast */} {/* Toast */}
{toast && ( {toast && (
<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"> <div className="fixed bottom-24 left-5 right-5 bg-bg-card border border-border text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
{toast} {toast}
</div> </div>
)} )}
@@ -182,38 +182,38 @@ function CreateThreadSheet({ token, categories, onClose, onCreated, showToast }:
return ( return (
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}> <div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" /> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="relative bg-white rounded-t-3xl p-6 pb-10 space-y-4" className="relative bg-bg-card border-t border-x border-border rounded-t-3xl p-6 pb-10 space-y-4"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto absolute left-1/2 -translate-x-1/2 top-3" /> <div className="w-10 h-1 bg-border rounded-full mx-auto absolute left-1/2 -translate-y-1/2 top-3" />
<h2 className="text-lg font-bold text-gray-900 mt-2">New Thread</h2> <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-100 mt-2"> <button type="button" onClick={onClose} className="w-8 h-8 flex items-center justify-center rounded-full bg-bg-deep border border-border text-text-muted mt-2 cursor-pointer">
<X size={15} className="text-gray-400" /> <X size={15} />
</button> </button>
</div> </div>
<select <select
value={categoryId} onChange={e => setCategoryId(e.target.value)} value={categoryId} onChange={e => setCategoryId(e.target.value)}
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-3.5 text-sm text-white focus:border-gold focus:outline-none"
> >
{categories.map(c => <option key={c.id} value={c.id}>{c.icon} {c.name}</option>)} {categories.map(c => <option key={c.id} value={c.id} className="bg-bg-card">{c.icon} {c.name}</option>)}
</select> </select>
<input <input
type="text" placeholder="Thread title" value={title} onChange={e => setTitle(e.target.value)} required type="text" placeholder="Thread title" value={title} onChange={e => setTitle(e.target.value)} required
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-4 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none"
/> />
<textarea <textarea
placeholder="Share your thoughts…" value={content} onChange={e => setContent(e.target.value)} required rows={4} placeholder="Share your thoughts…" value={content} onChange={e => setContent(e.target.value)} required rows={4}
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-3 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none resize-none"
/> />
<button <button
type="submit" disabled={loading} type="submit" disabled={loading}
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform" className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform cursor-pointer"
> >
{loading ? 'Posting…' : 'Post Thread'} {loading ? 'Posting…' : 'Post Thread'}
</button> </button>
+9 -8
View File
@@ -67,18 +67,19 @@ input, select, textarea {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
/* Glassmorphism utility */ /* Glassmorphism utility (Spatial Vision OS style) */
.glass { .glass {
background: rgba(17, 17, 24, 0.95); background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(20px); backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
} }
.glass-light { .glass-light {
background: rgba(255, 255, 255, 0.05); background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px); backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(20px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
} }
+37 -37
View File
@@ -27,24 +27,24 @@ interface MapCenter {
function UpgradeCTA() { function UpgradeCTA() {
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24 flex flex-col items-center justify-center px-6 text-center space-y-4"> <div className="min-h-dvh bg-bg-deep pb-32 flex flex-col items-center justify-center px-6 text-center space-y-4 text-white">
<div className="w-16 h-16 rounded-2xl bg-amber-50 flex items-center justify-center"> <div className="w-16 h-16 rounded-2xl bg-bg-card border border-gold/20 flex items-center justify-center">
<Crown size={32} className="text-amber-500" /> <Crown size={32} className="text-gold" />
</div> </div>
<h1 className="text-2xl font-bold text-gray-900">Halal Monitor</h1> <h1 className="text-2xl font-bold text-white">Halal Monitor</h1>
<p className="text-gray-500 max-w-md text-sm"> <p className="text-text-secondary max-w-md text-sm">
Find mosques and halal restaurants anywhere in the world. Available to Premium members. Find mosques and halal restaurants anywhere in the world. Available to Premium members.
</p> </p>
<div className="bg-white border border-gray-100 shadow-md rounded-3xl p-6 max-w-sm w-full space-y-3"> <div className="bg-bg-card border border-border shadow-lg rounded-3xl p-6 max-w-sm w-full space-y-3">
<ul className="text-sm text-gray-500 space-y-2 text-left"> <ul className="text-sm text-text-secondary space-y-2 text-left">
<li className="flex items-center gap-2"><span className="text-emerald-500"></span> Mosques and prayer spaces worldwide</li> <li className="flex items-center gap-2"><span className="text-gold"></span> Mosques and prayer spaces worldwide</li>
<li className="flex items-center gap-2"><span className="text-emerald-500"></span> Halal restaurants near you</li> <li className="flex items-center gap-2"><span className="text-gold"></span> Halal restaurants near you</li>
<li className="flex items-center gap-2"><span className="text-emerald-500"></span> Bookmark favourite places</li> <li className="flex items-center gap-2"><span className="text-gold"></span> Bookmark favourite places</li>
<li className="flex items-center gap-2"><span className="text-emerald-500"></span> City search anywhere in the world</li> <li className="flex items-center gap-2"><span className="text-gold"></span> City search anywhere in the world</li>
</ul> </ul>
<a <a
href="/upgrade" href="/upgrade"
className="block w-full bg-emerald-600 text-white py-3.5 rounded-2xl font-bold active:scale-[0.97] transition-transform text-center" className="block w-full gold-gradient text-bg-deep py-3.5 rounded-2xl font-bold active:scale-[0.97] transition-transform text-center cursor-pointer shadow-sm shadow-gold/10"
> >
Upgrade Now Upgrade Now
</a> </a>
@@ -55,17 +55,17 @@ function UpgradeCTA() {
function LoginGate() { function LoginGate() {
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24 flex flex-col items-center justify-center px-6 text-center space-y-4"> <div className="min-h-dvh bg-bg-deep pb-32 flex flex-col items-center justify-center px-6 text-center space-y-4 text-white">
<div className="w-16 h-16 rounded-2xl bg-gray-100 flex items-center justify-center"> <div className="w-16 h-16 rounded-2xl bg-bg-card border border-border flex items-center justify-center">
<Lock size={32} className="text-gray-400" /> <Lock size={32} className="text-text-muted" />
</div> </div>
<h1 className="text-2xl font-bold text-gray-900">Halal Monitor</h1> <h1 className="text-2xl font-bold text-white">Halal Monitor</h1>
<p className="text-gray-500 max-w-md text-sm"> <p className="text-text-secondary max-w-md text-sm">
Sign in to access the global halal map mosques, prayer spaces, and halal restaurants near you. Sign in to access the global halal map mosques, prayer spaces, and halal restaurants near you.
</p> </p>
<a <a
href="/login" href="/login"
className="inline-flex items-center gap-2 bg-emerald-600 text-white px-8 py-4 rounded-2xl font-bold active:scale-[0.97] transition-transform" className="inline-flex items-center gap-2 gold-gradient text-bg-deep px-8 py-4 rounded-2xl font-bold active:scale-[0.97] transition-transform cursor-pointer shadow-sm shadow-gold/10"
> >
Sign In Sign In
</a> </a>
@@ -193,8 +193,8 @@ export default function HalalMonitorPage() {
if (authLoading) { if (authLoading) {
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24 flex items-center justify-center"> <div className="min-h-dvh bg-bg-deep pb-24 flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-emerald-300 border-t-emerald-600 animate-spin" /> <div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div> </div>
) )
} }
@@ -205,37 +205,37 @@ export default function HalalMonitorPage() {
const restaurants = places.filter(p => p.type === 'restaurant') const restaurants = places.filter(p => p.type === 'restaurant')
return ( return (
<div className="flex flex-col h-[calc(100vh-3.5rem)]"> <div className="flex flex-col h-[calc(100vh-3.5rem)] text-white bg-bg-deep">
{/* Toolbar */} {/* Toolbar */}
<div className="bg-white border-b border-gray-100 px-4 py-2.5 flex items-center gap-3 shrink-0 flex-wrap"> <div className="bg-bg-deep border-b border-border/80 px-4 py-2.5 flex items-center gap-3 shrink-0 flex-wrap">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Crown size={15} className="text-amber-500" /> <Crown size={15} className="text-gold" />
<span className="text-sm font-semibold text-gray-900">Halal Monitor</span> <span className="text-sm font-semibold text-white">Halal Monitor</span>
</div> </div>
<form onSubmit={handleCitySearch} className="flex gap-2 flex-1 max-w-sm"> <form onSubmit={handleCitySearch} className="flex gap-2 flex-1 max-w-sm">
<div className="relative flex-1"> <div className="relative flex-1">
<Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" /> <Search size={13} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text-muted" />
<input <input
type="text" type="text"
placeholder="Search city..." placeholder="Search city..."
value={citySearch} value={citySearch}
onChange={e => setCitySearch(e.target.value)} onChange={e => setCitySearch(e.target.value)}
className="w-full bg-gray-50 border border-gray-200 rounded-xl pl-8 pr-3 py-1.5 text-xs text-gray-900 placeholder-gray-400 focus:border-emerald-400 outline-none" className="w-full bg-bg-card border border-border rounded-xl pl-8 pr-3 py-1.5 text-xs text-white placeholder-text-muted focus:border-gold outline-none"
/> />
</div> </div>
<button type="submit" className="bg-emerald-600 text-white px-3 py-1.5 rounded-xl text-xs font-semibold active:scale-[0.97] transition-transform"> <button type="submit" className="gold-gradient text-bg-deep px-3 py-1.5 rounded-xl text-xs font-bold active:scale-[0.97] transition-transform cursor-pointer">
Go Go
</button> </button>
</form> </form>
<button <button
onClick={handleGeolocate} onClick={handleGeolocate}
className="flex items-center gap-1 text-xs text-gray-500 active:text-emerald-600 transition" className="flex items-center gap-1 text-xs text-text-secondary hover:text-gold transition cursor-pointer"
> >
<Navigation size={13} /> Near me <Navigation size={13} /> Near me
</button> </button>
{loading && <Loader2 size={14} className="animate-spin text-emerald-600" />} {loading && <Loader2 size={14} className="animate-spin text-gold" />}
{places.length > 0 && ( {places.length > 0 && (
<span className="text-xs text-gray-400 ml-auto"> <span className="text-xs text-text-muted ml-auto">
{mosques.length} mosques · {restaurants.length} restaurants {mosques.length} mosques · {restaurants.length} restaurants
</span> </span>
)} )}
@@ -243,21 +243,21 @@ export default function HalalMonitorPage() {
{/* Error bar */} {/* Error bar */}
{error && ( {error && (
<div className="bg-red-50 border-b border-red-100 px-4 py-2 flex items-center justify-between shrink-0"> <div className="bg-red-950/20 border-b border-red-900/40 px-4 py-2 flex items-center justify-between shrink-0">
<span className="text-xs text-red-600">{error}</span> <span className="text-xs text-red-400">{error}</span>
<button onClick={() => setError(null)}><X size={14} className="text-red-400" /></button> <button onClick={() => setError(null)} className="cursor-pointer text-red-400 hover:text-red-300"><X size={14} /></button>
</div> </div>
)} )}
{/* Map */} {/* Map */}
<div className="flex-1 relative"> <div className="flex-1 relative">
{!center ? ( {!center ? (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center space-y-4 bg-[#f5f4f0]"> <div className="absolute inset-0 flex flex-col items-center justify-center text-center space-y-4 bg-bg-deep text-white">
<MapPin size={40} className="text-gray-300" /> <MapPin size={40} className="text-text-muted" />
<p className="text-gray-500 text-sm">Search a city or use your location to find halal places.</p> <p className="text-text-secondary text-sm">Search a city or use your location to find halal places.</p>
<button <button
onClick={handleGeolocate} onClick={handleGeolocate}
className="flex items-center gap-2 bg-emerald-600 text-white px-5 py-3 rounded-2xl text-sm font-semibold active:scale-[0.97] transition-transform" className="flex items-center gap-2 gold-gradient text-bg-deep px-5 py-3 rounded-2xl text-sm font-bold active:scale-[0.97] transition-transform cursor-pointer shadow-sm shadow-gold/10"
> >
<Navigation size={16} /> Use My Location <Navigation size={16} /> Use My Location
</button> </button>
+6 -6
View File
@@ -171,7 +171,7 @@ function PostCard({ post }: { post: (typeof POSTS)[number] }) {
} }
return ( return (
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden"> <div className="glass rounded-[24px] overflow-hidden">
{/* Author row */} {/* Author row */}
<div className="flex items-center gap-3 px-4 pt-4 pb-2"> <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"> <div className="w-9 h-9 rounded-full bg-[#D4AF37]/20 flex items-center justify-center shrink-0">
@@ -343,7 +343,7 @@ export default function HomePage() {
</h1> </h1>
<p className="text-[#999] text-sm mt-0.5">Welcome to Falah</p> <p className="text-[#999] text-sm mt-0.5">Welcome to Falah</p>
</div> </div>
<button className="w-9 h-9 bg-[#111118] rounded-xl flex items-center justify-center border border-[#222] active:scale-90 transition-transform"> <button className="w-9 h-9 glass-light rounded-xl flex items-center justify-center active:scale-90 transition-transform">
<Bell size={17} className="text-[#999]" /> <Bell size={17} className="text-[#999]" />
</button> </button>
</div> </div>
@@ -351,7 +351,7 @@ export default function HomePage() {
{/* ─── 1. Prayer Countdown Ring ─── */} {/* ─── 1. Prayer Countdown Ring ─── */}
<div className="mx-5 mb-5"> <div className="mx-5 mb-5">
<div className="bg-[#111118] rounded-2xl border border-[#222] p-5"> <div className="glass rounded-[24px] p-5">
<div className="flex items-center gap-5"> <div className="flex items-center gap-5">
{/* SVG Ring */} {/* SVG Ring */}
<div className="relative shrink-0"> <div className="relative shrink-0">
@@ -402,7 +402,7 @@ export default function HomePage() {
{/* ─── 2. AI Morning Briefing ─── */} {/* ─── 2. AI Morning Briefing ─── */}
<div className="mx-5 mb-5"> <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"> <div className="glass rounded-[24px] p-5 relative overflow-hidden bg-gradient-to-br from-[#D4AF37]/5 to-transparent">
{/* Decorative glow */} {/* Decorative glow */}
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#D4AF37]/10 rounded-full blur-3xl" /> <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="relative z-10">
@@ -433,7 +433,7 @@ export default function HomePage() {
{/* ─── 3. Verse of the Day ─── */} {/* ─── 3. Verse of the Day ─── */}
<div className="mx-5 mb-5"> <div className="mx-5 mb-5">
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden"> <div className="glass rounded-[24px] overflow-hidden">
{/* Gold header bar */} {/* Gold header bar */}
<div className="gold-gradient px-4 py-2.5 flex items-center gap-2"> <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"> <span className="text-white text-xs font-bold uppercase tracking-wider">
@@ -461,7 +461,7 @@ export default function HomePage() {
{/* ─── 4. Quick Dhikr ─── */} {/* ─── 4. Quick Dhikr ─── */}
<div className="mx-5 mb-5"> <div className="mx-5 mb-5">
<div className="bg-[#111118] rounded-2xl border border-[#222] p-5"> <div className="glass rounded-[24px] p-5">
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-base">📿</span> <span className="text-base">📿</span>
+16 -16
View File
@@ -30,20 +30,20 @@ export default function LoginPage() {
} }
return ( return (
<div className="min-h-dvh flex flex-col bg-white px-6"> <div className="min-h-dvh flex flex-col bg-bg-deep px-6">
{/* Brand */} {/* Brand */}
<div className="flex flex-col items-center pt-16 pb-10"> <div className="flex flex-col items-center pt-16 pb-10">
<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"> <div className="w-20 h-20 rounded-3xl gold-gradient flex items-center justify-center mb-5 shadow-lg shadow-gold/20">
<span className="text-3xl font-bold text-white">ف</span> <span className="text-3xl font-bold text-bg-deep">ف</span>
</div> </div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Welcome back</h1> <h1 className="text-2xl font-bold text-white tracking-tight">Welcome back</h1>
<p className="text-gray-500 text-sm mt-1">Sign in to your Falah account</p> <p className="text-text-secondary text-sm mt-1">Sign in to your Falah account</p>
</div> </div>
{/* Form */} {/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 flex-1"> <form onSubmit={handleSubmit} className="space-y-4 flex-1">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Email</label> <label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Email</label>
<input <input
type="email" type="email"
placeholder="you@example.com" placeholder="you@example.com"
@@ -52,12 +52,12 @@ export default function LoginPage() {
required required
autoComplete="email" autoComplete="email"
inputMode="email" inputMode="email"
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" className="w-full bg-bg-card border border-border rounded-xl px-4 py-4 text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Password</label> <label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Password</label>
<div className="relative"> <div className="relative">
<input <input
type={showPass ? 'text' : 'password'} type={showPass ? 'text' : 'password'}
@@ -66,12 +66,12 @@ export default function LoginPage() {
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
required required
autoComplete="current-password" autoComplete="current-password"
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" className="w-full bg-bg-card border border-border rounded-xl px-4 py-4 pr-14 text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
<button <button
type="button" type="button"
onClick={() => setShowPass(v => !v)} onClick={() => setShowPass(v => !v)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 p-1" className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted p-1"
> >
{showPass ? <EyeOff size={20} /> : <Eye size={20} />} {showPass ? <EyeOff size={20} /> : <Eye size={20} />}
</button> </button>
@@ -79,28 +79,28 @@ export default function LoginPage() {
</div> </div>
{error && ( {error && (
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3"> <div className="bg-red-950/20 border border-red-900/40 rounded-xl px-4 py-3">
<p className="text-red-500 text-sm">{error}</p> <p className="text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
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" className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.97] transition-all mt-2 shadow-sm shadow-gold/10 cursor-pointer"
> >
{loading ? 'Signing in...' : 'Sign In'} {loading ? 'Signing in...' : 'Sign In'}
</button> </button>
</form> </form>
<div className="py-8 space-y-4 text-center"> <div className="py-8 space-y-4 text-center">
<p className="text-sm text-gray-500"> <p className="text-sm text-text-secondary">
No account?{' '} No account?{' '}
<Link href="/register" className="text-emerald-600 font-semibold"> <Link href="/register" className="text-gold font-semibold">
Create one free Create one free
</Link> </Link>
</p> </p>
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p> <p className="text-xs text-text-muted">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
</div> </div>
</div> </div>
) )
+9 -11
View File
@@ -129,7 +129,7 @@ export default function NurPage() {
}, []) }, [])
return ( return (
<div className="min-h-dvh bg-[#0a0a0f] flex flex-col pb-32 select-none"> <div className="min-h-dvh bg-bg-deep flex flex-col pb-32 select-none">
{/* ═══ Header ═══ */} {/* ═══ Header ═══ */}
<div className="shrink-0 flex items-center gap-3 px-5 pt-14 pb-3"> <div className="shrink-0 flex items-center gap-3 px-5 pt-14 pb-3">
{/* Avatar */} {/* Avatar */}
@@ -149,19 +149,18 @@ export default function NurPage() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Link <Link
href="/home" href="/home"
className="w-9 h-9 rounded-xl bg-[#111118] border border-[#222] flex items-center justify-center active:scale-90 transition-transform" className="w-9 h-9 rounded-xl glass-light flex items-center justify-center active:scale-90 transition-transform"
> >
<House size={17} className="text-[#D4AF37]" /> <House size={17} className="text-[#D4AF37]" />
</Link> </Link>
<button className="w-9 h-9 rounded-xl bg-[#111118] border border-[#222] flex items-center justify-center active:scale-90 transition-transform"> <button className="w-9 h-9 rounded-xl glass-light flex items-center justify-center active:scale-90 transition-transform">
<Settings size={16} className="text-[#666]" /> <Settings size={16} className="text-[#666]" />
</button> </button>
</div> </div>
</div> </div>
{/* ═══ Mood Check-in ═══ */}
<div className="shrink-0 px-5 pb-3"> <div className="shrink-0 px-5 pb-3">
<div className="bg-[#111118] rounded-2xl border border-[#222] px-4 py-3"> <div className="glass rounded-2xl px-4 py-3">
<p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-2.5"> <p className="text-[11px] text-[#666] font-medium uppercase tracking-widest mb-2.5">
How are you feeling? How are you feeling?
</p> </p>
@@ -214,8 +213,8 @@ export default function NurPage() {
<div <div
className={`max-w-[80%] px-4 py-3 ${ className={`max-w-[80%] px-4 py-3 ${
msg.role === 'user' msg.role === 'user'
? 'gold-gradient text-[#0a0a0f] rounded-2xl rounded-br-sm' ? 'gold-gradient text-[#0a0a0f] rounded-2xl rounded-br-sm font-semibold'
: 'bg-[#1a1a2e] border border-[rgba(167,139,250,0.2)] text-white rounded-2xl rounded-bl-sm' : 'glass-light text-white rounded-2xl rounded-bl-sm'
}`} }`}
> >
<p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.content}</p> <p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.content}</p>
@@ -240,7 +239,7 @@ export default function NurPage() {
<button <button
key={cmd} key={cmd}
onClick={() => setInput(cmd + ' ')} onClick={() => setInput(cmd + ' ')}
className="shrink-0 bg-[#111118] border border-[#222] rounded-full px-4 py-1.5 active:scale-95 transition-transform" className="shrink-0 glass-light rounded-full px-4 py-1.5 active:scale-95 transition-transform"
> >
<span className="text-xs text-[#D4AF37] font-medium">{cmd}</span> <span className="text-xs text-[#D4AF37] font-medium">{cmd}</span>
</button> </button>
@@ -248,9 +247,8 @@ export default function NurPage() {
</div> </div>
</div> </div>
{/* ═══ Input Bar (fixed at bottom above nav) ═══ */}
<div className="shrink-0 px-5 pb-1"> <div className="shrink-0 px-5 pb-1">
<div className="flex items-center gap-2 bg-[#111118] border border-[#222] rounded-[28px] px-4 py-2"> <div className="flex items-center gap-2 glass rounded-[28px] px-4 py-2">
<input <input
type="text" type="text"
value={input} value={input}
@@ -286,7 +284,7 @@ export default function NurPage() {
className={`shrink-0 rounded-full px-3.5 py-1.5 text-xs font-semibold active:scale-95 transition-all ${ className={`shrink-0 rounded-full px-3.5 py-1.5 text-xs font-semibold active:scale-95 transition-all ${
isActive isActive
? 'bg-[#D4AF37]/15 border border-[#D4AF37]/40 text-[#D4AF37]' ? 'bg-[#D4AF37]/15 border border-[#D4AF37]/40 text-[#D4AF37]'
: 'bg-[#111118] border border-[#222] text-[#666]' : 'glass-light text-[#666]'
}`} }`}
> >
{p.name} {p.name}
+38 -38
View File
@@ -140,10 +140,10 @@ export default function PrayerPage() {
const strokeOffset = C * (1 - arcPercent) const strokeOffset = C * (1 - arcPercent)
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] flex flex-col pb-24 select-none"> <div className="min-h-dvh bg-bg-deep flex flex-col pb-32 select-none text-white">
{/* Header gradient card */} {/* Header gradient card */}
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 rounded-b-3xl px-5 pt-10 pb-8"> <div className="bg-gradient-to-br from-emerald-950/40 via-bg-card to-bg-deep border-b border-border/80 rounded-b-3xl px-5 pt-14 pb-8">
{/* Top bar inside header */} {/* Top bar inside header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
@@ -151,15 +151,15 @@ export default function PrayerPage() {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{city ? ( {city ? (
<> <>
<MapPin size={12} className="text-white/70" /> <MapPin size={12} className="text-gold" />
<span className="text-xs text-white/80">{city}</span> <span className="text-xs text-text-secondary">{city}</span>
</> </>
) : ( ) : (
<span className="text-xs text-white/60">Detecting location</span> <span className="text-xs text-text-muted">Detecting location</span>
)} )}
</div> </div>
{hijri && ( {hijri && (
<p className="text-[11px] text-white/60 mt-0.5"> <p className="text-[11px] text-text-muted mt-0.5">
{hijri.date} {hijri.month.en} {hijri.year} AH {hijri.date} {hijri.month.en} {hijri.year} AH
</p> </p>
)} )}
@@ -167,9 +167,9 @@ export default function PrayerPage() {
<button <button
onClick={requestLocation} onClick={requestLocation}
disabled={loading} disabled={loading}
className="w-9 h-9 rounded-xl bg-white/10 flex items-center justify-center active:scale-[0.97] transition-transform" className="w-9 h-9 rounded-xl bg-bg-card border border-border flex items-center justify-center active:scale-[0.97] transition-transform cursor-pointer"
> >
<RefreshCw size={15} className={`text-white/80 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw size={15} className={`text-text-secondary ${loading ? 'animate-spin' : ''}`} />
</button> </button>
</div> </div>
@@ -180,7 +180,7 @@ export default function PrayerPage() {
<div className="relative flex items-center justify-center mb-5"> <div className="relative flex items-center justify-center mb-5">
<svg width="160" height="160" className="-rotate-90"> <svg width="160" height="160" className="-rotate-90">
{/* Background ring */} {/* Background ring */}
<circle cx="80" cy="80" r={R} fill="none" stroke="rgba(255,255,255,0.15)" strokeWidth="6" /> <circle cx="80" cy="80" r={R} fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="6" />
{/* Progress arc */} {/* Progress arc */}
<circle <circle
cx="80" cy="80" r={R} fill="none" cx="80" cy="80" r={R} fill="none"
@@ -194,7 +194,7 @@ export default function PrayerPage() {
{/* Glow dot at end of arc */} {/* Glow dot at end of arc */}
<circle <circle
cx="80" cy="80" r={R} fill="none" cx="80" cy="80" r={R} fill="none"
stroke="rgba(255,255,255,0.4)" stroke="rgba(255,255,255,0.3)"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeDasharray="1" strokeDasharray="1"
@@ -205,18 +205,18 @@ export default function PrayerPage() {
{/* Center content */} {/* Center content */}
<div className="absolute inset-0 flex flex-col items-center justify-center"> <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-3xl mb-0.5">{PRAYER_DATA[next.name].emoji}</span>
<span className="text-[10px] text-white/60 uppercase tracking-widest">Next Prayer</span> <span className="text-[10px] text-text-muted uppercase tracking-widest">Next Prayer</span>
<span className="text-base font-bold text-white mt-0.5">{next.name}</span> <span className="text-base font-bold text-white mt-0.5">{next.name}</span>
</div> </div>
</div> </div>
{/* Countdown */} {/* Countdown */}
<div className="text-center"> <div className="text-center">
<p className="text-5xl font-bold text-white tabular-nums leading-none tracking-tight"> <p className="text-5xl font-extrabold text-white tabular-nums leading-none tracking-tight">
{countdown.value} {countdown.value}
</p> </p>
<p className="text-sm text-white/60 mt-1.5">{countdown.unit}</p> <p className="text-sm text-text-secondary mt-1.5">{countdown.unit}</p>
<p className="text-xs text-white/50 mt-1"> <p className="text-xs text-text-muted mt-1">
{next.name} at {fmt12(timings[next.name])} {next.name} at {fmt12(timings[next.name])}
</p> </p>
</div> </div>
@@ -226,22 +226,22 @@ export default function PrayerPage() {
{/* Loading skeleton inside header */} {/* Loading skeleton inside header */}
{loading && !timings && ( {loading && !timings && (
<div className="flex flex-col items-center gap-3 py-4"> <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-40 h-40 rounded-full bg-bg-card border border-border animate-pulse" />
<div className="w-24 h-8 rounded-xl bg-white/10 animate-pulse mt-2" /> <div className="w-24 h-8 rounded-xl bg-bg-card border border-border animate-pulse mt-2" />
<div className="w-32 h-4 rounded-lg bg-white/10 animate-pulse" /> <div className="w-32 h-4 rounded-lg bg-bg-card border border-border animate-pulse" />
</div> </div>
)} )}
</div> </div>
{/* Location denied */} {/* Location denied */}
{locationDenied && ( {locationDenied && (
<div className="mx-5 mt-5 bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center"> <div className="mx-5 mt-5 bg-bg-card border border-border rounded-3xl p-8 text-center">
<div className="text-4xl mb-4">🕌</div> <div className="text-4xl mb-4">🕌</div>
<p className="text-gray-900 font-bold text-lg mb-1">Enable Location</p> <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> <p className="text-text-secondary text-sm mb-6">Prayer times require your location to be accurate</p>
<button <button
onClick={requestLocation} 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" className="w-full gold-gradient text-bg-deep font-bold py-4 rounded-2xl active:scale-[0.97] transition-transform cursor-pointer"
> >
Allow Location Access Allow Location Access
</button> </button>
@@ -250,8 +250,8 @@ export default function PrayerPage() {
{/* Error */} {/* Error */}
{error && !locationDenied && ( {error && !locationDenied && (
<div className="mx-5 mt-4 bg-red-50 border border-red-100 rounded-2xl px-4 py-3 text-center"> <div className="mx-5 mt-4 bg-red-950/20 border border-red-900/40 rounded-2xl px-4 py-3 text-center">
<p className="text-red-500 text-sm">{error}</p> <p className="text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
@@ -268,10 +268,10 @@ export default function PrayerPage() {
key={prayer} key={prayer}
className={`flex items-center gap-3 px-4 py-3.5 rounded-2xl transition-all ${ className={`flex items-center gap-3 px-4 py-3.5 rounded-2xl transition-all ${
isNext isNext
? 'bg-amber-50 border border-[#D4AF37]' ? 'bg-[#D4AF37]/10 border border-[#D4AF37]/40'
: isPast : isPast
? 'bg-white border border-gray-100 shadow-sm opacity-50' ? 'bg-bg-card/60 border border-border/60 shadow-sm opacity-50'
: 'bg-white border border-gray-100 shadow-sm' : 'bg-bg-card border border-border shadow-sm'
}`} }`}
> >
{/* Emoji */} {/* Emoji */}
@@ -280,7 +280,7 @@ export default function PrayerPage() {
{/* Names */} {/* Names */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className={`font-semibold text-sm ${isNext ? 'text-emerald-700' : 'text-gray-900'}`}> <span className={`font-semibold text-sm ${isNext ? 'text-[#D4AF37]' : 'text-white'}`}>
{prayer} {prayer}
</span> </span>
{isNext && ( {isNext && (
@@ -289,11 +289,11 @@ export default function PrayerPage() {
</span> </span>
)} )}
</div> </div>
<span className="text-[11px] text-gray-500">{PRAYER_DATA[prayer].arabic}</span> <span className="text-[11px] text-text-secondary">{PRAYER_DATA[prayer].arabic}</span>
</div> </div>
{/* Time */} {/* Time */}
<span className={`text-sm font-bold tabular-nums ${isNext ? 'text-[#D4AF37]' : 'text-gray-500'}`}> <span className={`text-sm font-bold tabular-nums ${isNext ? 'text-[#D4AF37]' : 'text-text-secondary'}`}>
{fmt12(timings[prayer])} {fmt12(timings[prayer])}
</span> </span>
</div> </div>
@@ -301,10 +301,10 @@ export default function PrayerPage() {
})} })}
{/* Sunrise */} {/* Sunrise */}
<div className="flex items-center gap-3 px-4 py-2.5 bg-white rounded-2xl border border-gray-100 shadow-sm opacity-40"> <div className="flex items-center gap-3 px-4 py-2.5 bg-bg-card/40 rounded-2xl border border-border/40 shadow-sm opacity-40">
<span className="text-lg w-7 text-center">🌄</span> <span className="text-lg w-7 text-center">🌄</span>
<span className="flex-1 text-xs text-gray-500">Sunrise</span> <span className="flex-1 text-xs text-text-muted">Sunrise</span>
<span className="text-xs text-gray-500 tabular-nums">{fmt12(timings.Sunrise)}</span> <span className="text-xs text-text-muted tabular-nums">{fmt12(timings.Sunrise)}</span>
</div> </div>
</div> </div>
)} )}
@@ -313,13 +313,13 @@ export default function PrayerPage() {
{loading && !timings && ( {loading && !timings && (
<div className="px-4 mt-5 space-y-2"> <div className="px-4 mt-5 space-y-2">
{[...Array(5)].map((_, i) => ( {[...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 key={i} className="bg-bg-card rounded-2xl border border-border px-4 py-3.5 flex items-center gap-3 animate-pulse">
<div className="w-7 h-7 rounded-full bg-gray-200 animate-pulse" /> <div className="w-7 h-7 rounded-full bg-bg-deep border border-border" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<div className="h-3.5 w-20 rounded bg-gray-200 animate-pulse" /> <div className="h-3.5 w-20 rounded bg-bg-deep" />
<div className="h-2.5 w-12 rounded bg-gray-200 animate-pulse" /> <div className="h-2.5 w-12 rounded bg-bg-deep" />
</div> </div>
<div className="h-3.5 w-16 rounded bg-gray-200 animate-pulse" /> <div className="h-3.5 w-16 rounded bg-bg-deep" />
</div> </div>
))} ))}
</div> </div>
@@ -327,7 +327,7 @@ export default function PrayerPage() {
{/* Footer note */} {/* Footer note */}
{timings && ( {timings && (
<p className="text-center text-[10px] text-gray-400 mt-8 mb-2"> <p className="text-center text-[10px] text-text-muted mt-8 mb-2">
Muslim World League (MWL) · AlAdhan Muslim World League (MWL) · AlAdhan
</p> </p>
)} )}
+17 -8
View File
@@ -208,15 +208,24 @@ export default function ProfilePage() {
</div> </div>
</div> </div>
{/* ═══ Sign Out ═══ */} {/* ═══ Sign Out / Login ═══ */}
<div className="mx-5 mb-6"> <div className="mx-5 mb-6">
<button {!user ? (
onClick={handleSignOut} <Link
className="w-full flex items-center justify-center gap-2 py-4 rounded-2xl border border-red-900/40 bg-red-950/20 active:bg-red-950/40 transition-colors" href="/login"
> className="w-full flex items-center justify-center gap-2 py-4 rounded-2xl gold-gradient text-bg-deep font-bold active:scale-[0.97] transition-all text-center"
<LogOut size={16} className="text-red-400" /> >
<span className="text-sm font-semibold text-red-400">Sign Out</span> Login
</button> </Link>
) : (
<button
onClick={handleSignOut}
className="w-full flex items-center justify-center gap-2 py-4 rounded-2xl border border-red-900/40 bg-red-950/20 active:bg-red-950/40 transition-colors cursor-pointer"
>
<LogOut size={16} className="text-red-400" />
<span className="text-sm font-semibold text-red-400">Logout</span>
</button>
)}
</div> </div>
{/* Bottom spacer */} {/* Bottom spacer */}
+21 -21
View File
@@ -31,24 +31,24 @@ export default function RegisterPage() {
} }
return ( return (
<div className="min-h-dvh flex flex-col bg-white px-6"> <div className="min-h-dvh flex flex-col bg-bg-deep px-6">
{/* Brand */} {/* Brand */}
<div className="flex flex-col items-center pt-12 pb-8"> <div className="flex flex-col items-center pt-12 pb-8">
<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"> <div className="w-20 h-20 rounded-3xl gold-gradient flex items-center justify-center mb-5 shadow-lg shadow-gold/20">
<span className="text-3xl font-bold text-white">ف</span> <span className="text-3xl font-bold text-bg-deep">ف</span>
</div> </div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Join Falah</h1> <h1 className="text-2xl font-bold text-white tracking-tight">Join Falah</h1>
<p className="text-gray-500 text-sm mt-1">Your Islamic lifestyle companion</p> <p className="text-text-secondary text-sm mt-1">Your Islamic lifestyle companion</p>
<div className="flex items-center gap-1.5 mt-4 bg-emerald-50 border border-emerald-200 rounded-full px-3.5 py-2"> <div className="flex items-center gap-1.5 mt-4 bg-emerald-950/20 border border-emerald-900/40 rounded-full px-3.5 py-2">
<Sparkles size={13} className="text-emerald-600" /> <Sparkles size={13} className="text-emerald-400" />
<span className="text-xs text-emerald-700 font-semibold">7-day Premium trial free</span> <span className="text-xs text-emerald-400 font-semibold">7-day Premium trial free</span>
</div> </div>
</div> </div>
{/* Form */} {/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 flex-1"> <form onSubmit={handleSubmit} className="space-y-4 flex-1">
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Your Name</label> <label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Your Name</label>
<input <input
type="text" type="text"
placeholder="Ahmad Abdullah" placeholder="Ahmad Abdullah"
@@ -56,12 +56,12 @@ export default function RegisterPage() {
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
required required
autoComplete="name" autoComplete="name"
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" className="w-full bg-bg-card border border-border rounded-xl px-4 py-4 text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Email</label> <label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Email</label>
<input <input
type="email" type="email"
placeholder="you@example.com" placeholder="you@example.com"
@@ -70,12 +70,12 @@ export default function RegisterPage() {
required required
autoComplete="email" autoComplete="email"
inputMode="email" inputMode="email"
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" className="w-full bg-bg-card border border-border rounded-xl px-4 py-4 text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-xs font-semibold text-gray-500 uppercase tracking-widest">Password</label> <label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Password</label>
<div className="relative"> <div className="relative">
<input <input
type={showPass ? 'text' : 'password'} type={showPass ? 'text' : 'password'}
@@ -85,12 +85,12 @@ export default function RegisterPage() {
required required
minLength={8} minLength={8}
autoComplete="new-password" autoComplete="new-password"
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" className="w-full bg-bg-card border border-border rounded-xl px-4 py-4 pr-14 text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
<button <button
type="button" type="button"
onClick={() => setShowPass(v => !v)} onClick={() => setShowPass(v => !v)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 p-1" className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted p-1"
> >
{showPass ? <EyeOff size={20} /> : <Eye size={20} />} {showPass ? <EyeOff size={20} /> : <Eye size={20} />}
</button> </button>
@@ -98,28 +98,28 @@ export default function RegisterPage() {
</div> </div>
{error && ( {error && (
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3"> <div className="bg-red-950/20 border border-red-900/40 rounded-xl px-4 py-3">
<p className="text-red-500 text-sm">{error}</p> <p className="text-red-400 text-sm">{error}</p>
</div> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
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" className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold text-base disabled:opacity-50 active:scale-[0.97] transition-all mt-1 shadow-sm shadow-gold/10 cursor-pointer"
> >
{loading ? 'Creating account...' : 'Create Free Account'} {loading ? 'Creating account...' : 'Create Free Account'}
</button> </button>
</form> </form>
<div className="py-8 space-y-3 text-center"> <div className="py-8 space-y-3 text-center">
<p className="text-sm text-gray-500"> <p className="text-sm text-text-secondary">
Already have an account?{' '} Already have an account?{' '}
<Link href="/login" className="text-emerald-600 font-semibold"> <Link href="/login" className="text-gold font-semibold">
Sign in Sign in
</Link> </Link>
</p> </p>
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p> <p className="text-xs text-text-muted">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
</div> </div>
</div> </div>
) )
+46 -46
View File
@@ -61,19 +61,19 @@ export default function SouqPage() {
} }
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24 select-none"> <div className="min-h-dvh bg-bg-deep pb-32 select-none text-white">
{/* Header */} {/* Header */}
<div className="px-5 pt-8 pb-4 bg-white"> <div className="px-5 pt-14 pb-4">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
<h1 className="text-xl font-bold text-gray-900">Souq</h1> <h1 className="text-xl font-bold text-white">Souq</h1>
<p className="text-[11px] text-gray-400 mt-0.5">Islamic digital marketplace</p> <p className="text-[11px] text-text-secondary mt-0.5">Islamic digital marketplace</p>
</div> </div>
{token && ( {token && (
<button <button
onClick={() => setShowCreate(true)} onClick={() => setShowCreate(true)}
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" className="flex items-center gap-1.5 gold-gradient text-bg-deep px-4 py-2.5 rounded-xl font-bold text-sm active:scale-[0.97] transition-transform cursor-pointer shadow-sm shadow-gold/10"
> >
<Plus size={16} /> Sell <Plus size={16} /> Sell
</button> </button>
@@ -82,27 +82,27 @@ export default function SouqPage() {
{/* Search */} {/* Search */}
<div className="relative"> <div className="relative">
<Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-400" /> <Search size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-text-muted" />
<input <input
type="search" type="search"
placeholder="Search products…" placeholder="Search products…"
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
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" className="w-full bg-bg-card border border-border rounded-xl pl-11 pr-4 py-4 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none transition"
/> />
</div> </div>
</div> </div>
{/* Category chips */} {/* Category chips */}
<div className="flex gap-2 px-5 py-4 overflow-x-auto scrollbar-none bg-white border-b border-gray-100" style={{ scrollbarWidth: 'none' }}> <div className="flex gap-2 px-5 py-4 overflow-x-auto scrollbar-none border-b border-border/40" style={{ scrollbarWidth: 'none' }}>
{CATEGORIES.map(c => ( {CATEGORIES.map(c => (
<button <button
key={c} key={c}
onClick={() => setCategory(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 ${ 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 cursor-pointer ${
category === c category === c
? 'bg-emerald-600 text-white' ? 'gold-gradient text-bg-deep shadow-sm shadow-gold/15'
: 'bg-white text-gray-500 border border-gray-200' : 'bg-bg-card text-text-secondary border border-border'
}`} }`}
> >
<span>{CATEGORY_ICONS[c]}</span> <span>{CATEGORY_ICONS[c]}</span>
@@ -115,7 +115,7 @@ export default function SouqPage() {
{loading && ( {loading && (
<div className="px-5 pt-4 space-y-3"> <div className="px-5 pt-4 space-y-3">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div key={i} className="h-28 rounded-2xl bg-gray-200 animate-pulse" /> <div key={i} className="h-28 rounded-2xl bg-bg-card border border-border/50 animate-pulse" />
))} ))}
</div> </div>
)} )}
@@ -124,8 +124,8 @@ export default function SouqPage() {
{!loading && filtered.length === 0 && ( {!loading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center pt-20 px-6 text-center"> <div className="flex flex-col items-center justify-center pt-20 px-6 text-center">
<div className="text-5xl mb-4">🏪</div> <div className="text-5xl mb-4">🏪</div>
<p className="text-gray-900 font-semibold mb-1">Nothing here yet</p> <p className="text-white font-semibold mb-1">Nothing here yet</p>
<p className="text-gray-400 text-sm">Be the first to list a product</p> <p className="text-text-secondary text-sm">Be the first to list a product</p>
</div> </div>
)} )}
@@ -136,25 +136,25 @@ export default function SouqPage() {
<button <button
key={l.id} key={l.id}
onClick={() => setSelectedListing(l)} onClick={() => setSelectedListing(l)}
className="w-full text-left bg-white rounded-2xl shadow-sm border border-gray-100 p-4 active:scale-[0.97] transition-transform" className="w-full text-left bg-bg-card rounded-2xl border border-border p-4 active:scale-[0.97] transition-transform hover:border-gold/30 cursor-pointer"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{/* Icon block */} {/* Icon block */}
<div className="w-12 h-12 rounded-xl bg-gray-100 flex items-center justify-center shrink-0 text-xl"> <div className="w-12 h-12 rounded-xl bg-bg-deep border border-border flex items-center justify-center shrink-0 text-xl">
{CATEGORY_ICONS[l.category] || '📦'} {CATEGORY_ICONS[l.category] || '📦'}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<h3 className="font-semibold text-gray-900 text-sm leading-tight line-clamp-1">{l.title}</h3> <h3 className="font-semibold text-white text-sm leading-tight line-clamp-1">{l.title}</h3>
{l.featured && <Zap size={13} className="text-[#D4AF37] shrink-0 mt-0.5" />} {l.featured && <Zap size={13} className="text-[#D4AF37] shrink-0 mt-0.5" />}
</div> </div>
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">{l.description}</p> <p className="text-xs text-text-secondary mt-0.5 line-clamp-1">{l.description}</p>
<div className="flex items-center justify-between mt-2"> <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-[#D4AF37] font-bold text-sm">{l.price_flh.toLocaleString()} FLH</span>
<span className="text-[11px] text-gray-400">{l.seller}</span> <span className="text-[11px] text-text-muted">{l.seller}</span>
</div> </div>
</div> </div>
<ChevronRight size={16} className="text-gray-300 shrink-0 mt-1" /> <ChevronRight size={16} className="text-text-muted shrink-0 mt-1" />
</div> </div>
</button> </button>
))} ))}
@@ -163,7 +163,7 @@ export default function SouqPage() {
{/* Toast */} {/* Toast */}
{toast && ( {toast && (
<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"> <div className="fixed bottom-24 left-5 right-5 bg-bg-card border border-border text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
{toast} {toast}
</div> </div>
)} )}
@@ -171,46 +171,46 @@ export default function SouqPage() {
{/* Detail bottom sheet */} {/* Detail bottom sheet */}
{selectedListing && ( {selectedListing && (
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={() => setSelectedListing(null)}> <div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={() => setSelectedListing(null)}>
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" /> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div <div
className="relative bg-white rounded-t-3xl p-6 pb-10 max-h-[80dvh] overflow-y-auto" className="relative bg-bg-card border-t border-x border-border rounded-t-3xl p-6 pb-10 max-h-[80dvh] overflow-y-auto"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
{/* Handle */} {/* Handle */}
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-5" /> <div className="w-10 h-1 bg-border rounded-full mx-auto mb-5" />
<div className="flex items-start gap-3 mb-4"> <div className="flex items-start gap-3 mb-4">
<div className="w-14 h-14 rounded-2xl bg-gray-100 flex items-center justify-center text-2xl shrink-0"> <div className="w-14 h-14 rounded-2xl bg-bg-deep border border-border flex items-center justify-center text-2xl shrink-0">
{CATEGORY_ICONS[selectedListing.category] || '📦'} {CATEGORY_ICONS[selectedListing.category] || '📦'}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-lg font-bold text-gray-900 leading-tight">{selectedListing.title}</h2> <h2 className="text-lg font-bold text-white leading-tight">{selectedListing.title}</h2>
<p className="text-xs text-gray-400 mt-0.5">by {selectedListing.seller}</p> <p className="text-xs text-text-muted mt-0.5">by {selectedListing.seller}</p>
</div> </div>
<button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100"> <button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-bg-deep border border-border text-text-muted cursor-pointer">
<X size={15} className="text-gray-400" /> <X size={15} />
</button> </button>
</div> </div>
<span className="inline-block text-[11px] bg-gray-100 text-gray-500 px-3 py-1 rounded-full mb-3">{selectedListing.category}</span> <span className="inline-block text-[11px] bg-bg-deep border border-border text-text-secondary 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> <p className="text-sm text-text-secondary leading-relaxed mb-5">{selectedListing.description}</p>
<div className="flex items-center justify-between mb-5"> <div className="flex items-center justify-between mb-5">
<p className="text-2xl font-bold text-[#D4AF37]">{selectedListing.price_flh.toLocaleString()} FLH</p> <p className="text-2xl font-bold text-[#D4AF37]">{selectedListing.price_flh.toLocaleString()} FLH</p>
{user && <p className="text-xs text-gray-400">Your balance: {user.flhBalance?.toLocaleString() || 0} FLH</p>} {user && <p className="text-xs text-text-muted">Your balance: {user.flhBalance?.toLocaleString() || 0} FLH</p>}
</div> </div>
{token && selectedListing.sellerId !== user?.id ? ( {token && selectedListing.sellerId !== user?.id ? (
<button <button
onClick={() => handlePurchase(selectedListing.id)} onClick={() => handlePurchase(selectedListing.id)}
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" className="w-full flex items-center justify-center gap-2 gold-gradient text-bg-deep py-4 rounded-2xl font-bold text-base active:scale-[0.97] transition-transform cursor-pointer"
> >
<ShoppingCart size={18} /> Purchase Now <ShoppingCart size={18} /> Purchase Now
</button> </button>
) : selectedListing.sellerId === user?.id ? ( ) : selectedListing.sellerId === user?.id ? (
<div className="w-full text-center py-4 rounded-2xl border border-gray-200 text-gray-400 text-sm">Your listing</div> <div className="w-full text-center py-4 rounded-2xl border border-border text-text-muted text-sm bg-bg-deep">Your listing</div>
) : ( ) : (
<button onClick={() => setSelectedListing(null)} className="w-full text-center py-4 rounded-2xl border border-gray-200 text-gray-400 text-sm"> <button onClick={() => setSelectedListing(null)} className="w-full text-center py-4 rounded-2xl border border-border text-text-muted text-sm bg-bg-deep">
Sign in to purchase Sign in to purchase
</button> </button>
)} )}
@@ -258,41 +258,41 @@ function CreateListingSheet({ token, onClose, onCreated, showToast }: {
return ( return (
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}> <div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={onClose}>
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" /> <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="relative bg-white rounded-t-3xl p-6 pb-10 space-y-4" className="relative bg-bg-card border-t border-x border-border rounded-t-3xl p-6 pb-10 space-y-4"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2" /> <div className="w-10 h-1 bg-border rounded-full mx-auto mb-2" />
<h2 className="text-lg font-bold text-gray-900">New Listing</h2> <h2 className="text-lg font-bold text-white">New Listing</h2>
<input <input
type="text" placeholder="Product title" value={title} onChange={e => setTitle(e.target.value)} required type="text" placeholder="Product title" value={title} onChange={e => setTitle(e.target.value)} required
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-4 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none"
/> />
<textarea <textarea
placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} required rows={3} placeholder="Description" value={description} onChange={e => setDescription(e.target.value)} required rows={3}
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-3 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none resize-none"
/> />
<select <select
value={category} onChange={e => setCategory(e.target.value)} value={category} onChange={e => setCategory(e.target.value)}
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-3.5 text-sm text-white focus:border-gold focus:outline-none"
> >
{['E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other'].map(c => <option key={c}>{c}</option>)} {['E-Books', 'Courses', 'Design', 'Audio', 'Video', 'Software', 'Other'].map(c => <option key={c} className="bg-bg-card">{c}</option>)}
</select> </select>
<input <input
type="number" placeholder="Price in FLH" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1} type="number" placeholder="Price in FLH" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1}
inputMode="numeric" inputMode="numeric"
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-4 text-sm text-white placeholder-text-muted focus:border-gold focus:outline-none"
/> />
<button <button
type="submit" disabled={loading} type="submit" disabled={loading}
className="w-full bg-emerald-600 text-white py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform" className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold disabled:opacity-50 active:scale-[0.97] transition-transform cursor-pointer"
> >
{loading ? 'Creating…' : 'Create Listing'} {loading ? 'Creating…' : 'Create Listing'}
</button> </button>
<button type="button" onClick={onClose} className="w-full text-gray-400 text-sm py-2"> <button type="button" onClick={onClose} className="w-full text-text-muted text-sm py-2 cursor-pointer hover:text-text-secondary">
Cancel Cancel
</button> </button>
</form> </form>
+24 -24
View File
@@ -101,30 +101,30 @@ function UpgradeContent() {
} }
if (authLoading) return ( if (authLoading) return (
<div className="min-h-dvh bg-[#f5f4f0] flex items-center justify-center pb-24"> <div className="min-h-dvh bg-bg-deep flex items-center justify-center pb-24">
<div className="w-8 h-8 rounded-full border-2 border-emerald-300 border-t-emerald-600 animate-spin" /> <div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div> </div>
) )
if (!token) return null if (!token) return null
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24"> <div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* Gradient header */} {/* Gradient header */}
<div className="bg-gradient-to-br from-emerald-700 to-teal-600 px-4 pt-12 pb-8"> <div className="bg-gradient-to-br from-emerald-950/40 via-bg-card to-bg-deep border-b border-border/80 px-4 pt-14 pb-8">
<button <button
onClick={() => router.back()} 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" className="w-9 h-9 flex items-center justify-center rounded-xl bg-bg-card border border-border text-white mb-4 active:scale-[0.97] transition-all cursor-pointer"
> >
<ChevronLeft size={20} /> <ChevronLeft size={20} />
</button> </button>
<h1 className="text-2xl font-bold text-white">Choose your plan</h1> <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> <p className="text-sm text-text-secondary mt-1">Cancel anytime, no hidden fees</p>
</div> </div>
{/* Trial reminder */} {/* Trial reminder */}
<div className="mx-4 -mt-4 mb-5 bg-white border border-emerald-100 rounded-2xl shadow-sm px-4 py-3"> <div className="mx-4 -mt-4 mb-5 bg-bg-card border border-emerald-900/30 rounded-2xl shadow-md px-4 py-3">
<p className="text-xs text-emerald-700 font-medium">New accounts include a 7-day Premium trial free</p> <p className="text-xs text-emerald-400 font-medium">New accounts include a 7-day Premium trial free</p>
</div> </div>
{/* Tiers */} {/* Tiers */}
@@ -134,30 +134,30 @@ function UpgradeContent() {
return ( return (
<div <div
key={tier.name} key={tier.name}
className={`relative bg-white rounded-3xl shadow-md border p-5 ${ className={`relative bg-bg-card rounded-3xl shadow-lg border p-5 ${
tier.highlighted tier.highlighted
? 'border-emerald-400' ? 'border-gold/60 shadow-gold/5'
: 'border-gray-200' : 'border-border'
}`} }`}
> >
{tier.badge && ( {tier.badge && (
<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"> <div className="absolute -top-3 left-5 bg-gradient-to-r from-[#D4AF37] to-amber-500 text-bg-deep text-[10px] font-bold px-3 py-1 rounded-full tracking-widest">
{tier.badge} {tier.badge}
</div> </div>
)} )}
<div className="flex items-start justify-between mb-4"> <div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${ <div className={`w-10 h-10 rounded-2xl flex items-center justify-center border border-border/60 ${
tier.highlighted ? 'bg-amber-50' : 'bg-gray-50' tier.highlighted ? 'bg-[#D4AF37]/10' : 'bg-bg-deep'
}`}> }`}>
<Icon size={20} className={tier.iconColor} /> <Icon size={20} className={tier.iconColor} />
</div> </div>
<div> <div>
<h2 className="font-bold text-gray-900">{tier.name}</h2> <h2 className="font-bold text-white">{tier.name}</h2>
<div className="flex items-baseline gap-0.5"> <div className="flex items-baseline gap-0.5">
<span className="text-xl font-bold text-gray-900">{tier.price}</span> <span className="text-xl font-bold text-white">{tier.price}</span>
<span className="text-gray-400 text-xs">{tier.period}</span> <span className="text-text-muted text-xs">{tier.period}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -166,8 +166,8 @@ function UpgradeContent() {
<ul className="space-y-2.5 mb-5"> <ul className="space-y-2.5 mb-5">
{tier.features.map((f) => ( {tier.features.map((f) => (
<li key={f} className="flex items-start gap-2.5"> <li key={f} className="flex items-start gap-2.5">
<Check size={14} className="text-emerald-500 mt-0.5 shrink-0" /> <Check size={14} className="text-gold mt-0.5 shrink-0" />
<span className="text-sm text-gray-600">{f}</span> <span className="text-sm text-text-secondary">{f}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -175,10 +175,10 @@ function UpgradeContent() {
{tier.priceId ? ( {tier.priceId ? (
<button <button
onClick={() => handleSubscribe(tier.priceId!)} onClick={() => handleSubscribe(tier.priceId!)}
className={`w-full py-4 rounded-2xl font-bold text-sm active:scale-[0.97] transition-all ${ className={`w-full py-4 rounded-2xl font-bold text-sm active:scale-[0.97] transition-all cursor-pointer ${
tier.highlighted tier.highlighted
? 'bg-emerald-600 text-white' ? 'gold-gradient text-bg-deep'
: 'bg-purple-50 text-purple-600 border border-purple-200' : 'bg-purple-950/20 text-[#A78BFA] border border-purple-900/40 hover:bg-purple-950/40'
}`} }`}
> >
{tier.cta} {tier.cta}
@@ -186,7 +186,7 @@ function UpgradeContent() {
) : ( ) : (
<button <button
onClick={() => router.push(tier.href ?? '/')} onClick={() => router.push(tier.href ?? '/')}
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" className="w-full py-4 rounded-2xl font-bold text-sm border border-border text-text-secondary bg-bg-deep active:scale-[0.97] transition-all cursor-pointer"
> >
{tier.cta} {tier.cta}
</button> </button>
@@ -196,7 +196,7 @@ function UpgradeContent() {
})} })}
</div> </div>
<p className="text-center text-xs text-gray-400 mt-6 px-8"> <p className="text-center text-xs text-text-muted mt-6 px-8">
Secure payments via Polar. Cancel anytime from your account settings. Secure payments via Polar. Cancel anytime from your account settings.
</p> </p>
</div> </div>
+36 -35
View File
@@ -44,8 +44,8 @@ export default function WalletPage() {
} }
if (authLoading) return ( if (authLoading) return (
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-20"> <div className="min-h-dvh flex items-center justify-center bg-bg-deep pb-20">
<div className="w-8 h-8 rounded-full border-2 border-emerald-200 border-t-emerald-600 animate-spin" /> <div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div> </div>
) )
if (!token) return null if (!token) return null
@@ -53,38 +53,39 @@ export default function WalletPage() {
const fiatValue = amount ? ((parseInt(amount) || 0) * 0.008).toFixed(2) : '0.00' const fiatValue = amount ? ((parseInt(amount) || 0) * 0.008).toFixed(2) : '0.00'
return ( return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24"> <div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* Header */} {/* Header */}
<div className="px-5 pt-8 pb-4 bg-white"> <div className="px-5 pt-14 pb-4">
<h1 className="text-xl font-bold text-gray-900">Wallet</h1> <h1 className="text-xl font-bold text-white">Wallet</h1>
<p className="text-[11px] text-gray-400 mt-0.5">Manage your FLH balance</p> <p className="text-[11px] text-text-secondary mt-0.5">Manage your FLH balance</p>
</div> </div>
{/* Balance card */} {/* Balance card */}
<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="mx-5 mb-5 rounded-3xl overflow-hidden shadow-lg glass border border-gold/30 relative">
<div className="px-6 pt-6 pb-6"> <div className="absolute inset-0 bg-gradient-to-br from-gold/10 to-transparent opacity-60 pointer-events-none" />
<div className="px-6 py-6 relative z-10">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<Wallet size={16} className="text-white/70" /> <Wallet size={16} className="text-gold" />
<span className="text-xs text-white/70 font-medium uppercase tracking-wider">FLH Balance</span> <span className="text-xs text-text-secondary font-semibold uppercase tracking-wider">FLH Balance</span>
</div> </div>
<p className="text-5xl font-bold text-white tabular-nums leading-none"> <p className="text-5xl font-extrabold text-white tabular-nums leading-none">
{(user?.flhBalance || 0).toLocaleString()} {(user?.flhBalance || 0).toLocaleString()}
</p> </p>
<p className="text-white/80 font-semibold mt-1 text-base">FLH Tokens</p> <p className="text-gold font-bold mt-1.5 text-base">FLH Tokens</p>
<p className="text-xs text-white/50 mt-3"> <p className="text-xs text-text-secondary mt-3">
£{((user?.flhBalance || 0) * 0.008).toFixed(2)} GBP at current rate £{((user?.flhBalance || 0) * 0.008).toFixed(2)} GBP at current rate
</p> </p>
</div> </div>
</div> </div>
{/* Cash Out */} {/* Cash Out */}
<div className="mx-5 mb-5 bg-white border border-gray-100 rounded-2xl shadow-sm p-5"> <div className="mx-5 mb-5 glass rounded-3xl p-5">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<ArrowDownLeft size={16} className="text-emerald-600" /> <ArrowDownLeft size={16} className="text-gold" />
<h2 className="font-bold text-gray-900 text-sm">Cash Out</h2> <h2 className="font-bold text-white text-sm">Cash Out</h2>
</div> </div>
<p className="text-[11px] text-gray-400 mb-4">100 FLH = £0.80 · Minimum 100 FLH</p> <p className="text-[11px] text-text-muted mb-4">100 FLH = £0.80 · Minimum 100 FLH</p>
<div className="space-y-3"> <div className="space-y-3">
<input <input
@@ -94,18 +95,18 @@ export default function WalletPage() {
onChange={e => setAmount(e.target.value)} onChange={e => setAmount(e.target.value)}
min={100} min={100}
inputMode="numeric" inputMode="numeric"
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" className="w-full bg-bg-deep border border-border rounded-xl px-4 py-4 text-white placeholder-text-muted text-sm focus:border-gold focus:outline-none"
/> />
{amount && parseInt(amount) > 0 && ( {amount && parseInt(amount) > 0 && (
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<span className="text-xs text-gray-400">{parseInt(amount).toLocaleString()} FLH</span> <span className="text-xs text-text-secondary">{parseInt(amount).toLocaleString()} FLH</span>
<span className="text-xs text-emerald-600 font-semibold">£{fiatValue} GBP</span> <span className="text-xs text-gold font-semibold">£{fiatValue} GBP</span>
</div> </div>
)} )}
<button <button
onClick={handleCashout} onClick={handleCashout}
disabled={loading || !amount || parseInt(amount) < 100} disabled={loading || !amount || parseInt(amount) < 100}
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" className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold text-sm disabled:opacity-40 active:scale-[0.97] transition-all cursor-pointer"
> >
{loading ? 'Submitting…' : 'Request Cash Out'} {loading ? 'Submitting…' : 'Request Cash Out'}
</button> </button>
@@ -116,32 +117,32 @@ export default function WalletPage() {
</div> </div>
{/* History */} {/* History */}
<div className="mx-5 bg-white border border-gray-100 rounded-2xl shadow-sm p-5"> <div className="mx-5 glass rounded-3xl p-5">
<h2 className="font-bold text-gray-900 text-sm mb-4">History</h2> <h2 className="font-bold text-white text-sm mb-4">History</h2>
{cashouts.length === 0 ? ( {cashouts.length === 0 ? (
<div className="text-center py-6"> <div className="text-center py-6">
<p className="text-gray-400 text-sm">No cashout requests yet</p> <p className="text-text-muted text-sm">No cashout requests yet</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{cashouts.map(c => ( {cashouts.map(c => (
<div key={c.id} className="flex items-center gap-3 py-2"> <div key={c.id} className="flex items-center gap-3 py-2 border-b border-border/40 last:border-b-0">
<div className="w-9 h-9 rounded-full bg-gray-100 flex items-center justify-center shrink-0"> <div className="w-9 h-9 rounded-full bg-bg-deep border border-border flex items-center justify-center shrink-0">
{c.status === 'pending' {c.status === 'pending'
? <Clock size={16} className="text-yellow-500" /> ? <Clock size={16} className="text-gold" />
: c.status === 'approved' : c.status === 'approved'
? <CheckCircle2 size={16} className="text-emerald-600" /> ? <CheckCircle2 size={16} className="text-emerald" />
: <XCircle size={16} className="text-red-500" /> : <XCircle size={16} className="text-red-500" />
} }
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-gray-900 font-medium">{c.amountFlh.toLocaleString()} FLH</p> <p className="text-sm text-white font-medium">{c.amountFlh.toLocaleString()} FLH</p>
<p className="text-xs text-gray-400">£{c.fiatAmount.toFixed(2)} GBP</p> <p className="text-xs text-text-secondary">£{c.fiatAmount.toFixed(2)} GBP</p>
</div> </div>
<span className={`text-xs font-semibold capitalize px-2.5 py-1 rounded-full ${ <span className={`text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-full border ${
c.status === 'pending' ? 'bg-yellow-50 text-yellow-700' c.status === 'pending' ? 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20'
: c.status === 'approved' ? 'bg-emerald-50 text-emerald-700' : c.status === 'approved' ? 'bg-emerald/10 text-emerald border-emerald/20'
: 'bg-red-50 text-red-600' : 'bg-red-500/10 text-red-500 border-red-500/20'
}`}>{c.status}</span> }`}>{c.status}</span>
</div> </div>
))} ))}
@@ -151,7 +152,7 @@ export default function WalletPage() {
{/* Toast */} {/* Toast */}
{toast && ( {toast && (
<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"> <div className="fixed bottom-24 left-5 right-5 bg-bg-card border border-border text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
{toast} {toast}
</div> </div>
)} )}
+272
View File
@@ -0,0 +1,272 @@
'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/lib/AuthContext'
import { useRouter } from 'next/navigation'
import { ArrowLeft, Gift, Heart, ShieldAlert, CheckCircle2, ChevronRight, X } from 'lucide-react'
import Link from 'next/link'
interface WaqfProject {
id: string
title: string
description: string
raisedFlh: number
targetFlh: number
category: string
icon: string
}
const INITIAL_PROJECTS: WaqfProject[] = [
{
id: 'mosque-expansion',
title: 'Mosque Expansion & Solar Power',
description: 'Provide sustainable solar energy and double the capacity of the rural community mosque.',
raisedFlh: 42000,
targetFlh: 80000,
category: 'Infrastructure',
icon: '🕌'
},
{
id: 'clean-water-well',
title: 'Solar Water Wells & Pumps',
description: 'Construct a clean drinking water borehole system powered entirely by solar pumps.',
raisedFlh: 21500,
targetFlh: 30000,
category: 'Water',
icon: '💧'
},
{
id: 'quran-distribution',
title: 'Quran Publishing & Distribution',
description: 'Print and translate 10,000 copies of the Quran to school children in underserved areas.',
raisedFlh: 7800,
targetFlh: 15000,
category: 'Education',
icon: '📖'
},
{
id: 'orphans-school',
title: 'Orphans Digital Literacy School',
description: 'Sponsor the construction of a new digital classroom and computers for Islamic orphanage.',
raisedFlh: 31000,
targetFlh: 50000,
category: 'Education',
icon: '💻'
}
]
export default function WaqfPage() {
const { token, user, loading: authLoading } = useAuth()
const router = useRouter()
const [projects, setProjects] = useState<WaqfProject[]>(INITIAL_PROJECTS)
const [selectedProject, setSelectedProject] = useState<WaqfProject | null>(null)
const [amount, setAmount] = useState('')
const [loading, setLoading] = useState(false)
const [toast, setToast] = useState('')
useEffect(() => {
if (!authLoading && !token) router.push('/login')
}, [token, authLoading, router])
const showToast = (msg: string) => {
setToast(msg)
setTimeout(() => setToast(''), 3000)
}
const handleContribute = async () => {
if (!token || !selectedProject || !amount) return
const flhAmount = parseInt(amount)
if (flhAmount <= 0) return
if (user && user.flhBalance < flhAmount) {
showToast('Insufficient FLH balance')
return
}
setLoading(true)
try {
const res = await fetch('/api/waqf/contribute', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
projectId: selectedProject.id,
amountFlh: flhAmount
})
})
const data = await res.json()
if (res.ok) {
// Increment project progress locally
setProjects(prev =>
prev.map(p =>
p.id === selectedProject.id ? { ...p, raisedFlh: p.raisedFlh + flhAmount } : p
)
)
showToast('JazakAllah Khair! Contribution successful!')
setSelectedProject(null)
setAmount('')
// Refresh page to sync balance
setTimeout(() => window.location.reload(), 1500)
} else {
showToast(data.error || 'Contribution failed')
}
} catch {
showToast('Something went wrong')
} finally {
setLoading(false)
}
}
if (authLoading) {
return (
<div className="min-h-dvh flex items-center justify-center bg-bg-deep pb-20">
<div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div>
)
}
if (!token) return null
return (
<div className="min-h-dvh bg-bg-deep pb-32 text-white select-none">
{/* Header */}
<div className="px-5 pt-14 pb-4 flex items-center gap-3">
<Link href="/profile" className="w-9 h-9 flex items-center justify-center rounded-xl bg-bg-card border border-border text-white active:scale-95 transition-all">
<ArrowLeft size={18} />
</Link>
<div>
<h1 className="text-xl font-bold text-white">Waqf Chain</h1>
<p className="text-[11px] text-text-secondary mt-0.5">Sadaqah Jariyah as Smart Endowments</p>
</div>
</div>
{/* Spirituality Stats Card */}
<div className="mx-5 mb-5 rounded-3xl bg-gradient-to-br from-emerald-950/40 via-bg-card to-bg-deep border border-border p-5">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-2xl bg-gold/10 flex items-center justify-center shrink-0">
<Heart size={22} className="text-gold" />
</div>
<div>
<h2 className="text-sm font-semibold text-white">Your Endowment Impact</h2>
<p className="text-xs text-text-secondary mt-0.5">Every contribution remains verified on the Waqf ledger.</p>
<p className="text-[11px] text-gold mt-2 font-medium">Your current balance: {user?.flhBalance?.toLocaleString() || 0} FLH</p>
</div>
</div>
</div>
{/* Projects List */}
<div className="px-5 space-y-4">
<p className="text-[11px] text-text-muted font-medium uppercase tracking-widest">Active Waqf Initiatives</p>
{projects.map(p => {
const percent = Math.min(100, Math.round((p.raisedFlh / p.targetFlh) * 100))
return (
<button
key={p.id}
onClick={() => setSelectedProject(p)}
className="w-full text-left bg-bg-card border border-border rounded-2xl p-4 active:scale-[0.98] transition-all hover:border-gold/30 cursor-pointer block"
>
<div className="flex items-start gap-3">
<span className="text-2xl w-12 h-12 flex items-center justify-center bg-bg-deep border border-border rounded-xl shrink-0">
{p.icon}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-[10px] bg-bg-deep border border-border text-text-secondary px-2.5 py-0.5 rounded-full uppercase tracking-wider font-semibold">
{p.category}
</span>
<span className="text-xs text-gold font-bold">{percent}%</span>
</div>
<h3 className="font-bold text-white text-sm mt-2 leading-tight">{p.title}</h3>
<p className="text-xs text-text-secondary mt-1 line-clamp-2 leading-relaxed">{p.description}</p>
{/* Progress bar */}
<div className="w-full h-1.5 bg-bg-deep border border-border rounded-full overflow-hidden mt-3.5">
<div
className="h-full rounded-full gold-gradient transition-all duration-500"
style={{ width: `${percent}%` }}
/>
</div>
<div className="flex justify-between items-center mt-2.5 text-[10px] text-text-muted">
<span>{p.raisedFlh.toLocaleString()} FLH raised</span>
<span>Goal: {p.targetFlh.toLocaleString()} FLH</span>
</div>
</div>
</div>
</button>
)}
)}
</div>
{/* Contribution Detail Sheet */}
{selectedProject && (
<div className="fixed inset-0 z-50 flex flex-col justify-end" onClick={() => setSelectedProject(null)}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" />
<div
className="relative bg-bg-card border-t border-x border-border rounded-t-3xl p-6 pb-10 max-h-[85dvh] overflow-y-auto"
onClick={e => e.stopPropagation()}
>
{/* Handle */}
<div className="w-10 h-1 bg-border rounded-full mx-auto mb-5" />
<div className="flex items-start gap-3 mb-4">
<div className="w-14 h-14 rounded-2xl bg-bg-deep border border-border flex items-center justify-center text-2xl shrink-0">
{selectedProject.icon}
</div>
<div className="flex-1 min-w-0">
<h2 className="text-lg font-bold text-white leading-tight">{selectedProject.title}</h2>
<p className="text-xs text-text-muted mt-0.5">Waqf Smart Contract Endowment</p>
</div>
<button onClick={() => setSelectedProject(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-bg-deep border border-border text-text-muted cursor-pointer">
<X size={15} />
</button>
</div>
<p className="text-sm text-text-secondary leading-relaxed mb-5">{selectedProject.description}</p>
<div className="space-y-4">
<div className="space-y-1.5">
<label className="text-xs font-semibold text-text-secondary uppercase tracking-widest">Contribute Amount (FLH)</label>
<input
type="number"
placeholder="Enter amount to contribute"
value={amount}
onChange={e => setAmount(e.target.value)}
min={1}
inputMode="numeric"
className="w-full bg-bg-deep border border-border rounded-xl px-4 py-4 text-white placeholder-text-muted text-sm focus:border-gold focus:outline-none"
/>
</div>
{amount && parseInt(amount) > 0 && (
<div className="flex justify-between text-xs px-1 text-text-secondary">
<span>Balance after Waqf:</span>
<span className="font-semibold text-gold">
{Math.max(0, (user?.flhBalance || 0) - parseInt(amount)).toLocaleString()} FLH
</span>
</div>
)}
<button
onClick={handleContribute}
disabled={loading || !amount || parseInt(amount) <= 0 || (user ? user.flhBalance < parseInt(amount) : false)}
className="w-full gold-gradient text-bg-deep py-4 rounded-2xl font-bold text-base active:scale-[0.97] transition-transform cursor-pointer disabled:opacity-40"
>
{loading ? 'Processing Endowment…' : 'Contribute Waqf'}
</button>
</div>
</div>
</div>
)}
{/* Toast */}
{toast && (
<div className="fixed bottom-24 left-5 right-5 bg-bg-card border border-border text-white rounded-2xl px-4 py-3 text-sm text-center z-50 shadow-xl">
{toast}
</div>
)}
</div>
)
}
+1 -1
View File
@@ -8,7 +8,7 @@ declare global {
} }
} }
declare const self: ServiceWorkerGlobalScope declare const self: WorkerGlobalScope
const serwist = new Serwist({ const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST, precacheEntries: self.__SW_MANIFEST,
+3 -1
View File
@@ -31,7 +31,9 @@
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"src/**/*.ts", "src/**/*.ts",
"src/**/*.tsx" "src/**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
environment: 'jsdom', environment: 'jsdom',
globals: true, globals: true,
setupFiles: ['./vitest.setup.ts'], setupFiles: ['./vitest.setup.ts'],
exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'],
alias: { alias: {
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),
}, },