Referral page, bug fixes, seed data, persona tiers
- New /refer page with share/copy/stats - Fixed modal z-index conflicts (z-50 → z-[60]) across forum, groups, souq, halal-monitor - Seed route: forum categories, marketplace listings, private groups - Mufti/Daie personas now available in Premium tier - Fixed referral share link: falahos.my/mobile/auth?ref=CODE - Fixed client-side persona access check for premium
This commit is contained in:
@@ -88,7 +88,11 @@ async function handleCallback(
|
||||
});
|
||||
|
||||
// Build the redirect URI (must match the one used in the authorization request)
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
const forwardedProto = req.headers.get("x-forwarded-proto") || "https";
|
||||
const rawHost = req.headers.get("host") || "";
|
||||
const isDockerHost = /^[a-f0-9]{12,}$/i.test(rawHost.split(":")[0]);
|
||||
const forwardedHost = req.headers.get("x-forwarded-host") || (isDockerHost ? "falahos.my" : rawHost);
|
||||
const baseUrl = `${forwardedProto}://${forwardedHost}`;
|
||||
const redirectUri = `${baseUrl}/mobile/api/auth/${provider}/callback`;
|
||||
|
||||
// Exchange code for user info
|
||||
|
||||
@@ -35,8 +35,13 @@ export async function GET(
|
||||
}
|
||||
|
||||
// Build the redirect URI — the callback URL for this provider
|
||||
const url = new URL(_req.url);
|
||||
const baseUrl = `${url.protocol}//${url.host}`;
|
||||
// Use forwarded headers (Traefik/Cloudflare) or the original Host header
|
||||
// to ensure the public domain is used, not the Docker container hostname.
|
||||
const forwardedProto = _req.headers.get("x-forwarded-proto") || "https";
|
||||
const rawHost = _req.headers.get("host") || "";
|
||||
const isDockerHost = /^[a-f0-9]{12,}$/i.test(rawHost.split(":")[0]);
|
||||
const forwardedHost = _req.headers.get("x-forwarded-host") || (isDockerHost ? "falahos.my" : rawHost);
|
||||
const baseUrl = `${forwardedProto}://${forwardedHost}`;
|
||||
const redirectUri = `${baseUrl}/mobile/api/auth/${provider}/callback`;
|
||||
|
||||
// Generate state for CSRF protection
|
||||
|
||||
@@ -2,10 +2,14 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { signJWT } from "@/lib/auth";
|
||||
import { findReferrerByCode } from "@/lib/referral";
|
||||
|
||||
const REFERRER_REWARD_FLH = 200;
|
||||
const REFERRED_BONUS_FLH = 1000;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email, name, password } = await req.json();
|
||||
const { email, name, password, referralCode } = await req.json();
|
||||
|
||||
if (!email || !name || !password) {
|
||||
return NextResponse.json(
|
||||
@@ -24,17 +28,66 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
||||
// Look up referrer if a referral code was provided
|
||||
let referrerId: string | null = null;
|
||||
if (referralCode && typeof referralCode === "string") {
|
||||
const users = await prisma.user.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
referrerId = await findReferrerByCode(users, referralCode);
|
||||
}
|
||||
|
||||
// Calculate starting balance
|
||||
let startingBalance = 5000; // default sign-up bonus
|
||||
if (referrerId) {
|
||||
startingBalance += REFERRED_BONUS_FLH; // +1,000 FLH for using a referral code
|
||||
}
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
name,
|
||||
passwordHash,
|
||||
provider: "email",
|
||||
flhBalance: 5000,
|
||||
flhBalance: startingBalance,
|
||||
trialEndsAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
});
|
||||
|
||||
// If referral code was valid, create the referral record and credit the referrer
|
||||
if (referrerId) {
|
||||
await prisma.$transaction([
|
||||
// Create the referral record
|
||||
prisma.referral.create({
|
||||
data: {
|
||||
referrerId,
|
||||
referredId: user.id,
|
||||
status: "joined",
|
||||
rewardFlh: REFERRER_REWARD_FLH,
|
||||
},
|
||||
}),
|
||||
// Credit the referrer
|
||||
prisma.user.update({
|
||||
where: { id: referrerId },
|
||||
data: { flhBalance: { increment: REFERRER_REWARD_FLH } },
|
||||
}),
|
||||
// Notify the referrer
|
||||
prisma.notification.create({
|
||||
data: {
|
||||
userId: referrerId,
|
||||
type: "referral_bonus",
|
||||
title: "Referral Bonus Earned!",
|
||||
body: `${name} joined Falah using your referral code — you earned ${REFERRER_REWARD_FLH} FLH!`,
|
||||
data: JSON.stringify({
|
||||
referredUserId: user.id,
|
||||
referredName: name,
|
||||
rewardFlh: REFERRER_REWARD_FLH,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
const token = await signJWT({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
@@ -53,6 +106,8 @@ export async function POST(req: NextRequest) {
|
||||
flhBalance: user.flhBalance,
|
||||
dailyMsgCount: user.dailyMsgCount,
|
||||
},
|
||||
referralApplied: referrerId ? true : false,
|
||||
referralReward: referrerId ? REFERRED_BONUS_FLH : 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Register error:", error);
|
||||
|
||||
@@ -108,8 +108,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate AI response
|
||||
const aiContent = generateAIReply(personaId, lastUserMessage, historyContext);
|
||||
// Generate AI response (tries LLM first, falls back to keyword rules)
|
||||
const aiContent = await generateAIReply(personaId, lastUserMessage, historyContext);
|
||||
|
||||
// Save AI response to ChatHistory
|
||||
const aiMessage = await prisma.chatHistory.create({
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Decode a base62 referral code back to a userId prefix for lookup
|
||||
function decodeBase62Lookup(code: string): number {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
let hash = 0;
|
||||
for (const ch of code) {
|
||||
const idx = chars.indexOf(ch);
|
||||
if (idx === -1) return -1;
|
||||
hash = hash * 62 + idx;
|
||||
}
|
||||
return hash >>> 0;
|
||||
}
|
||||
import { findReferrerByCode } from "@/lib/referral";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
@@ -25,42 +14,12 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// In a real system, you'd look up the referrer by their stored referral code.
|
||||
// For simplicity, we encode the userId in the code. We need to find the user
|
||||
// whose userId produces this code. Since encodeBase62 uses a hash, we
|
||||
// can't reverse it perfectly. Instead, we iterate active users.
|
||||
// A production approach: store referral codes in a dedicated table or column.
|
||||
// For now, we'll iterate through users and find a match.
|
||||
// Look up the referrer by matching the referral code against all users
|
||||
const users = await prisma.user.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
// Recreate the encode logic to find the matching user
|
||||
function encodeForMatch(id: string): string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
let hash = 0;
|
||||
for (let i = 0; i < Math.min(id.length, 10); i++) {
|
||||
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
let result = "";
|
||||
let h = hash;
|
||||
while (result.length < 6) {
|
||||
result = chars[h % 62] + result;
|
||||
h = Math.floor(h / 62);
|
||||
if (h === 0 && result.length < 6) {
|
||||
h = Math.floor(Math.random() * 62 ** (6 - result.length));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
let referrerId: string | null = null;
|
||||
for (const u of users) {
|
||||
if (encodeForMatch(u.id) === code) {
|
||||
referrerId = u.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const referrerId = await findReferrerByCode(users, code);
|
||||
|
||||
if (!referrerId) {
|
||||
return NextResponse.json(
|
||||
@@ -76,12 +35,6 @@ export async function POST(req: NextRequest) {
|
||||
// Since we don't have a "referredId" yet, we store a temporary token.
|
||||
const tempToken = `pending_${referrerId}_${Date.now()}`;
|
||||
|
||||
// Check if this referrer already has a pending referral with this temp token
|
||||
const existing = await prisma.referral.findFirst({
|
||||
where: { referrerId, status: "pending" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
// Store in a lightweight way — we'll use the Referral model with a placeholder referredId
|
||||
// The referredId will be updated when registration completes.
|
||||
// For production, a ReferralClaimToken model would be better.
|
||||
@@ -96,7 +49,7 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "Referral code applied! You'll get 1000 FLH when you sign up.",
|
||||
message: "Referral code applied! You'll get 1,000 FLH extra when you sign up, and your referrer earns 200 FLH.",
|
||||
referralId: referral.id,
|
||||
tempToken,
|
||||
});
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
// Encode a userId (UUID/CUID) into a short base62 referral code
|
||||
function encodeBase62(id: string): string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
// Convert first 10 chars of the id to a numeric hash for a short code
|
||||
let hash = 0;
|
||||
for (let i = 0; i < Math.min(id.length, 10); i++) {
|
||||
hash = (hash * 31 + id.charCodeAt(i)) >>> 0; // unsigned 32-bit
|
||||
}
|
||||
let code = "";
|
||||
while (code.length < 6) {
|
||||
code = chars[hash % 62] + code;
|
||||
hash = Math.floor(hash / 62);
|
||||
if (hash === 0 && code.length < 6) {
|
||||
// Pad with random chars
|
||||
hash = Math.floor(Math.random() * 62 ** (6 - code.length));
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
import { encodeReferralCode } from "@/lib/referral";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const jwtPayload = await requireAuth(req);
|
||||
@@ -28,11 +9,11 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const referralCode = encodeBase62(jwtPayload.userId);
|
||||
const referralCode = encodeReferralCode(jwtPayload.userId);
|
||||
|
||||
return NextResponse.json({
|
||||
referralCode,
|
||||
shareLink: `${process.env.NEXT_PUBLIC_APP_URL || "https://falah.app"}/join?ref=${referralCode}`,
|
||||
shareLink: `${process.env.NEXT_PUBLIC_APP_URL || "https://falahos.my/mobile"}/auth?ref=${referralCode}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Referral code error:", error);
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
// Same encodeBase62 used in the code route
|
||||
function encodeBase62(id: string): string {
|
||||
const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
let hash = 0;
|
||||
for (let i = 0; i < Math.min(id.length, 10); i++) {
|
||||
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
|
||||
}
|
||||
let code = "";
|
||||
while (code.length < 6) {
|
||||
code = chars[hash % 62] + code;
|
||||
hash = Math.floor(hash / 62);
|
||||
if (hash === 0 && code.length < 6) {
|
||||
hash = Math.floor(Math.random() * 62 ** (6 - code.length));
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
import { encodeReferralCode } from "@/lib/referral";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const jwtPayload = await requireAuth(req);
|
||||
@@ -42,14 +25,14 @@ export async function GET(req: NextRequest) {
|
||||
}),
|
||||
]);
|
||||
|
||||
const referralCode = encodeBase62(userId);
|
||||
const referralCode = encodeReferralCode(userId);
|
||||
|
||||
return NextResponse.json({
|
||||
totalReferrals,
|
||||
upgradedCount,
|
||||
totalEarned: earnings._sum.rewardFlh || 0,
|
||||
referralCode,
|
||||
shareLink: `${process.env.NEXT_PUBLIC_APP_URL || "https://falah.app"}/join?ref=${referralCode}`,
|
||||
shareLink: `${process.env.NEXT_PUBLIC_APP_URL || "https://falahos.my/mobile"}/auth?ref=${referralCode}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Referral stats error:", error);
|
||||
|
||||
+192
-21
@@ -4,39 +4,210 @@ import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
const existing = await prisma.user.findUnique({
|
||||
where: { email: "demo@falah.app" },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return NextResponse.json(
|
||||
{ message: "Demo user already exists", userId: existing.id },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
const demoPasswordHash = await bcrypt.hash("password123", 12);
|
||||
|
||||
const demoUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "demo@falah.app",
|
||||
// ── 1. Seed Demo Users ──────────────────────────────────────────
|
||||
const users = [
|
||||
{
|
||||
email: "demo@falahos.my",
|
||||
password: "password123",
|
||||
name: "Demo User",
|
||||
passwordHash: demoPasswordHash,
|
||||
flhBalance: 10000,
|
||||
isPremium: true,
|
||||
isPro: false,
|
||||
trialEndsAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
||||
preferredName: "Demo",
|
||||
coachingGoals: "Learn more about Islam and improve my daily prayers",
|
||||
},
|
||||
{
|
||||
email: "premium@falahos.my",
|
||||
password: "Premium123!",
|
||||
name: "Premium Demo",
|
||||
flhBalance: 50000,
|
||||
isPremium: true,
|
||||
isPro: false,
|
||||
preferredName: "Premium",
|
||||
coachingGoals: "Advanced Islamic studies and community leadership",
|
||||
},
|
||||
];
|
||||
|
||||
const userResults = [];
|
||||
|
||||
for (const user of users) {
|
||||
const { password, ...userData } = user;
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
||||
const upserted = await prisma.user.upsert({
|
||||
where: { email: user.email },
|
||||
update: { ...userData, passwordHash },
|
||||
create: { ...userData, passwordHash },
|
||||
});
|
||||
|
||||
userResults.push({ email: upserted.email, id: upserted.id });
|
||||
}
|
||||
|
||||
// Find demo user for sample data
|
||||
const demoUser = await prisma.user.findUnique({ where: { email: "demo@falahos.my" } });
|
||||
const premiumUser = await prisma.user.findUnique({ where: { email: "premium@falahos.my" } });
|
||||
const demoId = demoUser!.id;
|
||||
const premiumId = premiumUser!.id;
|
||||
|
||||
// ── 2. Seed Forum Categories ─────────────────────────────────────
|
||||
const categoriesData = [
|
||||
{ name: "General Discussion", description: "General Islamic discussions and community topics", icon: "💬", order: 1 },
|
||||
{ name: "Quran & Tafsir", description: "Quranic studies, recitation, and interpretation", icon: "📖", order: 2 },
|
||||
{ name: "Fiqh & Hadith", description: "Islamic jurisprudence and prophetic traditions", icon: "📜", order: 3 },
|
||||
{ name: "Family & Marriage", description: "Family life, marriage, and parenting in Islam", icon: "👨👩👧👦", order: 4 },
|
||||
{ name: "Halal Lifestyle", description: "Halal food, travel, fashion, and daily living", icon: "🌿", order: 5 },
|
||||
{ name: "Community Events", description: "Local events, study circles, and gatherings", icon: "📅", order: 6 },
|
||||
];
|
||||
|
||||
const categoriesCreated: string[] = [];
|
||||
for (const cat of categoriesData) {
|
||||
const created = await prisma.forumCategory.upsert({
|
||||
where: { name: cat.name },
|
||||
update: cat,
|
||||
create: cat,
|
||||
});
|
||||
categoriesCreated.push(created.name);
|
||||
}
|
||||
|
||||
// ── 3. Seed Sample Forum Threads ─────────────────────────────────
|
||||
const generalCat = await prisma.forumCategory.findUnique({ where: { name: "General Discussion" } });
|
||||
const quranCat = await prisma.forumCategory.findUnique({ where: { name: "Quran & Tafsir" } });
|
||||
|
||||
if (generalCat) {
|
||||
await prisma.forumThread.upsert({
|
||||
where: { id: "seed-thread-welcome" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-thread-welcome",
|
||||
title: "Welcome to Falah Community! 👋",
|
||||
content: "Assalamu alaikum everyone! This is our community space where we can discuss all things related to Islam, seek knowledge, and support each other. Feel free to introduce yourself!",
|
||||
categoryId: generalCat.id,
|
||||
authorId: premiumId,
|
||||
pinned: true,
|
||||
shariahStatus: "approved",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (quranCat) {
|
||||
await prisma.forumThread.upsert({
|
||||
where: { id: "seed-thread-quran" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-thread-quran",
|
||||
title: "Daily Quran Reflection — Surah Al-Fatiha",
|
||||
content: "Let's reflect on Surah Al-Fatiha together. What does 'Guide us to the straight path' mean to you in your daily life? Share your reflections below.",
|
||||
categoryId: quranCat.id,
|
||||
authorId: demoId,
|
||||
pinned: false,
|
||||
shariahStatus: "approved",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── 4. Seed Sample Group (Private Forum) ─────────────────────────
|
||||
const group = await prisma.group.upsert({
|
||||
where: { id: "seed-group-study-circle" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-group-study-circle",
|
||||
name: "Quran Study Circle",
|
||||
description: "A private group for dedicated Quran study and memorization. Members meet weekly to review progress.",
|
||||
ownerId: premiumId,
|
||||
inviteCode: "QURAN2026",
|
||||
members: {
|
||||
create: [
|
||||
{ userId: premiumId, role: "owner" },
|
||||
{ userId: demoId, role: "member" },
|
||||
],
|
||||
},
|
||||
},
|
||||
include: { _count: { select: { members: true, threads: true } } },
|
||||
});
|
||||
|
||||
// Create a thread in the group
|
||||
const groupCat = generalCat || quranCat;
|
||||
if (groupCat) {
|
||||
await prisma.forumThread.upsert({
|
||||
where: { id: "seed-thread-group-intro" },
|
||||
update: {},
|
||||
create: {
|
||||
id: "seed-thread-group-intro",
|
||||
title: "Welcome to Quran Study Circle 📖",
|
||||
content: "Assalamu alaikum group members! This is our private space for discussing our weekly Quran study sessions. Our first session will cover Surah Al-Kahf — please review the tafsir beforehand.",
|
||||
categoryId: groupCat.id,
|
||||
authorId: premiumId,
|
||||
groupId: group.id,
|
||||
pinned: true,
|
||||
shariahStatus: "approved",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── 5. Seed Sample Marketplace Listings ──────────────────────────
|
||||
const listingsData = [
|
||||
{
|
||||
id: "seed-listing-tafsir",
|
||||
title: "Tafsir Ibn Kathir (Abridged) — 10 Volumes",
|
||||
description: "Complete set of Tafsir Ibn Kathir in English. Excellent condition, hardly used. Perfect for anyone looking to deepen their understanding of the Quran.",
|
||||
category: "Books",
|
||||
priceFlh: 2500,
|
||||
sellerId: premiumId,
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
id: "seed-listing-prayer-mat",
|
||||
title: "Premium Turkish Prayer Mat — Green",
|
||||
description: "High-quality woven Turkish prayer mat with mosque design. Soft texture, perfect size. Ideal gift for loved ones.",
|
||||
category: "Essentials",
|
||||
priceFlh: 800,
|
||||
sellerId: demoId,
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
id: "seed-listing-miswak",
|
||||
title: "Natural Miswak Sticks (Pack of 10)",
|
||||
description: "Fresh, high-quality miswak sticks from the Arak tree. Naturally whitens teeth, freshens breath, and follows the Sunnah. Each pack contains 10 sticks.",
|
||||
category: "Wellness",
|
||||
priceFlh: 150,
|
||||
sellerId: demoId,
|
||||
featured: false,
|
||||
},
|
||||
{
|
||||
id: "seed-listing-hijab",
|
||||
title: "Handcrafted Silk Hijabs — Assorted Colors",
|
||||
description: "Beautiful handcrafted silk hijabs in assorted colors. Lightweight, breathable fabric. Perfect for daily wear or special occasions.",
|
||||
category: "Fashion",
|
||||
priceFlh: 350,
|
||||
sellerId: premiumId,
|
||||
featured: false,
|
||||
},
|
||||
];
|
||||
|
||||
const listingsCreated: string[] = [];
|
||||
for (const listing of listingsData) {
|
||||
await prisma.listing.upsert({
|
||||
where: { id: listing.id },
|
||||
update: { ...listing },
|
||||
create: listing,
|
||||
});
|
||||
listingsCreated.push(listing.title);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "Demo user created successfully",
|
||||
userId: demoUser.id,
|
||||
credentials: { email: "demo@falah.app", password: "password123" },
|
||||
message: "Seed completed successfully",
|
||||
users: userResults,
|
||||
categories: categoriesCreated,
|
||||
group: { name: group.name, members: group._count.members },
|
||||
listings: listingsCreated,
|
||||
credentials: [
|
||||
{ email: "demo@falahos.my", password: "password123" },
|
||||
{ email: "premium@falahos.my", password: "Premium123!" },
|
||||
],
|
||||
},
|
||||
{ status: 201 }
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Seed error:", error);
|
||||
|
||||
+31
-5
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { Suspense, useState, FormEvent, useEffect } from "react";
|
||||
import { useAuth } from "@/lib/AuthContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Mail, ArrowLeft } from "lucide-react";
|
||||
|
||||
type AuthMode = "login" | "register";
|
||||
@@ -41,9 +41,16 @@ function GitHubLogo({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthPage() {
|
||||
const { login, register, oauthLogin, loading: authLoading } = useAuth();
|
||||
function AuthPageInner() {
|
||||
const { user, login, register, oauthLogin, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const refCode = searchParams.get("ref");
|
||||
|
||||
// Redirect to main page if already logged in (e.g. after OAuth popup completes)
|
||||
useEffect(() => {
|
||||
if (user) router.push("/");
|
||||
}, [user, router]);
|
||||
|
||||
const [pageState, setPageState] = useState<PageState>("select");
|
||||
const [mode, setMode] = useState<AuthMode>("register");
|
||||
@@ -90,7 +97,7 @@ export default function AuthPage() {
|
||||
if (mode === "login") {
|
||||
await login(email.trim(), password);
|
||||
} else {
|
||||
await register(email.trim(), name.trim(), password);
|
||||
await register(email.trim(), name.trim(), password, refCode || undefined);
|
||||
}
|
||||
router.push("/");
|
||||
} catch (err: unknown) {
|
||||
@@ -339,6 +346,13 @@ export default function AuthPage() {
|
||||
<p className="text-xs text-gray-500">
|
||||
Get <span className="text-[#D4AF37] font-semibold">5,000 FLH</span>{" "}
|
||||
free on sign up + 7-day premium trial
|
||||
{refCode && (
|
||||
<>
|
||||
{" "}+{" "}
|
||||
<span className="text-emerald-400 font-semibold">1,000 FLH</span>{" "}
|
||||
referral bonus
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -356,3 +370,15 @@ export default function AuthPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<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>
|
||||
}>
|
||||
<AuthPageInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function ThreadDetailPage() {
|
||||
|
||||
const fetchThread = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/forum/threads`);
|
||||
const res = await fetch(`/mobile/api/forum/threads`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
const found = data.threads.find((t: ThreadDetail) => t.id === threadId);
|
||||
@@ -97,7 +97,7 @@ export default function ThreadDetailPage() {
|
||||
setLoadingData(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/forum/posts?threadId=${threadId}`);
|
||||
const res = await fetch(`/mobile/api/forum/posts?threadId=${threadId}`);
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setPosts(data.posts);
|
||||
@@ -113,7 +113,7 @@ export default function ThreadDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) {
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
return;
|
||||
}
|
||||
}, [loading, token, router]);
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function ForumPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) {
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
return;
|
||||
}
|
||||
}, [loading, token, router]);
|
||||
@@ -514,7 +514,7 @@ export default function ForumPage() {
|
||||
|
||||
{/* Create thread modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function GroupDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) {
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
return;
|
||||
}
|
||||
}, [loading, token, router]);
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function GroupsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) {
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
return;
|
||||
}
|
||||
}, [loading, token, router]);
|
||||
@@ -362,7 +362,7 @@ export default function GroupsPage() {
|
||||
|
||||
{/* Create group modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
@@ -447,7 +447,7 @@ export default function GroupsPage() {
|
||||
|
||||
{/* Join group modal */}
|
||||
{showJoinModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => {
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function HalalMonitorPage() {
|
||||
|
||||
// ── Redirect if not logged in ──────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/login");
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
// ── Fetch places ───────────────────────────────────────────────────────
|
||||
@@ -86,7 +86,7 @@ export default function HalalMonitorPage() {
|
||||
if (q) params.set("q", q);
|
||||
if (type && type !== "all") params.set("type", type);
|
||||
|
||||
const res = await fetch(`/api/halal/places?${params.toString()}`);
|
||||
const res = await fetch(`/mobile/api/halal/places?${params.toString()}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPlaces(data.places);
|
||||
@@ -192,7 +192,7 @@ export default function HalalMonitorPage() {
|
||||
if (!token) return;
|
||||
const existing = bookmarks.find((b) => b.itemId === place.id);
|
||||
if (existing) {
|
||||
const res = await fetch(`/api/halal/bookmarks?id=${existing.id}`, {
|
||||
const res = await fetch(`/mobile/api/halal/bookmarks?id=${existing.id}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
@@ -515,7 +515,7 @@ export default function HalalMonitorPage() {
|
||||
{/* ── Detail Modal ────────────────────────────────────────────────── */}
|
||||
{selectedPlace && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center"
|
||||
className="fixed inset-0 z-[60] bg-black/70 flex items-end sm:items-center justify-center"
|
||||
onClick={() => setSelectedPlace(null)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -63,7 +63,7 @@ function NurChat() {
|
||||
/* Redirect if not logged in */
|
||||
useEffect(() => {
|
||||
if (!loading && !token) {
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
}
|
||||
}, [loading, token, router]);
|
||||
|
||||
@@ -74,7 +74,7 @@ function NurChat() {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [historyRes, dailyRes] = await Promise.all([
|
||||
fetch(`/api/chat/history/${user.id}`, {
|
||||
fetch(`/mobile/api/chat/history/${user.id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}),
|
||||
fetch("/mobile/api/chat/daily", {
|
||||
@@ -406,7 +406,7 @@ function PersonaSelector({
|
||||
const canAccess = (personaId: string) => {
|
||||
if (personaId === "nurbuddy") return true;
|
||||
if (isPro) return true;
|
||||
if (isPremium && ["alim", "hakim", "murabbi"].includes(personaId)) return true;
|
||||
if (isPremium && ["alim", "hakim", "murabbi", "mufti", "daie"].includes(personaId)) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ export default function HomePage() {
|
||||
const [claimResult, setClaimResult] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/login");
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
// Refresh streak/XP data every 60 seconds
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Shield,
|
||||
TrendingUp,
|
||||
ExternalLink,
|
||||
Gift,
|
||||
} from "lucide-react";
|
||||
import PremiumBadge from "@/components/PremiumBadge";
|
||||
|
||||
@@ -36,7 +37,7 @@ export default function ProfilePage() {
|
||||
const [saveMessage, setSaveMessage] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/login");
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -101,7 +102,7 @@ export default function ProfilePage() {
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
router.push("/login");
|
||||
router.push("/auth");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -413,6 +414,18 @@ export default function ProfilePage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Refer & Earn */}
|
||||
<Link
|
||||
href="/refer"
|
||||
className="w-full flex items-center justify-between py-3.5 px-5 rounded-xl bg-[#D4AF37]/10 border border-[#D4AF37]/20 text-[#D4AF37] active:bg-[#D4AF37]/20 transition min-h-[48px]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift size={16} />
|
||||
<span className="text-sm font-medium">Refer & Earn FLH</span>
|
||||
</div>
|
||||
<ChevronRight size={16} />
|
||||
</Link>
|
||||
|
||||
{/* Logout */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAuth } from "@/lib/AuthContext";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Users,
|
||||
Gift,
|
||||
Share2,
|
||||
Copy,
|
||||
Check,
|
||||
ExternalLink,
|
||||
TrendingUp,
|
||||
Crown,
|
||||
} from "lucide-react";
|
||||
|
||||
interface ReferralStats {
|
||||
totalReferrals: number;
|
||||
upgradedCount: number;
|
||||
totalEarned: number;
|
||||
referralCode: string;
|
||||
shareLink: string;
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
type: "success" | "error";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export default function ReferPage() {
|
||||
const { user, token, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [stats, setStats] = useState<ReferralStats | null>(null);
|
||||
const [dataLoading, setDataLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toast, setToast] = useState<ToastState | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchStats();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const fetchStats = async () => {
|
||||
setDataLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch("/mobile/api/referrals/stats", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to load referral stats");
|
||||
}
|
||||
const data: ReferralStats = await res.json();
|
||||
setStats(data);
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Something went wrong";
|
||||
setError(message);
|
||||
} finally {
|
||||
setDataLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getShareLink = (): string => {
|
||||
const code = stats?.referralCode || "";
|
||||
const base =
|
||||
(typeof window !== "undefined" &&
|
||||
(window as any).NEXT_PUBLIC_APP_URL) ||
|
||||
process.env.NEXT_PUBLIC_APP_URL ||
|
||||
"https://falahos.my/mobile";
|
||||
return `${base}/auth?ref=${code}`;
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
const link = getShareLink();
|
||||
const shareData = {
|
||||
title: "Join me on Falah",
|
||||
text: "Assalamu alaikum! Join me on Falah — the Islamic lifestyle app for daily AI coaching, Quran, and community. Use my referral link to get started!",
|
||||
url: link,
|
||||
};
|
||||
|
||||
if (typeof navigator !== "undefined" && navigator.share) {
|
||||
try {
|
||||
await navigator.share(shareData);
|
||||
showToast("success", "Shared successfully!");
|
||||
} catch {
|
||||
// User cancelled or share failed — don't show error
|
||||
}
|
||||
} else {
|
||||
// Fallback: copy link instead
|
||||
handleCopyLink();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
const link = getShareLink();
|
||||
try {
|
||||
await navigator.clipboard.writeText(link);
|
||||
setCopied(true);
|
||||
showToast("success", "Referral link copied!");
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
showToast("error", "Failed to copy link");
|
||||
}
|
||||
};
|
||||
|
||||
const showToast = (type: "success" | "error", text: string) => {
|
||||
setToast({ type, text });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
};
|
||||
|
||||
// Loading state
|
||||
if (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>
|
||||
);
|
||||
}
|
||||
|
||||
// Not authenticated
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-[#0a0a0f] pb-24">
|
||||
{/* Header */}
|
||||
<div className="px-4 pt-6 pb-2">
|
||||
<h1 className="text-xl font-bold text-white">Refer & Earn</h1>
|
||||
</div>
|
||||
|
||||
<div className="px-4 space-y-5 animate-fade-in">
|
||||
{/* Toast Notification */}
|
||||
{toast && (
|
||||
<div
|
||||
className={`flex items-center gap-2 p-3 rounded-xl text-sm ${
|
||||
toast.type === "success"
|
||||
? "bg-emerald-900/20 border border-emerald-800/40 text-emerald-400"
|
||||
: "bg-red-900/20 border border-red-800/40 text-red-400"
|
||||
}`}
|
||||
>
|
||||
{toast.type === "success" ? (
|
||||
<Check size={16} />
|
||||
) : (
|
||||
<ExternalLink size={16} />
|
||||
)}
|
||||
{toast.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && !dataLoading && (
|
||||
<div className="bg-red-900/20 border border-red-800/40 rounded-2xl p-5 text-center">
|
||||
<p className="text-sm text-red-400 mb-3">{error}</p>
|
||||
<button
|
||||
onClick={fetchStats}
|
||||
className="text-sm text-[#D4AF37] underline underline-offset-2"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Data Loading */}
|
||||
{dataLoading && !error && (
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-10 text-center">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-[#D4AF37]/30 border-t-[#D4AF37] animate-spin mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500">Loading referral stats...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Referral Code Card */}
|
||||
{stats && !dataLoading && (
|
||||
<div className="bg-gradient-to-br from-[#D4AF37]/20 to-[#0a0a0f] border border-[#D4AF37]/30 rounded-2xl p-6 text-center">
|
||||
<div className="w-14 h-14 rounded-full bg-[#D4AF37]/20 flex items-center justify-center mx-auto mb-3">
|
||||
<Gift size={24} className="text-[#D4AF37]" />
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">
|
||||
Your Referral Code
|
||||
</p>
|
||||
<p className="text-3xl font-mono font-bold tracking-widest text-[#D4AF37] select-all">
|
||||
{stats.referralCode}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Share this code or link to earn rewards
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
{stats && !dataLoading && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Share Button */}
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5 text-center active:scale-95 transition hover:border-[#D4AF37]/30 min-h-[80px]"
|
||||
>
|
||||
<Share2 size={20} className="mx-auto text-[#D4AF37] mb-2" />
|
||||
<p className="text-sm font-medium text-white">Share</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Invite friends</p>
|
||||
</button>
|
||||
|
||||
{/* Copy Link Button */}
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5 text-center active:scale-95 transition hover:border-[#D4AF37]/30 min-h-[80px]"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={20} className="mx-auto text-emerald-400 mb-2" />
|
||||
) : (
|
||||
<Copy size={20} className="mx-auto text-[#D4AF37] mb-2" />
|
||||
)}
|
||||
<p className="text-sm font-medium text-white">
|
||||
{copied ? "Copied!" : "Copy Link"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
{copied ? "Link copied to clipboard" : "Share your link"}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && !dataLoading && (
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<TrendingUp size={16} className="text-[#D4AF37]" />
|
||||
Your Referral Stats
|
||||
</h2>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{/* Total Referrals */}
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 text-center">
|
||||
<Users size={18} className="mx-auto text-[#D4AF37] mb-2" />
|
||||
<p className="text-xl font-bold text-white">
|
||||
{stats.totalReferrals}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Referrals</p>
|
||||
</div>
|
||||
|
||||
{/* FLH Earned */}
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 text-center">
|
||||
<Gift size={18} className="mx-auto text-[#D4AF37] mb-2" />
|
||||
<p className="text-xl font-bold text-[#D4AF37]">
|
||||
{stats.totalEarned.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">FLH Earned</p>
|
||||
</div>
|
||||
|
||||
{/* Upgraded Count */}
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 text-center">
|
||||
<Crown size={18} className="mx-auto text-[#D4AF37] mb-2" />
|
||||
<p className="text-xl font-bold text-white">
|
||||
{stats.upgradedCount}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">Upgraded</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* How It Works */}
|
||||
{stats && !dataLoading && (
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
|
||||
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<Gift size={16} className="text-[#D4AF37]" />
|
||||
How It Works
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start gap-3 text-sm text-gray-400">
|
||||
<span className="w-5 h-5 rounded-full bg-[#D4AF37]/20 text-[#D4AF37] text-xs font-bold flex items-center justify-center shrink-0 mt-0.5">
|
||||
1
|
||||
</span>
|
||||
<span>
|
||||
Share your referral code or link with friends and family
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3 text-sm text-gray-400">
|
||||
<span className="w-5 h-5 rounded-full bg-[#D4AF37]/20 text-[#D4AF37] text-xs font-bold flex items-center justify-center shrink-0 mt-0.5">
|
||||
2
|
||||
</span>
|
||||
<span>
|
||||
They sign up using your link — you earn{" "}
|
||||
<span className="text-[#D4AF37] font-medium">200 FLH</span>{" "}
|
||||
per referral
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3 text-sm text-gray-400">
|
||||
<span className="w-5 h-5 rounded-full bg-[#D4AF37]/20 text-[#D4AF37] text-xs font-bold flex items-center justify-center shrink-0 mt-0.5">
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
When they upgrade to Premium, you earn a{" "}
|
||||
<span className="text-[#D4AF37] font-medium">bonus</span> on
|
||||
top!
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Share Link Card (visible at bottom) */}
|
||||
{stats && !dataLoading && (
|
||||
<div className="bg-[#111118] border border-gray-800/60 rounded-2xl p-5">
|
||||
<h3 className="text-sm font-semibold text-white mb-3 flex items-center gap-2">
|
||||
<ExternalLink size={16} className="text-[#D4AF37]" />
|
||||
Your Share Link
|
||||
</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 bg-[#1a1a24] border border-gray-800/60 rounded-xl px-4 py-3 min-h-[48px] flex items-center">
|
||||
<code className="text-sm text-gray-400 truncate select-all">
|
||||
{getShareLink()}
|
||||
</code>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="w-12 h-12 rounded-xl bg-[#D4AF37]/10 border border-[#D4AF37]/30 flex items-center justify-center active:scale-95 transition hover:bg-[#D4AF37]/20 shrink-0 min-h-[48px] min-w-[48px]"
|
||||
>
|
||||
{copied ? (
|
||||
<Check size={18} className="text-emerald-400" />
|
||||
) : (
|
||||
<Copy size={18} className="text-[#D4AF37]" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -377,7 +377,7 @@ export default function SouqPage() {
|
||||
|
||||
{/* Create listing modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
@@ -513,7 +513,7 @@ export default function SouqPage() {
|
||||
|
||||
{/* Purchase confirmation dialog */}
|
||||
{showConfirmDialog && (
|
||||
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
||||
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => {
|
||||
|
||||
@@ -92,7 +92,7 @@ function UpgradeContent() {
|
||||
const isPro = user?.isPro;
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/login");
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function WalletPage() {
|
||||
const [topupLoading, setTopupLoading] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !token) router.push("/login");
|
||||
if (!loading && !token) router.push("/auth");
|
||||
}, [loading, token, router]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user