feat(falahmobile): retheme app to premium dark spatial UI, implement waqf module, and resolve testing suite config
This commit is contained in:
+22
-14
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+1
-1
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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
@@ -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'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'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">
|
||||
“Whoever says SubhanAllah 33 times, Alhamdulillah 33 times, and Allahu Akbar 34 times after each prayer…”
|
||||
</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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -208,15 +208,24 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ═══ Sign Out ═══ */}
|
||||
{/* ═══ Sign Out / Login ═══ */}
|
||||
<div className="mx-5 mb-6">
|
||||
{!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"
|
||||
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">Sign Out</span>
|
||||
<span className="text-sm font-semibold text-red-400">Logout</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom spacer */}
|
||||
|
||||
+21
-21
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
declare const self: ServiceWorkerGlobalScope
|
||||
declare const self: WorkerGlobalScope
|
||||
|
||||
const serwist = new Serwist({
|
||||
precacheEntries: self.__SW_MANIFEST,
|
||||
|
||||
+3
-1
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user