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:
root
2026-06-24 07:02:03 +02:00
parent 913fa94fb9
commit cfff74e2db
81 changed files with 12132 additions and 2101 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

+49
View File
@@ -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"
}
]
}
+89
View File
@@ -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 });
}
}