feat: Phase 1 WP integration — API proxy, Store, Freebies, Blog, Tools pages
Deploy Staging / build (push) Failing after 9m32s

- Add WP REST API proxy (/api/wp/[...path])
- Add Store page ( from Gumroad/Stripe)
- Add Freebies page (WP content via REST API)
- Add Blog page (WP posts via REST API)
- Add Tools page (index of Islamic tools)
- Add public nav entries in BottomNav
- No WP modifications — all server-side

Docs: https://git.falahos.my/falah-os/war-room/wiki/UI-UX-Audit-mobile.falahos.my
This commit is contained in:
2026-07-09 18:41:47 +02:00
parent 6d946571b0
commit 6c07dcd098
9 changed files with 509 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
const WP_API_BASE = "https://ummah.falahos.my/wp-json";
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
try {
const { path } = await params;
const pathStr = path.join("/");
const searchParams = _request.nextUrl.searchParams.toString();
const url = `${WP_API_BASE}/${pathStr}${searchParams ? `?${searchParams}` : ""}`;
const response = await fetch(url, {
headers: {
"Accept": "application/json",
"User-Agent": "FalahOS-Mobile/1.0",
},
next: { revalidate: 300 }, // 5 min cache
});
if (!response.ok) {
return NextResponse.json(
{ error: `WP API error: ${response.statusText}` },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, {
headers: {
"Cache-Control": "public, s-maxage=300, stale-while-revalidate=600",
},
});
} catch (error) {
console.error("WP API proxy error:", error);
return NextResponse.json(
{ error: "Failed to fetch from WordPress API" },
{ status: 502 }
);
}
}
+7
View File
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
);
}
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { useState, useEffect } from "react";
import { Newspaper, Calendar, ArrowLeft, ExternalLink } from "lucide-react";
import Link from "next/link";
interface Post {
id: number;
title: string;
slug: string;
excerpt: string;
date: string;
link: string;
}
export default function BlogPage() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/mobile/api/wp/wp/v2/posts?per_page=10&_fields=id,title,slug,excerpt,date,link")
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data)) {
setPosts(
data.map((p: any) => ({
id: p.id,
title: p.title?.rendered || "",
slug: p.slug,
excerpt: p.excerpt?.rendered?.replace(/<[^>]+>/g, "") || "",
date: p.date,
link: p.link,
}))
);
}
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
const formatDate = (d: string) =>
new Date(d).toLocaleDateString("en-MY", {
day: "numeric",
month: "long",
year: "numeric",
});
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
<div className="px-4 pt-6 pb-4 flex items-center gap-3">
<Link href="/" className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center active:opacity-60 transition">
<ArrowLeft size={18} className="text-gray-400" />
</Link>
<div>
<h1 className="text-lg font-bold text-white flex items-center gap-2">
<Newspaper size={18} className="text-sky-400" />
Falah Fintech Blog
</h1>
<p className="text-xs text-gray-500">Islamic fintech insights & updates</p>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
) : posts.length === 0 ? (
<div className="text-center py-10 px-4">
<Newspaper size={40} className="mx-auto text-gray-700 mb-3" />
<p className="text-sm text-gray-500">No posts yet</p>
<p className="text-xs text-gray-700 mt-1">Check back soon for updates</p>
</div>
) : (
<div className="px-4 space-y-3">
{posts.map((post) => (
<a
key={post.id}
href={post.link}
target="_blank"
rel="noopener noreferrer"
className="block bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.98] transition"
>
<div className="flex items-start justify-between mb-2">
<h2 className="text-sm font-semibold text-white flex-1 pr-4">
{post.title}
</h2>
<ExternalLink size={14} className="text-gray-600 shrink-0 mt-1" />
</div>
{post.excerpt && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">{post.excerpt}</p>
)}
<div className="flex items-center gap-2 text-[10px] text-gray-600">
<Calendar size={10} />
{formatDate(post.date)}
</div>
</a>
))}
</div>
)}
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
);
}
+62
View File
@@ -0,0 +1,62 @@
"use client";
import { useState, useEffect } from "react";
import { Download, Gift, ExternalLink, ArrowLeft } from "lucide-react";
import Link from "next/link";
export default function FreebiesPage() {
const [content, setContent] = useState<string>("");
const [title, setTitle] = useState("Free Islamic Resources");
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/mobile/api/wp/wp/v2/pages?slug=freebies")
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data) && data.length > 0) {
const page = data[0];
setTitle(page.title?.rendered || "Free Islamic Resources");
setContent(page.content?.rendered || "");
}
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
<div className="px-4 pt-6 pb-4 flex items-center gap-3">
<Link href="/" className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center active:opacity-60 transition">
<ArrowLeft size={18} className="text-gray-400" />
</Link>
<div>
<h1 className="text-lg font-bold text-white flex items-center gap-2">
<Gift size={18} className="text-emerald-400" />
{title}
</h1>
<p className="text-xs text-gray-500">Free downloads & resources</p>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
) : (
<div className="px-4">
{content ? (
<div
className="prose prose-invert max-w-none text-sm text-gray-300 [&_a]:text-[#D4AF37] [&_h2]:text-white [&_h3]:text-white [&_ul]:space-y-2 [&_li]:text-gray-400 [&_img]:rounded-xl"
dangerouslySetInnerHTML={{ __html: content }}
/>
) : (
<div className="text-center py-10">
<Download size={40} className="mx-auto text-gray-700 mb-3" />
<p className="text-sm text-gray-500">Loading resources...</p>
</div>
)}
</div>
)}
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
export default function Loading() {
return (
<div className="min-h-dvh bg-[#0a0a0f] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
);
}
+187
View File
@@ -0,0 +1,187 @@
"use client";
import { useState, useEffect } from "react";
import { ShoppingBag, ExternalLink, Star, Download, ArrowLeft } from "lucide-react";
import Link from "next/link";
interface Product {
id: string;
name: string;
description: string;
price: string;
currency: string;
image: string;
url: string;
}
export default function StorePage() {
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const wpContent = sessionStorage.getItem("wp_store_content");
if (wpContent) {
try {
setProducts(JSON.parse(wpContent));
setLoading(false);
return;
} catch {}
}
fetch("/mobile/api/wp/wp/v2/pages?slug=store")
.then((r) => r.json())
.then((data) => {
if (Array.isArray(data) && data.length > 0) {
const page = data[0];
const parser = new DOMParser();
const doc = parser.parseFromString(page.content?.rendered || "", "text/html");
const productElements = doc.querySelectorAll(".edd_download, .product, .wp-block-column");
const parsed: Product[] = Array.from(productElements).map((el, i) => ({
id: `wp-${i}`,
name: el.querySelector("h3, .edd_download_title, strong")?.textContent || `Product ${i + 1}`,
description: el.querySelector("p, .edd_download_excerpt")?.textContent || "",
price: el.querySelector(".edd_price, .amount, .price")?.textContent || "$—",
currency: "USD",
image: el.querySelector("img")?.getAttribute("src") || "",
url: el.querySelector("a")?.getAttribute("href") || "#",
}));
setProducts(parsed.length > 0 ? parsed : getDefaultProducts());
} else {
setProducts(getDefaultProducts());
}
setLoading(false);
})
.catch(() => {
setProducts(getDefaultProducts());
setLoading(false);
});
}, []);
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
{/* Header */}
<div className="px-4 pt-6 pb-4 flex items-center gap-3">
<Link href="/" className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center active:opacity-60 transition">
<ArrowLeft size={18} className="text-gray-400" />
</Link>
<div>
<h1 className="text-lg font-bold text-white flex items-center gap-2">
<ShoppingBag size={18} className="text-[#D4AF37]" />
Digital Store
</h1>
<p className="text-xs text-gray-500">Islamic digital products</p>
</div>
</div>
{loading ? (
<div className="flex items-center justify-center py-20">
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin" />
</div>
) : error ? (
<div className="mx-4 p-5 bg-red-900/20 border border-red-800/30 rounded-2xl text-center">
<p className="text-sm text-red-400">{error}</p>
</div>
) : (
<div className="px-4 space-y-4">
{products.length === 0 && (
<div className="text-center py-10">
<ShoppingBag size={40} className="mx-auto text-gray-700 mb-3" />
<p className="text-sm text-gray-500">No products available yet</p>
</div>
)}
{products.map((product) => (
<div
key={product.id}
className="bg-[#111118] border border-gray-800/60 rounded-2xl overflow-hidden active:scale-[0.98] transition"
>
{product.image && (
<img
src={product.image}
alt={product.name}
className="w-full h-40 object-cover"
loading="lazy"
/>
)}
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<h3 className="text-sm font-semibold text-white flex-1">{product.name}</h3>
<span className="text-sm font-bold text-[#D4AF37] ml-2">{product.price}</span>
</div>
{product.description && (
<p className="text-xs text-gray-500 mb-3 line-clamp-2">{product.description}</p>
)}
<div className="flex items-center gap-2">
<span className="text-[10px] px-2 py-1 rounded-full bg-emerald-900/30 text-emerald-300 border border-emerald-700/30 flex items-center gap-1">
<Download size={10} /> Digital
</span>
<span className="text-[10px] px-2 py-1 rounded-full bg-[#D4AF37]/15 text-[#D4AF37] border border-[#D4AF37]/30">
{product.currency}
</span>
</div>
<a
href={product.url}
target="_blank"
rel="noopener noreferrer"
className="mt-3 w-full py-3 bg-gradient-to-r from-[#D4AF37] to-amber-500 text-[#0a0a0f] text-sm font-semibold rounded-xl flex items-center justify-center gap-2 active:scale-[0.98] transition min-h-[44px]"
>
<ExternalLink size={14} /> Buy Now
</a>
</div>
</div>
))}
</div>
)}
</div>
);
}
function getDefaultProducts(): Product[] {
return [
{
id: "gumroad-1",
name: "Halal Business Plan Pro",
description: "Comprehensive halal business plan template with financial projections, market analysis, and Islamic finance compliance checklist.",
price: "$12.00",
currency: "USD",
image: "",
url: "https://alfalahtech.gumroad.com/l/halal-business-plan-pro",
},
{
id: "gumroad-2",
name: "Faraid Inheritance Workbook",
description: "Step-by-step workbook for Islamic inheritance planning (Faraid). Includes calculation sheets, distribution charts, and family worksheets.",
price: "$10.00",
currency: "USD",
image: "",
url: "https://alfalahtech.gumroad.com/l/faraid-inheritance-workbook",
},
{
id: "gumroad-3",
name: "30-Day Quran Journal",
description: "Structured daily Quran journal with reflection prompts, Arabic verses, translation, and space for personal notes — spanning 30 days.",
price: "$7.00",
currency: "USD",
image: "",
url: "https://alfalahtech.gumroad.com/l/30-day-quran-journal",
},
{
id: "gumroad-4",
name: "Islamic Budgeting Bundle",
description: "Bundle of budgeting templates aligned with Islamic finance principles: monthly budget, debt tracker, savings goal planner, and zakat calculator.",
price: "$9.00",
currency: "USD",
image: "",
url: "https://alfalahtech.gumroad.com/l/islamic-budgeting-bundle",
},
{
id: "gumroad-5",
name: "Islamic Social Templates 50pk",
description: "50 premium social media templates for Islamic content creators. Includes Instagram posts, stories, and Facebook covers in PSD and Canva formats.",
price: "$10.00",
currency: "USD",
image: "",
url: "https://alfalahtech.gumroad.com/l/islamic-social-templates-50pk",
},
];
}
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { useState, useEffect } from "react";
import { Compass, CircleDot, BookOpen, MapPin, ArrowLeft, Wrench } from "lucide-react";
import Link from "next/link";
const tools = [
{
href: "/prayer",
icon: Compass,
label: "Prayer Times",
desc: "Accurate prayer times based on your location",
color: "bg-emerald-900/30 text-emerald-300",
public: true,
},
{
href: "/qibla",
icon: MapPin,
label: "Qibla Finder",
desc: "Find direction to the Kaaba",
color: "bg-amber-900/30 text-amber-300",
public: false,
},
{
href: "/dhikr",
icon: CircleDot,
label: "Dhikr Counter",
desc: "Digital tasbih with daily tracking",
color: "bg-sky-900/30 text-sky-300",
public: false,
},
{
href: "/learn",
icon: BookOpen,
label: "Learning Hub",
desc: "Islamic courses and modules",
color: "bg-purple-900/30 text-purple-300",
public: false,
},
];
export default function ToolsPage() {
return (
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
<div className="px-4 pt-6 pb-4 flex items-center gap-3">
<Link href="/" className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center active:opacity-60 transition">
<ArrowLeft size={18} className="text-gray-400" />
</Link>
<div>
<h1 className="text-lg font-bold text-white flex items-center gap-2">
<Wrench size={18} className="text-gray-400" />
Islamic Tools
</h1>
<p className="text-xs text-gray-500">Essential daily tools</p>
</div>
</div>
<div className="px-4 space-y-3">
{tools.map((tool) => (
<Link
key={tool.href}
href={tool.href}
className="block bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.98] transition"
>
<div className="flex items-center gap-3">
<div className={`w-12 h-12 rounded-xl ${tool.color} flex items-center justify-center`}>
<tool.icon size={22} />
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
{tool.label}
{!tool.public && (
<span className="text-[9px] px-1.5 py-0.5 rounded-full bg-gray-800 text-gray-500">
login required
</span>
)}
</h3>
<p className="text-xs text-gray-500 mt-0.5">{tool.desc}</p>
</div>
</div>
</Link>
))}
</div>
</div>
);
}
+7
View File
@@ -18,10 +18,17 @@ import {
X,
Home,
GraduationCap,
Wrench,
Gift,
Newspaper,
} from "lucide-react";
// All destinations except Home (Home stays in center nav)
const overflowItems = [
{ href: "/store", label: "Store", icon: ShoppingBag, color: "text-[#D4AF37]", bg: "bg-[#D4AF37]/15" },
{ href: "/freebies", label: "Freebies", icon: Star, color: "text-emerald-400", bg: "bg-emerald-900/20" },
{ href: "/blog", label: "Blog", icon: MessageCircle, color: "text-sky-400", bg: "bg-sky-900/20" },
{ href: "/tools", label: "Tools", icon: Wrench, color: "text-amber-400", bg: "bg-amber-900/20" },
{ href: "/nur", label: "Nur AI", icon: Bot, color: "text-amber-400", bg: "bg-amber-900/20" },
{ href: "/prayer", label: "Prayer", icon: Clock, color: "text-emerald-400", bg: "bg-emerald-900/20" },
{ href: "/forum", label: "Forum", icon: MessageCircle, color: "text-sky-400", bg: "bg-sky-900/20" },