feat: Souq native integration + auth simplification + CE-wide enhancements
Souq Marketplace: - Native souq pages: browse+filters, create listing, detail+seller profile, orders+chat - New API: reviews (POST), sellers/[id], upload (multipart), polar-checkout webhook - Listings API enhanced: minPrice, maxPrice, sortBy, deliveryDays filters - Seller workflow: start order, mark delivered, upload files, confirm delivery - Wallet: 5 Polar.sh FLH tiers, production checkout, mock fallback, success banner - Fix: order redirect /souq/history → /souq/orders Auth System: - Unified auth page, removed OAuth provider routing (2 files deleted) - Deleted oauth.ts (319 lines) — simplified auth chain - Streamlined login/register API routes Prisma Schema (+179 lines): - New models: Listing, Purchase, Review, Group/GroupMember - Gamification: DailyStreak, XpTransaction, Achievement, UserLevel - Learning: LearnCourse/Module/Enrollment/Certificate - Notifications, Referrals, Dhikr, PremiumBenefits Deleted legacy code: - src/app/api/marketplace/* (3 files) — replaced by /api/souq/* - src/app/api/files/[listingId]/route.ts — replaced by /api/souq/upload - src/app/api/seller/[sellerId]/route.ts — replaced by /api/souq/sellers/[id] Infrastructure: - Dockerfile: standalone output, Prisma runtime migration - docker-compose.yml: Polar env vars, healthcheck fix - docker-compose.staging.yml: staging on port 4014 - .gitea/workflows/ci.yml: CI pipeline
This commit is contained in:
+163
-65
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/lib/AuthContext";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
@@ -10,20 +10,34 @@ import {
|
||||
Wallet,
|
||||
User,
|
||||
Clock,
|
||||
Compass,
|
||||
BookOpen,
|
||||
MapPin,
|
||||
Sparkles,
|
||||
Star,
|
||||
X,
|
||||
Home,
|
||||
GraduationCap,
|
||||
} from "lucide-react";
|
||||
|
||||
const navItems = [
|
||||
{ href: "/nur", label: "Nur", icon: Bot },
|
||||
{ href: "/souq", label: "Souq", icon: ShoppingBag },
|
||||
{ href: "/", label: "Home", icon: null, isHome: true },
|
||||
{ href: "/prayer", label: "Prayer", icon: Clock },
|
||||
{ href: "/forum", label: "Forum", icon: MessageCircle },
|
||||
{ href: "/wallet", label: "Wallet", icon: Wallet },
|
||||
{ href: "/profile", label: "Profile", icon: User },
|
||||
// All destinations except Home (Home stays in center nav)
|
||||
const overflowItems = [
|
||||
{ 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" },
|
||||
{ href: "/wallet", label: "Wallet", icon: Wallet, color: "text-blue-400", bg: "bg-blue-900/20" },
|
||||
{ href: "/profile", label: "Profile", icon: User, color: "text-purple-400", bg: "bg-purple-900/20" },
|
||||
{ href: "/dhikr", label: "Dhikr", icon: BookOpen, color: "text-emerald-400", bg: "bg-emerald-900/20" },
|
||||
{ href: "/qibla", label: "Qibla", icon: Compass, color: "text-amber-400", bg: "bg-amber-900/20" },
|
||||
{ href: "/souq", label: "Souq", icon: ShoppingBag, color: "text-[#D4AF37]", bg: "bg-[#D4AF37]/15" },
|
||||
{ href: "/halal-monitor", label: "Halal Monitor", icon: MapPin, color: "text-emerald-400", bg: "bg-emerald-900/20" },
|
||||
{ href: "/learn", label: "Learn", icon: GraduationCap, color: "text-[#D4AF37]", bg: "bg-[#D4AF37]/15" },
|
||||
{ href: "/upgrade", label: "Upgrade", icon: Sparkles, color: "text-[#D4AF37]", bg: "bg-[#D4AF37]/15" },
|
||||
];
|
||||
|
||||
export default function BottomNav() {
|
||||
const pathname = usePathname();
|
||||
const [showSheet, setShowSheet] = useState(false);
|
||||
|
||||
// Hide bottom nav on auth pages
|
||||
if (pathname.startsWith("/auth")) {
|
||||
@@ -31,65 +45,149 @@ export default function BottomNav() {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-50 bg-[#0a0a0f]/95 backdrop-blur-md border-t border-gray-800/60 safe-area-bottom"
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }}
|
||||
>
|
||||
<div className="flex items-stretch h-14 max-w-lg mx-auto">
|
||||
{navItems.map((item) => {
|
||||
const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href + "/"));
|
||||
const Icon = item.icon;
|
||||
const isHome = item.isHome;
|
||||
<>
|
||||
{/* Overflow Bottom Sheet */}
|
||||
{showSheet && (
|
||||
<div className="fixed inset-0 z-[70] flex flex-col justify-end">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowSheet(false)}
|
||||
/>
|
||||
{/* Sheet */}
|
||||
<div
|
||||
className="relative w-full bg-[#111118] border-t border-gray-800/60 rounded-t-2xl animate-fade-in max-h-[80dvh] overflow-y-auto"
|
||||
style={{ paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)" }}
|
||||
>
|
||||
{/* Handle */}
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<div className="w-10 h-1 rounded-full bg-gray-700" />
|
||||
</div>
|
||||
|
||||
if (isHome) {
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href="/"
|
||||
className="flex-1 flex items-center justify-center"
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-2">
|
||||
<h2 className="text-sm font-semibold text-gray-400">Navigate</h2>
|
||||
<button
|
||||
onClick={() => setShowSheet(false)}
|
||||
className="w-8 h-8 rounded-full bg-gray-800/60 flex items-center justify-center active:bg-gray-700/60 transition"
|
||||
>
|
||||
<div
|
||||
className={`w-11 h-11 rounded-full flex items-center justify-center transition-all ${
|
||||
pathname === "/"
|
||||
? "bg-[#D4AF37] text-[#0a0a0f] shadow-[0_0_12px_rgba(212,175,55,0.3)]"
|
||||
: "bg-gray-800/60 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
<X size={16} className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="flex-1 flex flex-col items-center justify-center gap-0.5 active:opacity-60 transition-opacity relative"
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-3 gap-2 px-4 py-3">
|
||||
{overflowItems.map((item) => {
|
||||
const isActive = !item.external && (pathname === item.href || (item.href !== "/" && pathname?.startsWith(item.href + "/")));
|
||||
const Icon = item.icon;
|
||||
|
||||
if (item.external) {
|
||||
return (
|
||||
<a
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => setShowSheet(false)}
|
||||
className="flex flex-col items-center gap-1.5 py-3 rounded-xl active:scale-95 transition-all active:bg-gray-800/40"
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-2xl ${item.bg} flex items-center justify-center ${item.color}`}>
|
||||
<Icon size={22} />
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 font-medium truncate max-w-[80px] text-center">
|
||||
{item.label}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
onClick={() => setShowSheet(false)}
|
||||
className={`flex flex-col items-center gap-1.5 py-3 rounded-xl active:scale-95 transition-all ${
|
||||
isActive ? "bg-gray-800/40" : "active:bg-gray-800/40"
|
||||
}`}
|
||||
>
|
||||
<div className={`w-12 h-12 rounded-2xl ${isActive ? `${item.bg} ring-1 ring-white/10` : item.bg} flex items-center justify-center ${item.color}`}>
|
||||
<Icon size={22} />
|
||||
</div>
|
||||
<span className={`text-xs font-medium truncate max-w-[80px] text-center ${
|
||||
isActive ? "text-white" : "text-gray-500"
|
||||
}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Nav */}
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-50 bg-[#0a0a0f]/95 backdrop-blur-md border-t border-gray-800/60 safe-area-bottom"
|
||||
style={{ paddingBottom: "env(safe-area-inset-bottom, 0px)" }}
|
||||
>
|
||||
<div className="flex items-stretch h-14 max-w-lg mx-auto">
|
||||
{/* Left three-dots */}
|
||||
<button
|
||||
onClick={() => setShowSheet(true)}
|
||||
className="flex-1 flex items-center justify-center active:opacity-60 transition-opacity"
|
||||
aria-label="Open navigation menu"
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-500">
|
||||
<circle cx="12" cy="5" r="1.5" fill="currentColor" stroke="none" />
|
||||
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
|
||||
<circle cx="12" cy="19" r="1.5" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Home — center */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center justify-center"
|
||||
style={{ flex: "0 0 auto" }}
|
||||
>
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${
|
||||
pathname === "/"
|
||||
? "bg-[#D4AF37] text-[#0a0a0f] shadow-[0_0_16px_rgba(212,175,55,0.4)]"
|
||||
: "bg-gray-800/60 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
size={22}
|
||||
className={isActive ? "text-[#D4AF37]" : "text-gray-600"}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={`text-xs font-medium ${
|
||||
isActive ? "text-[#D4AF37]" : "text-gray-600"
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{/* Active indicator */}
|
||||
{isActive && (
|
||||
<div className="absolute -top-0.5 left-1/2 -translate-x-1/2 w-5 h-0.5 rounded-full bg-[#D4AF37]" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Right three-dots */}
|
||||
<button
|
||||
onClick={() => setShowSheet(true)}
|
||||
className="flex-1 flex items-center justify-center active:opacity-60 transition-opacity"
|
||||
aria-label="Open navigation menu"
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-gray-800/50 flex items-center justify-center">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-gray-500">
|
||||
<circle cx="12" cy="5" r="1.5" fill="currentColor" stroke="none" />
|
||||
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
|
||||
<circle cx="12" cy="19" r="1.5" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function PWARegister() {
|
||||
useEffect(() => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker
|
||||
.register("/mobile/sw.js", { scope: "/mobile" })
|
||||
.then((reg) => {
|
||||
console.log("[PWA] SW registered:", reg.scope);
|
||||
})
|
||||
.catch((err) => console.warn("[PWA] SW registration failed:", err));
|
||||
|
||||
// Listen for beforeinstallprompt (Android Chrome)
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
(window as Record<string, unknown>).__deferredPrompt = e;
|
||||
});
|
||||
|
||||
window.addEventListener("appinstalled", () => {
|
||||
console.log("[PWA] App installed");
|
||||
(window as Record<string, unknown>).__deferredPrompt = null;
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user