Block 8 complete: streaks, notifications, referrals, premium identity, gamification

This commit is contained in:
root
2026-06-15 10:32:10 +02:00
parent 6423430275
commit 5e73d1d7ad
24 changed files with 1565 additions and 38 deletions
+110
View File
@@ -0,0 +1,110 @@
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;
}
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { code } = body;
if (!code || typeof code !== "string") {
return NextResponse.json(
{ error: "Referral code is required" },
{ status: 400 }
);
}
// 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.
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;
}
}
if (!referrerId) {
return NextResponse.json(
{ error: "Invalid referral code" },
{ status: 400 }
);
}
// Save the referral code in a cookie/localStorage so when user registers,
// the newly created user can be linked. For now, we just create a placeholder
// "pending" referral. The actual linking happens when the user registers
// and passes the referral code again (to be paired via the referred user's ID).
// 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.
const referral = await prisma.referral.create({
data: {
referrerId,
referredId: `pending_${tempToken}`, // placeholder, will be updated on registration
status: "pending",
rewardFlh: 0,
},
});
return NextResponse.json({
success: true,
message: "Referral code applied! You'll get 1000 FLH when you sign up.",
referralId: referral.id,
tempToken,
});
} catch (error) {
console.error("Referral claim error:", error);
return NextResponse.json(
{ error: "Failed to claim referral" },
{ status: 500 }
);
}
}
+44
View File
@@ -0,0 +1,44 @@
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;
}
export async function GET(req: NextRequest) {
const jwtPayload = await requireAuth(req);
if (!jwtPayload) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const referralCode = encodeBase62(jwtPayload.userId);
return NextResponse.json({
referralCode,
shareLink: `${process.env.NEXT_PUBLIC_APP_URL || "https://falah.app"}/join?ref=${referralCode}`,
});
} catch (error) {
console.error("Referral code error:", error);
return NextResponse.json(
{ error: "Failed to generate referral code" },
{ status: 500 }
);
}
}
+61
View File
@@ -0,0 +1,61 @@
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;
}
export async function GET(req: NextRequest) {
const jwtPayload = await requireAuth(req);
if (!jwtPayload) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const userId = jwtPayload.userId;
const [totalReferrals, upgradedCount, earnings] = await Promise.all([
prisma.referral.count({
where: { referrerId: userId },
}),
prisma.referral.count({
where: { referrerId: userId, status: "upgraded" },
}),
prisma.referral.aggregate({
where: { referrerId: userId },
_sum: { rewardFlh: true },
}),
]);
const referralCode = encodeBase62(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}`,
});
} catch (error) {
console.error("Referral stats error:", error);
return NextResponse.json(
{ error: "Failed to get referral stats" },
{ status: 500 }
);
}
}