feat: Phase 1 WP integration — API proxy, Store, Freebies, Blog, Tools pages
Deploy Staging / build (push) Failing after 9m32s
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:
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user