From 0cacc4fa1aa9c3e4b2b3ecacbbc51c491b848a5c Mon Sep 17 00:00:00 2001 From: FalahMobile Date: Wed, 8 Jul 2026 18:07:07 +0800 Subject: [PATCH] feat(falahmobile): retheme app to premium dark spatial UI, implement waqf module, and resolve testing suite config --- e2e/auth-flow.spec.ts | 36 ++-- next-env.d.ts | 2 +- playwright.config.ts | 2 +- src/app/api/seed/route.ts | 8 +- src/app/api/waqf/contribute/route.ts | 46 +++++ src/app/dhikr/page.tsx | 96 +++++----- src/app/forum/page.tsx | 70 +++---- src/app/globals.css | 17 +- src/app/halal-monitor/page.tsx | 74 ++++---- src/app/home/page.tsx | 12 +- src/app/login/page.tsx | 32 ++-- src/app/nur/page.tsx | 20 +- src/app/prayer/page.tsx | 76 ++++---- src/app/profile/page.tsx | 25 ++- src/app/register/page.tsx | 42 ++--- src/app/souq/page.tsx | 92 ++++----- src/app/upgrade/page.tsx | 48 ++--- src/app/wallet/page.tsx | 71 +++---- src/app/waqf/page.tsx | 272 +++++++++++++++++++++++++++ src/sw.ts | 2 +- tsconfig.json | 4 +- vitest.config.ts | 1 + 22 files changed, 693 insertions(+), 355 deletions(-) create mode 100644 src/app/api/waqf/contribute/route.ts create mode 100644 src/app/waqf/page.tsx diff --git a/e2e/auth-flow.spec.ts b/e2e/auth-flow.spec.ts index 6687775..71ed246 100644 --- a/e2e/auth-flow.spec.ts +++ b/e2e/auth-flow.spec.ts @@ -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(); }); }); diff --git a/next-env.d.ts b/next-env.d.ts index c4b7818..9edff1c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -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. diff --git a/playwright.config.ts b/playwright.config.ts index d61dbbc..74c77c9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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, }, diff --git a/src/app/api/seed/route.ts b/src/app/api/seed/route.ts index c30b9fc..8bd1bea 100644 --- a/src/app/api/seed/route.ts +++ b/src/app/api/seed/route.ts @@ -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[] = [] diff --git a/src/app/api/waqf/contribute/route.ts b/src/app/api/waqf/contribute/route.ts new file mode 100644 index 0000000..e183387 --- /dev/null +++ b/src/app/api/waqf/contribute/route.ts @@ -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 }) + } +} diff --git a/src/app/dhikr/page.tsx b/src/app/dhikr/page.tsx index 6201a8f..a0a76f7 100644 --- a/src/app/dhikr/page.tsx +++ b/src/app/dhikr/page.tsx @@ -109,14 +109,14 @@ export default function DhikrPage() { if (!user) { return ( -
+
📿
-

Daily Dhikr

-

+

Daily Dhikr

+

Build a daily habit of remembrance. Sign in to track your streak.

@@ -125,24 +125,24 @@ export default function DhikrPage() { if (loading) { return ( -
-
+
+
) } return ( -
+
{/* Header gradient */} -
+

Daily Dhikr

-

100 remembrances · after prayer

+

100 remembrances · after prayer

{/* Streak badge inside header */} -
- +
+ {streak}
@@ -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' }`} >
{d.transliteration}
@@ -183,18 +183,18 @@ export default function DhikrPage() { {/* Completed today (not just-finished) */} {todayCompleted && !justFinished ? (
-
-
- +
+
+
-

MashaAllah!

-

You've completed your daily dhikr

-
- - {streak} day streak +

MashaAllah!

+

You've completed your daily dhikr

+
+ + {streak} day streak
-
@@ -202,20 +202,20 @@ export default function DhikrPage() { ) : justFinished ? ( /* Completion screen */
-
-

SubhanAllah!

-

100 remembrances complete

-
+
+

SubhanAllah!

+

100 remembrances complete

+
- {streak} day streak + {streak} day streak
-
-

+

+

“Whoever says SubhanAllah 33 times, Alhamdulillah 33 times, and Allahu Akbar 34 times after each prayer…”

-

— Sahih Muslim

+

— Sahih Muslim

-
@@ -223,11 +223,11 @@ export default function DhikrPage() { ) : ( /* Main counter */
- {/* Ring counter — white card */} -
+ {/* Ring counter — card */} +
{/* Dhikr text below ring, inside card */}
-

{current.arabic}

+

{current.arabic}

{current.transliteration}

-

{current.meaning}

+

{current.meaning}

-

Tap to count

+

Tap to count

)} {/* Bottom progress bar */} {!justFinished && ( -
-
+
+
Total today {Math.min(totalCount, TOTAL)} / {TOTAL}
-
+
{ setToast(msg); setTimeout(() => setToast(''), 3000) } return ( -
+
{/* Header */} -
+
{view === 'threads' && ( - )}
-

{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}

-

+

{view === 'threads' && selectedCat ? selectedCat.name : 'Forum'}

+

{view === 'categories' ? 'Community discussions' : `${threads.length} threads`}

@@ -59,7 +59,7 @@ export default function ForumPage() { {token && ( @@ -70,7 +70,7 @@ export default function ForumPage() { {loading && (
{[...Array(4)].map((_, i) => ( -
+
))}
)} @@ -81,23 +81,23 @@ export default function ForumPage() { {categories.length === 0 ? (
🕌
-

No categories yet

+

No categories yet

) : categories.map(c => ( ))} @@ -110,21 +110,21 @@ export default function ForumPage() { {threads.length === 0 ? (
💬
-

No threads yet

-

Start the first discussion

+

No threads yet

+

Start the first discussion

) : threads.map(t => ( -
-

{t.title}

-

{t.content}

+
+

{t.title}

+

{t.content}

-
- {t.author.name[0]} +
+ {t.author.name[0]}
- {t.author.name} + {t.author.name}
-
+
{t._count.posts}
@@ -136,7 +136,7 @@ export default function ForumPage() { {/* Toast */} {toast && ( -
+
{toast}
)} @@ -182,38 +182,38 @@ function CreateThreadSheet({ token, categories, onClose, onCreated, showToast }: return (
-
+
e.stopPropagation()} >
-
-

New Thread

-
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" />