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('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 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
await page.click('text=Login');
await page.click('a[href="/login"]');
await expect(page.locator('form')).toBeVisible();
// Try invalid login → see error message
@@ -16,23 +20,27 @@ test.describe('Authentication Flow', () => {
await page.click('button[type="submit"]');
await expect(page.locator('text=Invalid credentials')).toBeVisible();
// Navigate to registration (assuming a link exists on login page)
await page.click('text=Register');
// Navigate to registration
await page.click('a[href="/register"]');
// Fill registration form → submit → redirect to home
await page.fill('input[name="name"]', 'Test User');
await page.fill('input[name="email"]', 'newuser@example.com');
await page.fill('input[name="password"]', 'securepassword123');
const randomEmail = `newuser_${Date.now()}_${Math.floor(Math.random() * 1000)}@example.com`;
await page.fill('input[type="text"]', 'Test User');
await page.fill('input[type="email"]', randomEmail);
await page.fill('input[placeholder="Min. 8 characters"]', 'securepassword123');
await page.click('button[type="submit"]');
// Check redirect and authentication state
await expect(page).toHaveURL('/');
// See user menu or similar in navbar when authenticated
await expect(page.locator('nav')).toContainText('Logout');
await expect(page).toHaveURL(/\/nur/);
// Click logout → redirect to home → see login button
await page.click('text=Logout');
await expect(page).toHaveURL('/');
await expect(page.locator('text=Login')).toBeVisible();
// Go to Profile tab and check authenticated state
await page.click('text=Profile');
await expect(page.locator('nav')).toContainText('Profile');
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/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// 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: {
command: 'npm run dev',
command: 'node .next/standalone/server.js',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
+4 -4
View File
@@ -7,10 +7,10 @@ export async function POST() {
const password = await bcrypt.hash('password123', 10)
const userData = [
{ email: 'ghazali@falahos.my', name: 'Abu Hamid Al-Ghazali', password, flhBalance: 5000 },
{ email: 'ibnabbas@falahos.my', name: 'Abdullah Ibn Abbas', password, flhBalance: 3000 },
{ email: 'rabia@falahos.my', name: "Rabi'a Al-Adawiyya", password, flhBalance: 2000 },
{ email: 'demo@falahos.my', name: 'Demo User', password, flhBalance: 500 },
{ email: 'ghazali@falahos.my', name: 'Abu Hamid Al-Ghazali', passwordHash: password, flhBalance: 5000 },
{ email: 'ibnabbas@falahos.my', name: 'Abdullah Ibn Abbas', passwordHash: password, flhBalance: 3000 },
{ email: 'rabia@falahos.my', name: "Rabi'a Al-Adawiyya", passwordHash: password, flhBalance: 2000 },
{ email: 'demo@falahos.my', name: 'Demo User', passwordHash: password, flhBalance: 500 },
]
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) {
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>
<h2 className="text-xl font-bold text-gray-900 mb-2">Daily Dhikr</h2>
<p className="text-gray-500 text-sm text-center mb-8">
<h2 className="text-xl font-bold text-white mb-2">Daily Dhikr</h2>
<p className="text-text-secondary text-sm text-center mb-8">
Build a daily habit of remembrance. Sign in to track your streak.
</p>
<button onClick={() => router.push('/login')}
className="w-full max-w-xs bg-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
</button>
</div>
@@ -125,24 +125,24 @@ export default function DhikrPage() {
if (loading) {
return (
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-24">
<div className="w-8 h-8 rounded-full border-2 border-emerald-700/30 border-t-emerald-700 animate-spin" />
<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-gold/30 border-t-gold animate-spin" />
</div>
)
}
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 */}
<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>
<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>
{/* 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">
<Flame size={14} className="text-orange-300" />
<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-400 animate-pulse" />
<span className="text-sm font-bold text-white">{streak}</span>
</div>
</div>
@@ -157,22 +157,22 @@ export default function DhikrPage() {
key={i}
className={`flex-1 py-1.5 rounded-xl text-center transition-all duration-300 ${
isActive
? 'bg-white shadow-sm border border-[#D4AF37]/40'
? 'bg-bg-card shadow-lg border border-gold/40'
: isDone
? 'bg-white/60 border border-gray-100'
: 'bg-white/40 border border-gray-100'
? 'bg-bg-card/60 border border-border/60'
: 'bg-bg-card/30 border border-border/30'
}`}
>
<div
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}
</div>
<div
className="mx-3 mt-1 h-0.5 rounded-full transition-all duration-500"
style={{
background: isDone ? d.ring : isActive ? `${d.ring}99` : '#e5e7eb',
background: isDone ? d.ring : isActive ? `${d.ring}99` : 'var(--color-border)',
}}
/>
</div>
@@ -183,18 +183,18 @@ export default function DhikrPage() {
{/* Completed today (not just-finished) */}
{todayCompleted && !justFinished ? (
<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="w-24 h-24 rounded-full border-2 border-[#D4AF37]/30 bg-amber-50 flex items-center justify-center mb-5">
<CheckCircle2 size={44} className="text-[#D4AF37]" />
<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-gold/30 bg-gold/5 flex items-center justify-center mb-5">
<CheckCircle2 size={44} className="text-gold" />
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-1">MashaAllah!</h2>
<p className="text-gray-500 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">
<Flame size={18} className="text-orange-400" />
<span className="text-lg font-bold text-orange-500">{streak} day streak</span>
<h2 className="text-2xl font-bold text-white mb-1">MashaAllah!</h2>
<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-950/20 border border-orange-900/40 rounded-2xl px-5 py-3 text-orange-400">
<Flame size={18} className="text-orange-400 animate-pulse" />
<span className="text-lg font-bold">{streak} day streak</span>
</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
</button>
</div>
@@ -202,20 +202,20 @@ export default function DhikrPage() {
) : justFinished ? (
/* Completion screen */
<div className="flex-1 flex flex-col items-center justify-center px-6 text-center">
<div className="text-6xl mb-5"></div>
<h2 className="text-3xl font-bold text-gray-900 mb-1">SubhanAllah!</h2>
<p className="text-gray-500 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="text-6xl mb-5 animate-bounce"></div>
<h2 className="text-3xl font-bold text-white mb-1">SubhanAllah!</h2>
<p className="text-text-secondary text-sm mb-6">100 remembrances complete</p>
<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" />
<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 className="bg-white border border-gray-100 rounded-2xl shadow-sm px-5 py-4 max-w-xs">
<p className="text-xs text-gray-500 leading-relaxed">
<div className="bg-bg-card border border-border rounded-2xl px-5 py-4 max-w-xs">
<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;
</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>
<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
</button>
</div>
@@ -223,11 +223,11 @@ export default function DhikrPage() {
) : (
/* Main counter */
<div className="flex-1 flex flex-col items-center justify-center px-5">
{/* Ring counter — white 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">
{/* Ring counter — card */}
<div className="bg-bg-card rounded-3xl border border-border mx-5 py-8 px-6 flex flex-col items-center w-full">
<button
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' }}
aria-label={`Tap to count ${current.transliteration}`}
>
@@ -244,7 +244,7 @@ export default function DhikrPage() {
{/* SVG progress ring */}
<svg width="200" height="200" className="-rotate-90" style={{ filter: `drop-shadow(0 0 8px ${current.glow})` }}>
{/* Track */}
<circle cx="100" cy="100" r={R} fill="none" stroke="#e5e7eb" strokeWidth="8" />
<circle cx="100" cy="100" r={R} fill="none" stroke="var(--color-border)" strokeWidth="8" />
{/* Progress */}
<circle
cx="100" cy="100" r={R} fill="none"
@@ -259,37 +259,37 @@ export default function DhikrPage() {
{/* Center content */}
<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={{
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-[11px] text-gray-400 mt-1">of {current.target}</span>
<span className="text-5xl font-extrabold text-white tabular-nums leading-none">{count}</span>
<span className="text-[11px] text-text-secondary mt-1">of {current.target}</span>
</div>
</button>
{/* Dhikr text below ring, inside card */}
<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="text-gray-500 text-sm">{current.meaning}</p>
<p className="text-text-secondary text-sm">{current.meaning}</p>
</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>
)}
{/* Bottom progress bar */}
{!justFinished && (
<div className="px-5 pb-2 mt-4">
<div className="flex items-center justify-between text-[11px] text-gray-400 mb-2">
<div className="px-5 pb-8 mt-4">
<div className="flex items-center justify-between text-[11px] text-text-secondary mb-2">
<span>Total today</span>
<span className="tabular-nums">{Math.min(totalCount, TOTAL)} / {TOTAL}</span>
</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
className="h-full rounded-full transition-all duration-300"
style={{
+35 -35
View File
@@ -39,19 +39,19 @@ export default function ForumPage() {
const showToast = (msg: string) => { setToast(msg); setTimeout(() => setToast(''), 3000) }
return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
<div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* 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">
{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">
<ArrowLeft size={18} className="text-gray-500" />
<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-text-secondary" />
</button>
)}
<div>
<h1 className="text-xl font-bold text-gray-900">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
<p className="text-[11px] text-gray-400 mt-0.5">
<h1 className="text-xl font-bold text-white">{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}</h1>
<p className="text-[11px] text-text-secondary mt-0.5">
{view === 'categories' ? 'Community discussions' : `${threads.length} threads`}
</p>
</div>
@@ -59,7 +59,7 @@ export default function ForumPage() {
{token && (
<button
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
</button>
@@ -70,7 +70,7 @@ export default function ForumPage() {
{loading && (
<div className="px-5 pt-4 space-y-3">
{[...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>
)}
@@ -81,23 +81,23 @@ export default function ForumPage() {
{categories.length === 0 ? (
<div className="text-center py-16">
<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>
) : categories.map(c => (
<button
key={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">
<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 || '💬'}
</span>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900 text-sm">{c.name}</h3>
<p className="text-xs text-gray-400 mt-0.5 line-clamp-1">{c.description}</p>
<h3 className="font-semibold text-white text-sm">{c.name}</h3>
<p className="text-xs text-text-secondary mt-0.5 line-clamp-1">{c.description}</p>
</div>
<ChevronRight size={16} className="text-gray-300 shrink-0" />
<ChevronRight size={16} className="text-text-muted shrink-0" />
</div>
</button>
))}
@@ -110,21 +110,21 @@ export default function ForumPage() {
{threads.length === 0 ? (
<div className="text-center py-16">
<div className="text-4xl mb-3">💬</div>
<p className="text-gray-900 font-semibold mb-1">No threads yet</p>
<p className="text-gray-400 text-sm">Start the first discussion</p>
<p className="text-white font-semibold mb-1">No threads yet</p>
<p className="text-text-secondary text-sm">Start the first discussion</p>
</div>
) : threads.map(t => (
<div key={t.id} className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4">
<h3 className="font-semibold text-gray-900 text-sm leading-snug mb-1">{t.title}</h3>
<p className="text-xs text-gray-400 line-clamp-2 mb-3">{t.content}</p>
<div key={t.id} className="bg-bg-card rounded-2xl border border-border p-4">
<h3 className="font-semibold text-white text-sm leading-snug mb-1">{t.title}</h3>
<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 gap-1.5">
<div className="w-5 h-5 rounded-full bg-emerald-100 flex items-center justify-center">
<span className="text-[9px] text-emerald-700 font-bold">{t.author.name[0]}</span>
<div className="w-5 h-5 rounded-full bg-bg-deep border border-border flex items-center justify-center">
<span className="text-[9px] text-[#D4AF37] font-bold">{t.author.name[0]}</span>
</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 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} />
{t._count.posts}
</div>
@@ -136,7 +136,7 @@ export default function ForumPage() {
{/* 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}
</div>
)}
@@ -182,38 +182,38 @@ function CreateThreadSheet({ token, categories, onClose, onCreated, showToast }:
return (
<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
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()}
>
<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" />
<h2 className="text-lg font-bold text-gray-900 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">
<X size={15} className="text-gray-400" />
<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-white mt-2">New Thread</h2>
<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} />
</button>
</div>
<select
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>
<input
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
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
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'}
</button>
+9 -8
View File
@@ -67,18 +67,19 @@ input, select, textarea {
-webkit-overflow-scrolling: touch;
}
/* Glassmorphism utility */
/* Glassmorphism utility (Spatial Vision OS style) */
.glass {
background: rgba(17, 17, 24, 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.4);
}
.glass-light {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(20px) saturate(150%);
-webkit-backdrop-filter: blur(20px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.06);
}
+37 -37
View File
@@ -27,24 +27,24 @@ interface MapCenter {
function UpgradeCTA() {
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="w-16 h-16 rounded-2xl bg-amber-50 flex items-center justify-center">
<Crown size={32} className="text-amber-500" />
<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-bg-card border border-gold/20 flex items-center justify-center">
<Crown size={32} className="text-gold" />
</div>
<h1 className="text-2xl font-bold text-gray-900">Halal Monitor</h1>
<p className="text-gray-500 max-w-md text-sm">
<h1 className="text-2xl font-bold text-white">Halal Monitor</h1>
<p className="text-text-secondary max-w-md text-sm">
Find mosques and halal restaurants anywhere in the world. Available to Premium members.
</p>
<div className="bg-white border border-gray-100 shadow-md rounded-3xl p-6 max-w-sm w-full space-y-3">
<ul className="text-sm text-gray-500 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-emerald-500"></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-emerald-500"></span> City search anywhere in the world</li>
<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-text-secondary space-y-2 text-left">
<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-gold"></span> Halal restaurants near you</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-gold"></span> City search anywhere in the world</li>
</ul>
<a
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
</a>
@@ -55,17 +55,17 @@ function UpgradeCTA() {
function LoginGate() {
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="w-16 h-16 rounded-2xl bg-gray-100 flex items-center justify-center">
<Lock size={32} className="text-gray-400" />
<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-bg-card border border-border flex items-center justify-center">
<Lock size={32} className="text-text-muted" />
</div>
<h1 className="text-2xl font-bold text-gray-900">Halal Monitor</h1>
<p className="text-gray-500 max-w-md text-sm">
<h1 className="text-2xl font-bold text-white">Halal Monitor</h1>
<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.
</p>
<a
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
</a>
@@ -193,8 +193,8 @@ export default function HalalMonitorPage() {
if (authLoading) {
return (
<div className="min-h-dvh bg-[#f5f4f0] 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="min-h-dvh bg-bg-deep pb-24 flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-gold/30 border-t-gold animate-spin" />
</div>
)
}
@@ -205,37 +205,37 @@ export default function HalalMonitorPage() {
const restaurants = places.filter(p => p.type === 'restaurant')
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 */}
<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">
<Crown size={15} className="text-amber-500" />
<span className="text-sm font-semibold text-gray-900">Halal Monitor</span>
<Crown size={15} className="text-gold" />
<span className="text-sm font-semibold text-white">Halal Monitor</span>
</div>
<form onSubmit={handleCitySearch} className="flex gap-2 flex-1 max-w-sm">
<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
type="text"
placeholder="Search city..."
value={citySearch}
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>
<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
</button>
</form>
<button
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
</button>
{loading && <Loader2 size={14} className="animate-spin text-emerald-600" />}
{loading && <Loader2 size={14} className="animate-spin text-gold" />}
{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
</span>
)}
@@ -243,21 +243,21 @@ export default function HalalMonitorPage() {
{/* Error bar */}
{error && (
<div className="bg-red-50 border-b border-red-100 px-4 py-2 flex items-center justify-between shrink-0">
<span className="text-xs text-red-600">{error}</span>
<button onClick={() => setError(null)}><X size={14} className="text-red-400" /></button>
<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-400">{error}</span>
<button onClick={() => setError(null)} className="cursor-pointer text-red-400 hover:text-red-300"><X size={14} /></button>
</div>
)}
{/* Map */}
<div className="flex-1 relative">
{!center ? (
<div className="absolute inset-0 flex flex-col items-center justify-center text-center space-y-4 bg-[#f5f4f0]">
<MapPin size={40} className="text-gray-300" />
<p className="text-gray-500 text-sm">Search a city or use your location to find halal places.</p>
<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-text-muted" />
<p className="text-text-secondary text-sm">Search a city or use your location to find halal places.</p>
<button
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
</button>
+6 -6
View File
@@ -171,7 +171,7 @@ function PostCard({ post }: { post: (typeof POSTS)[number] }) {
}
return (
<div className="bg-[#111118] rounded-2xl border border-[#222] overflow-hidden">
<div className="glass rounded-[24px] overflow-hidden">
{/* Author row */}
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
<div className="w-9 h-9 rounded-full bg-[#D4AF37]/20 flex items-center justify-center shrink-0">
@@ -343,7 +343,7 @@ export default function HomePage() {
</h1>
<p className="text-[#999] text-sm mt-0.5">Welcome to Falah</p>
</div>
<button className="w-9 h-9 bg-[#111118] rounded-xl flex items-center justify-center border border-[#222] active:scale-90 transition-transform">
<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]" />
</button>
</div>
@@ -351,7 +351,7 @@ export default function HomePage() {
{/* ─── 1. Prayer Countdown Ring ─── */}
<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">
{/* SVG Ring */}
<div className="relative shrink-0">
@@ -402,7 +402,7 @@ export default function HomePage() {
{/* ─── 2. AI Morning Briefing ─── */}
<div className="mx-5 mb-5">
<div className="bg-gradient-to-br from-[#D4AF37]/10 to-[#B8860B]/5 rounded-2xl border border-[#D4AF37]/20 p-5 relative overflow-hidden">
<div className="glass rounded-[24px] p-5 relative overflow-hidden bg-gradient-to-br from-[#D4AF37]/5 to-transparent">
{/* Decorative glow */}
<div className="absolute -top-10 -right-10 w-32 h-32 bg-[#D4AF37]/10 rounded-full blur-3xl" />
<div className="relative z-10">
@@ -433,7 +433,7 @@ export default function HomePage() {
{/* ─── 3. Verse of the Day ─── */}
<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 */}
<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">
@@ -461,7 +461,7 @@ export default function HomePage() {
{/* ─── 4. Quick Dhikr ─── */}
<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 gap-2">
<span className="text-base">📿</span>
+16 -16
View File
@@ -30,20 +30,20 @@ export default function LoginPage() {
}
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 */}
<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">
<span className="text-3xl font-bold text-white">ف</span>
<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-bg-deep">ف</span>
</div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Welcome back</h1>
<p className="text-gray-500 text-sm mt-1">Sign in to your Falah account</p>
<h1 className="text-2xl font-bold text-white tracking-tight">Welcome back</h1>
<p className="text-text-secondary text-sm mt-1">Sign in to your Falah account</p>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 flex-1">
<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
type="email"
placeholder="you@example.com"
@@ -52,12 +52,12 @@ export default function LoginPage() {
required
autoComplete="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 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">
<input
type={showPass ? 'text' : 'password'}
@@ -66,12 +66,12 @@ export default function LoginPage() {
onChange={e => setPassword(e.target.value)}
required
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
type="button"
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} />}
</button>
@@ -79,28 +79,28 @@ export default function LoginPage() {
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3">
<p className="text-red-500 text-sm">{error}</p>
<div className="bg-red-950/20 border border-red-900/40 rounded-xl px-4 py-3">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
<button
type="submit"
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'}
</button>
</form>
<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?{' '}
<Link href="/register" className="text-emerald-600 font-semibold">
<Link href="/register" className="text-gold font-semibold">
Create one free
</Link>
</p>
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
<p className="text-xs text-text-muted">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
</div>
</div>
)
+9 -11
View File
@@ -129,7 +129,7 @@ export default function NurPage() {
}, [])
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 ═══ */}
<div className="shrink-0 flex items-center gap-3 px-5 pt-14 pb-3">
{/* Avatar */}
@@ -149,19 +149,18 @@ export default function NurPage() {
<div className="flex items-center gap-1.5">
<Link
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]" />
</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]" />
</button>
</div>
</div>
{/* ═══ Mood Check-in ═══ */}
<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">
How are you feeling?
</p>
@@ -214,8 +213,8 @@ export default function NurPage() {
<div
className={`max-w-[80%] px-4 py-3 ${
msg.role === 'user'
? 'gold-gradient text-[#0a0a0f] rounded-2xl rounded-br-sm'
: 'bg-[#1a1a2e] border border-[rgba(167,139,250,0.2)] text-white rounded-2xl rounded-bl-sm'
? 'gold-gradient text-[#0a0a0f] rounded-2xl rounded-br-sm font-semibold'
: 'glass-light text-white rounded-2xl rounded-bl-sm'
}`}
>
<p className="text-sm leading-relaxed whitespace-pre-wrap">{msg.content}</p>
@@ -240,7 +239,7 @@ export default function NurPage() {
<button
key={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>
</button>
@@ -248,9 +247,8 @@ export default function NurPage() {
</div>
</div>
{/* ═══ Input Bar (fixed at bottom above nav) ═══ */}
<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
type="text"
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 ${
isActive
? 'bg-[#D4AF37]/15 border border-[#D4AF37]/40 text-[#D4AF37]'
: 'bg-[#111118] border border-[#222] text-[#666]'
: 'glass-light text-[#666]'
}`}
>
{p.name}
+38 -38
View File
@@ -140,10 +140,10 @@ export default function PrayerPage() {
const strokeOffset = C * (1 - arcPercent)
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 */}
<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 */}
<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">
{city ? (
<>
<MapPin size={12} className="text-white/70" />
<span className="text-xs text-white/80">{city}</span>
<MapPin size={12} className="text-gold" />
<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>
{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
</p>
)}
@@ -167,9 +167,9 @@ export default function PrayerPage() {
<button
onClick={requestLocation}
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>
</div>
@@ -180,7 +180,7 @@ export default function PrayerPage() {
<div className="relative flex items-center justify-center mb-5">
<svg width="160" height="160" className="-rotate-90">
{/* 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 */}
<circle
cx="80" cy="80" r={R} fill="none"
@@ -194,7 +194,7 @@ export default function PrayerPage() {
{/* Glow dot at end of arc */}
<circle
cx="80" cy="80" r={R} fill="none"
stroke="rgba(255,255,255,0.4)"
stroke="rgba(255,255,255,0.3)"
strokeWidth="2"
strokeLinecap="round"
strokeDasharray="1"
@@ -205,18 +205,18 @@ export default function PrayerPage() {
{/* Center content */}
<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-[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>
</div>
</div>
{/* Countdown */}
<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}
</p>
<p className="text-sm text-white/60 mt-1.5">{countdown.unit}</p>
<p className="text-xs text-white/50 mt-1">
<p className="text-sm text-text-secondary mt-1.5">{countdown.unit}</p>
<p className="text-xs text-text-muted mt-1">
{next.name} at {fmt12(timings[next.name])}
</p>
</div>
@@ -226,22 +226,22 @@ export default function PrayerPage() {
{/* Loading skeleton inside header */}
{loading && !timings && (
<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-24 h-8 rounded-xl bg-white/10 animate-pulse mt-2" />
<div className="w-32 h-4 rounded-lg 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-bg-card border border-border animate-pulse mt-2" />
<div className="w-32 h-4 rounded-lg bg-bg-card border border-border animate-pulse" />
</div>
)}
</div>
{/* Location denied */}
{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>
<p className="text-gray-900 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-white font-bold text-lg mb-1">Enable Location</p>
<p className="text-text-secondary text-sm mb-6">Prayer times require your location to be accurate</p>
<button
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
</button>
@@ -250,8 +250,8 @@ export default function PrayerPage() {
{/* Error */}
{error && !locationDenied && (
<div className="mx-5 mt-4 bg-red-50 border border-red-100 rounded-2xl px-4 py-3 text-center">
<p className="text-red-500 text-sm">{error}</p>
<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-400 text-sm">{error}</p>
</div>
)}
@@ -268,10 +268,10 @@ export default function PrayerPage() {
key={prayer}
className={`flex items-center gap-3 px-4 py-3.5 rounded-2xl transition-all ${
isNext
? 'bg-amber-50 border border-[#D4AF37]'
? 'bg-[#D4AF37]/10 border border-[#D4AF37]/40'
: isPast
? 'bg-white border border-gray-100 shadow-sm opacity-50'
: 'bg-white border border-gray-100 shadow-sm'
? 'bg-bg-card/60 border border-border/60 shadow-sm opacity-50'
: 'bg-bg-card border border-border shadow-sm'
}`}
>
{/* Emoji */}
@@ -280,7 +280,7 @@ export default function PrayerPage() {
{/* Names */}
<div className="flex-1 min-w-0">
<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}
</span>
{isNext && (
@@ -289,11 +289,11 @@ export default function PrayerPage() {
</span>
)}
</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>
{/* 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])}
</span>
</div>
@@ -301,10 +301,10 @@ export default function PrayerPage() {
})}
{/* 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="flex-1 text-xs text-gray-500">Sunrise</span>
<span className="text-xs text-gray-500 tabular-nums">{fmt12(timings.Sunrise)}</span>
<span className="flex-1 text-xs text-text-muted">Sunrise</span>
<span className="text-xs text-text-muted tabular-nums">{fmt12(timings.Sunrise)}</span>
</div>
</div>
)}
@@ -313,13 +313,13 @@ export default function PrayerPage() {
{loading && !timings && (
<div className="px-4 mt-5 space-y-2">
{[...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 className="w-7 h-7 rounded-full bg-gray-200 animate-pulse" />
<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-bg-deep border border-border" />
<div className="flex-1 space-y-1.5">
<div className="h-3.5 w-20 rounded bg-gray-200 animate-pulse" />
<div className="h-2.5 w-12 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-bg-deep" />
</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>
@@ -327,7 +327,7 @@ export default function PrayerPage() {
{/* Footer note */}
{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
</p>
)}
+17 -8
View File
@@ -208,15 +208,24 @@ export default function ProfilePage() {
</div>
</div>
{/* ═══ Sign Out ═══ */}
{/* ═══ Sign Out / Login ═══ */}
<div className="mx-5 mb-6">
<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"
>
<LogOut size={16} className="text-red-400" />
<span className="text-sm font-semibold text-red-400">Sign Out</span>
</button>
{!user ? (
<Link
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"
>
Login
</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>
{/* Bottom spacer */}
+21 -21
View File
@@ -31,24 +31,24 @@ export default function RegisterPage() {
}
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 */}
<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">
<span className="text-3xl font-bold text-white">ف</span>
<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-bg-deep">ف</span>
</div>
<h1 className="text-2xl font-bold text-gray-900 tracking-tight">Join Falah</h1>
<p className="text-gray-500 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">
<Sparkles size={13} className="text-emerald-600" />
<span className="text-xs text-emerald-700 font-semibold">7-day Premium trial free</span>
<h1 className="text-2xl font-bold text-white tracking-tight">Join Falah</h1>
<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-950/20 border border-emerald-900/40 rounded-full px-3.5 py-2">
<Sparkles size={13} className="text-emerald-400" />
<span className="text-xs text-emerald-400 font-semibold">7-day Premium trial free</span>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-4 flex-1">
<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
type="text"
placeholder="Ahmad Abdullah"
@@ -56,12 +56,12 @@ export default function RegisterPage() {
onChange={e => setName(e.target.value)}
required
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 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
type="email"
placeholder="you@example.com"
@@ -70,12 +70,12 @@ export default function RegisterPage() {
required
autoComplete="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 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">
<input
type={showPass ? 'text' : 'password'}
@@ -85,12 +85,12 @@ export default function RegisterPage() {
required
minLength={8}
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
type="button"
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} />}
</button>
@@ -98,28 +98,28 @@ export default function RegisterPage() {
</div>
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3">
<p className="text-red-500 text-sm">{error}</p>
<div className="bg-red-950/20 border border-red-900/40 rounded-xl px-4 py-3">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
<button
type="submit"
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'}
</button>
</form>
<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?{' '}
<Link href="/login" className="text-emerald-600 font-semibold">
<Link href="/login" className="text-gold font-semibold">
Sign in
</Link>
</p>
<p className="text-xs text-gray-300">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
<p className="text-xs text-text-muted">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</p>
</div>
</div>
)
+46 -46
View File
@@ -61,19 +61,19 @@ export default function SouqPage() {
}
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 */}
<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>
<h1 className="text-xl font-bold text-gray-900">Souq</h1>
<p className="text-[11px] text-gray-400 mt-0.5">Islamic digital marketplace</p>
<h1 className="text-xl font-bold text-white">Souq</h1>
<p className="text-[11px] text-text-secondary mt-0.5">Islamic digital marketplace</p>
</div>
{token && (
<button
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
</button>
@@ -82,27 +82,27 @@ export default function SouqPage() {
{/* Search */}
<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
type="search"
placeholder="Search products…"
value={search}
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>
{/* 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 => (
<button
key={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
? 'bg-emerald-600 text-white'
: 'bg-white text-gray-500 border border-gray-200'
? 'gold-gradient text-bg-deep shadow-sm shadow-gold/15'
: 'bg-bg-card text-text-secondary border border-border'
}`}
>
<span>{CATEGORY_ICONS[c]}</span>
@@ -115,7 +115,7 @@ export default function SouqPage() {
{loading && (
<div className="px-5 pt-4 space-y-3">
{[...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>
)}
@@ -124,8 +124,8 @@ export default function SouqPage() {
{!loading && filtered.length === 0 && (
<div className="flex flex-col items-center justify-center pt-20 px-6 text-center">
<div className="text-5xl mb-4">🏪</div>
<p className="text-gray-900 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-white font-semibold mb-1">Nothing here yet</p>
<p className="text-text-secondary text-sm">Be the first to list a product</p>
</div>
)}
@@ -136,25 +136,25 @@ export default function SouqPage() {
<button
key={l.id}
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">
{/* 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] || '📦'}
</div>
<div className="flex-1 min-w-0">
<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" />}
</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">
<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>
<ChevronRight size={16} className="text-gray-300 shrink-0 mt-1" />
<ChevronRight size={16} className="text-text-muted shrink-0 mt-1" />
</div>
</button>
))}
@@ -163,7 +163,7 @@ export default function SouqPage() {
{/* 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}
</div>
)}
@@ -171,46 +171,46 @@ export default function SouqPage() {
{/* Detail bottom sheet */}
{selectedListing && (
<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
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()}
>
{/* 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="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] || '📦'}
</div>
<div className="flex-1 min-w-0">
<h2 className="text-lg font-bold text-gray-900 leading-tight">{selectedListing.title}</h2>
<p className="text-xs text-gray-400 mt-0.5">by {selectedListing.seller}</p>
<h2 className="text-lg font-bold text-white leading-tight">{selectedListing.title}</h2>
<p className="text-xs text-text-muted mt-0.5">by {selectedListing.seller}</p>
</div>
<button onClick={() => setSelectedListing(null)} className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100">
<X size={15} className="text-gray-400" />
<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} />
</button>
</div>
<span className="inline-block text-[11px] bg-gray-100 text-gray-500 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>
<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-text-secondary leading-relaxed mb-5">{selectedListing.description}</p>
<div className="flex items-center justify-between mb-5">
<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>
{token && selectedListing.sellerId !== user?.id ? (
<button
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
</button>
) : 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
</button>
)}
@@ -258,41 +258,41 @@ function CreateListingSheet({ token, onClose, onCreated, showToast }: {
return (
<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
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()}
>
<div className="w-10 h-1 bg-gray-200 rounded-full mx-auto mb-2" />
<h2 className="text-lg font-bold text-gray-900">New Listing</h2>
<div className="w-10 h-1 bg-border rounded-full mx-auto mb-2" />
<h2 className="text-lg font-bold text-white">New Listing</h2>
<input
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
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
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>
<input
type="number" placeholder="Price in FLH" value={priceFlh} onChange={e => setPriceFlh(e.target.value)} required min={1}
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
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'}
</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
</button>
</form>
+24 -24
View File
@@ -101,30 +101,30 @@ function UpgradeContent() {
}
if (authLoading) return (
<div className="min-h-dvh bg-[#f5f4f0] 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="min-h-dvh bg-bg-deep flex items-center justify-center pb-24">
<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-[#f5f4f0] pb-24">
<div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* 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
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} />
</button>
<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>
{/* Trial reminder */}
<div className="mx-4 -mt-4 mb-5 bg-white border border-emerald-100 rounded-2xl shadow-sm px-4 py-3">
<p className="text-xs text-emerald-700 font-medium">New accounts include a 7-day Premium trial free</p>
<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-400 font-medium">New accounts include a 7-day Premium trial free</p>
</div>
{/* Tiers */}
@@ -134,30 +134,30 @@ function UpgradeContent() {
return (
<div
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
? 'border-emerald-400'
: 'border-gray-200'
? 'border-gold/60 shadow-gold/5'
: 'border-border'
}`}
>
{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}
</div>
)}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center ${
tier.highlighted ? 'bg-amber-50' : 'bg-gray-50'
<div className={`w-10 h-10 rounded-2xl flex items-center justify-center border border-border/60 ${
tier.highlighted ? 'bg-[#D4AF37]/10' : 'bg-bg-deep'
}`}>
<Icon size={20} className={tier.iconColor} />
</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">
<span className="text-xl font-bold text-gray-900">{tier.price}</span>
<span className="text-gray-400 text-xs">{tier.period}</span>
<span className="text-xl font-bold text-white">{tier.price}</span>
<span className="text-text-muted text-xs">{tier.period}</span>
</div>
</div>
</div>
@@ -166,8 +166,8 @@ function UpgradeContent() {
<ul className="space-y-2.5 mb-5">
{tier.features.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<Check size={14} className="text-emerald-500 mt-0.5 shrink-0" />
<span className="text-sm text-gray-600">{f}</span>
<Check size={14} className="text-gold mt-0.5 shrink-0" />
<span className="text-sm text-text-secondary">{f}</span>
</li>
))}
</ul>
@@ -175,10 +175,10 @@ function UpgradeContent() {
{tier.priceId ? (
<button
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
? 'bg-emerald-600 text-white'
: 'bg-purple-50 text-purple-600 border border-purple-200'
? 'gold-gradient text-bg-deep'
: 'bg-purple-950/20 text-[#A78BFA] border border-purple-900/40 hover:bg-purple-950/40'
}`}
>
{tier.cta}
@@ -186,7 +186,7 @@ function UpgradeContent() {
) : (
<button
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}
</button>
@@ -196,7 +196,7 @@ function UpgradeContent() {
})}
</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.
</p>
</div>
+36 -35
View File
@@ -44,8 +44,8 @@ export default function WalletPage() {
}
if (authLoading) return (
<div className="min-h-dvh flex items-center justify-center bg-[#f5f4f0] pb-20">
<div className="w-8 h-8 rounded-full border-2 border-emerald-200 border-t-emerald-600 animate-spin" />
<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
@@ -53,38 +53,39 @@ export default function WalletPage() {
const fiatValue = amount ? ((parseInt(amount) || 0) * 0.008).toFixed(2) : '0.00'
return (
<div className="min-h-dvh bg-[#f5f4f0] pb-24">
<div className="min-h-dvh bg-bg-deep pb-32 text-white">
{/* Header */}
<div className="px-5 pt-8 pb-4 bg-white">
<h1 className="text-xl font-bold text-gray-900">Wallet</h1>
<p className="text-[11px] text-gray-400 mt-0.5">Manage your FLH balance</p>
<div className="px-5 pt-14 pb-4">
<h1 className="text-xl font-bold text-white">Wallet</h1>
<p className="text-[11px] text-text-secondary mt-0.5">Manage your FLH balance</p>
</div>
{/* 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="px-6 pt-6 pb-6">
<div className="mx-5 mb-5 rounded-3xl overflow-hidden shadow-lg glass border border-gold/30 relative">
<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">
<Wallet size={16} className="text-white/70" />
<span className="text-xs text-white/70 font-medium uppercase tracking-wider">FLH Balance</span>
<Wallet size={16} className="text-gold" />
<span className="text-xs text-text-secondary font-semibold uppercase tracking-wider">FLH Balance</span>
</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()}
</p>
<p className="text-white/80 font-semibold mt-1 text-base">FLH Tokens</p>
<p className="text-xs text-white/50 mt-3">
<p className="text-gold font-bold mt-1.5 text-base">FLH Tokens</p>
<p className="text-xs text-text-secondary mt-3">
£{((user?.flhBalance || 0) * 0.008).toFixed(2)} GBP at current rate
</p>
</div>
</div>
{/* 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">
<ArrowDownLeft size={16} className="text-emerald-600" />
<h2 className="font-bold text-gray-900 text-sm">Cash Out</h2>
<ArrowDownLeft size={16} className="text-gold" />
<h2 className="font-bold text-white text-sm">Cash Out</h2>
</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">
<input
@@ -94,18 +95,18 @@ export default function WalletPage() {
onChange={e => setAmount(e.target.value)}
min={100}
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 && (
<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-emerald-600 font-semibold">£{fiatValue} GBP</span>
<span className="text-xs text-text-secondary">{parseInt(amount).toLocaleString()} FLH</span>
<span className="text-xs text-gold font-semibold">£{fiatValue} GBP</span>
</div>
)}
<button
onClick={handleCashout}
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'}
</button>
@@ -116,32 +117,32 @@ export default function WalletPage() {
</div>
{/* History */}
<div className="mx-5 bg-white border border-gray-100 rounded-2xl shadow-sm p-5">
<h2 className="font-bold text-gray-900 text-sm mb-4">History</h2>
<div className="mx-5 glass rounded-3xl p-5">
<h2 className="font-bold text-white text-sm mb-4">History</h2>
{cashouts.length === 0 ? (
<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 className="space-y-3">
{cashouts.map(c => (
<div key={c.id} className="flex items-center gap-3 py-2">
<div className="w-9 h-9 rounded-full bg-gray-100 flex items-center justify-center shrink-0">
<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-bg-deep border border-border flex items-center justify-center shrink-0">
{c.status === 'pending'
? <Clock size={16} className="text-yellow-500" />
? <Clock size={16} className="text-gold" />
: c.status === 'approved'
? <CheckCircle2 size={16} className="text-emerald-600" />
? <CheckCircle2 size={16} className="text-emerald" />
: <XCircle size={16} className="text-red-500" />
}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-gray-900 font-medium">{c.amountFlh.toLocaleString()} FLH</p>
<p className="text-xs text-gray-400">£{c.fiatAmount.toFixed(2)} GBP</p>
<p className="text-sm text-white font-medium">{c.amountFlh.toLocaleString()} FLH</p>
<p className="text-xs text-text-secondary">£{c.fiatAmount.toFixed(2)} GBP</p>
</div>
<span className={`text-xs font-semibold capitalize px-2.5 py-1 rounded-full ${
c.status === 'pending' ? 'bg-yellow-50 text-yellow-700'
: c.status === 'approved' ? 'bg-emerald-50 text-emerald-700'
: 'bg-red-50 text-red-600'
<span className={`text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-full border ${
c.status === 'pending' ? 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20'
: c.status === 'approved' ? 'bg-emerald/10 text-emerald border-emerald/20'
: 'bg-red-500/10 text-red-500 border-red-500/20'
}`}>{c.status}</span>
</div>
))}
@@ -151,7 +152,7 @@ export default function WalletPage() {
{/* 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}
</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({
precacheEntries: self.__SW_MANIFEST,
+3 -1
View File
@@ -31,7 +31,9 @@
"include": [
"next-env.d.ts",
"src/**/*.ts",
"src/**/*.tsx"
"src/**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules",
+1
View File
@@ -8,6 +8,7 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**'],
alias: {
'@': path.resolve(__dirname, './src'),
},