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
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 759 B |
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "Falah — Islamic Lifestyle",
|
||||
"short_name": "Falah",
|
||||
"description": "Nur AI coaching, Halal marketplace, community & wallet",
|
||||
"start_url": "/mobile",
|
||||
"scope": "/mobile",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#0a0a0f",
|
||||
"theme_color": "#0a0a0f",
|
||||
"categories": ["lifestyle", "education", "finance"],
|
||||
"lang": "en",
|
||||
"dir": "ltr",
|
||||
"id": "/mobile/",
|
||||
"icons": [
|
||||
{ "src": "/mobile/icons/icon-48x48.png", "sizes": "48x48", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-72x72.png", "sizes": "72x72", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-96x96.png", "sizes": "96x96", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-384x384.png", "sizes": "384x384", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/mobile/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||
{ "src": "/mobile/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
],
|
||||
"screenshots": [],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Nur AI Coach",
|
||||
"short_name": "Nur AI",
|
||||
"description": "Open AI coaching assistant",
|
||||
"url": "/mobile/nur"
|
||||
},
|
||||
{
|
||||
"name": "Halal Market",
|
||||
"short_name": "Market",
|
||||
"description": "Browse halal marketplace",
|
||||
"url": "/mobile/market"
|
||||
},
|
||||
{
|
||||
"name": "Wallet",
|
||||
"short_name": "Wallet",
|
||||
"description": "Falah wallet & payments",
|
||||
"url": "/mobile/wallet"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Falah PWA Service Worker
|
||||
const CACHE = "falah-cache-v1";
|
||||
const IMMUTABLE_CACHE = "falah-immutable-v1";
|
||||
|
||||
// Assets to pre-cache on install
|
||||
const PRECACHE_ASSETS = [
|
||||
"/mobile/manifest.json",
|
||||
"/mobile/icons/icon-192x192.png",
|
||||
"/mobile/icons/icon-512x512.png",
|
||||
"/mobile/apple-touch-icon.png",
|
||||
"/mobile/offline"
|
||||
];
|
||||
|
||||
// Install: pre-cache core assets
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(IMMUTABLE_CACHE).then((cache) => cache.addAll(PRECACHE_ASSETS))
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate: clean old caches
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(
|
||||
keys
|
||||
.filter((k) => k !== CACHE && k !== IMMUTABLE_CACHE)
|
||||
.map((k) => caches.delete(k))
|
||||
)
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// Fetch: network-first for pages, cache-first for static assets
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle same-origin /mobile requests
|
||||
if (!url.pathname.startsWith("/mobile")) return;
|
||||
|
||||
// API calls — never cache
|
||||
if (url.pathname.startsWith("/mobile/api/")) return;
|
||||
|
||||
// Static assets (icons, fonts, images) — cache-first
|
||||
if (
|
||||
url.pathname.match(/\.(png|jpg|jpeg|gif|svg|ico|webp|woff2?|css)$/)
|
||||
) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigation & pages — network-first with fallback
|
||||
if (request.mode === "navigate") {
|
||||
event.respondWith(networkFirst(request));
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else — network-only
|
||||
event.respondWith(fetch(request).catch(() => caches.match(request)));
|
||||
});
|
||||
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(CACHE);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
// Offline fallback page
|
||||
return caches.match("/mobile/offline");
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cached = await caches.match(request);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
return await fetch(request);
|
||||
} catch {
|
||||
return new Response("", { status: 408 });
|
||||
}
|
||||
}
|
||||