Initial Falah Mobile rebuild — all 7 blocks complete

- Auth system (login/register/profile)
- Nur AI chat with persona system
- Souq marketplace with FLH economy
- Forum community with Shariah moderation
- Wallet & FLH top-up
- Premium/Pro tier upgrade with Polar.sh
- Halal Monitor with map & bookmarks
- Home dashboard with daily verse & streaks
- Health endpoints

Next.js 16.2.7, Prisma v5 SQLite, JWT auth, Tailwind CSS v4
This commit is contained in:
root
2026-06-15 09:28:22 +02:00
parent ab8a2053e1
commit 5483dd291e
56 changed files with 7619 additions and 100 deletions
+53
View File
@@ -0,0 +1,53 @@
import { requireAuth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getUserTier, DAILY_LIMITS } from "@/lib/tiers";
export const runtime = "nodejs";
export async function GET(request: Request) {
try {
const user = await requireAuth(request);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const dbUser = await prisma.user.findUnique({
where: { id: user.userId },
select: {
dailyMsgCount: true,
dailyMsgResetAt: true,
isPremium: true,
isPro: true,
},
});
if (!dbUser) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const tier = getUserTier(dbUser.isPremium, dbUser.isPro);
const limits = DAILY_LIMITS[tier];
const now = new Date();
let count = dbUser.dailyMsgCount;
let resetAt = dbUser.dailyMsgResetAt;
// Reset if new day
if (!resetAt || now.getTime() - resetAt.getTime() > 24 * 60 * 60 * 1000) {
count = 0;
resetAt = now;
}
return Response.json({
count,
limit: limits.msgs === Infinity ? -1 : limits.msgs,
resetAt: resetAt?.toISOString() || now.toISOString(),
tier,
remaining:
limits.msgs === Infinity ? -1 : Math.max(0, limits.msgs - count),
});
} catch (error) {
console.error("Daily chat error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
+44
View File
@@ -0,0 +1,44 @@
export const runtime = "nodejs";
const DEMO_CONVERSATION: { role: string; content: string; personaId: string }[] = [
{
role: "user",
content: "Assalamualaikum! I've been feeling really stressed about work lately.",
personaId: "nurbuddy",
},
{
role: "assistant",
content:
"Waalaikumsalam warahmatullahi wabarakatuh! 😊 I'm sorry to hear you're feeling stressed. Remember what Allah says: **\"Verily, with hardship comes ease\"** (Quran 94:6). \n\nHere are a few things that might help:\n• Take a deep breath and recite *SubhanAllah* 33 times\n• Make wudu — it's surprisingly calming!\n• Step outside and appreciate Allah's creation for a few minutes\n\nYou're not alone, and this too shall pass, insha'Allah. 💛",
personaId: "nurbuddy",
},
{
role: "user",
content: "That helps, thank you. Can you tell me more about patience in Islam?",
personaId: "hakim",
},
{
role: "assistant",
content:
"Ah, sabr (patience) — one of the most beautiful virtues in Islam. Let me share a story:\n\nProphet Ayyub (Job, peace be upon him) lost everything — wealth, children, and health. Yet he never stopped praising Allah. When asked how, he said: **\"Allah gave, and Allah has taken. Blessed be Allah in both.\"**\n\nAllah rewarded him tremendously: **\"And We gave him back his family and the like thereof with them — a mercy from Us and a reminder for those who worship\"** (Quran 38:43).\n\nYour patience through work stress is a form of worship too. Every moment you endure with grace is recorded. 🌱",
personaId: "hakim",
},
{
role: "user",
content: "SubhanAllah, that's really powerful. JazakAllah khair!",
personaId: "nurbuddy",
},
{
role: "assistant",
content:
"Wa iyyak! 😊 So glad it resonated with you. Remember, the Prophet ﷺ said: **\"He who does not thank people, does not thank Allah.\"** \n\nMay Allah grant you ease in your work and bless your patience. Come back anytime you need a friend to talk to! 🌟",
personaId: "nurbuddy",
},
];
export async function GET() {
return Response.json({
conversation: DEMO_CONVERSATION,
personas: ["nurbuddy", "hakim"],
});
}
@@ -0,0 +1,45 @@
import { requireAuth, type JWTContents } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export const runtime = "nodejs";
export async function GET(
request: Request,
{ params }: { params: Promise<{ userId: string }> }
) {
try {
const user = await requireAuth(request);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { userId } = await params;
// Verify the requested userId matches the authenticated user
if (user.userId !== userId) {
return Response.json({ error: "Forbidden" }, { status: 403 });
}
const messages = await prisma.chatHistory.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
take: 50,
});
// Return in chronological order
const ordered = messages.reverse();
return Response.json({
messages: ordered.map((m) => ({
id: m.id,
role: m.role,
content: m.content,
metadata: m.metadata ? JSON.parse(m.metadata) : null,
createdAt: m.createdAt.toISOString(),
})),
});
} catch (error) {
console.error("Chat history error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
+158
View File
@@ -0,0 +1,158 @@
import { requireAuth, type JWTContents } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getPersona } from "@/lib/personas";
import { getUserTier, DAILY_LIMITS } from "@/lib/tiers";
import { generateAIReply } from "@/lib/ai-server";
export const runtime = "nodejs";
export async function POST(request: Request) {
try {
const user = await requireAuth(request);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json();
const { messages, personaId } = body as {
messages?: { role: string; content: string }[];
personaId?: string;
};
if (!messages || !Array.isArray(messages) || messages.length === 0) {
return Response.json({ error: "messages array is required" }, { status: 400 });
}
if (!personaId || typeof personaId !== "string") {
return Response.json({ error: "personaId is required" }, { status: 400 });
}
// Verify persona exists
const persona = getPersona(personaId);
if (!persona) {
return Response.json({ error: "Invalid persona" }, { status: 400 });
}
// Get user from DB to check limits
const dbUser = await prisma.user.findUnique({
where: { id: user.userId },
});
if (!dbUser) {
return Response.json({ error: "User not found" }, { status: 404 });
}
const tier = getUserTier(dbUser.isPremium, dbUser.isPro);
const limits = DAILY_LIMITS[tier];
// Check persona access for this tier
if (!limits.personas.includes(personaId)) {
const tierName = tier.charAt(0).toUpperCase() + tier.slice(1);
return Response.json(
{
error: `This persona requires ${tier === "free" ? "Premium" : "Pro"} subscription`,
tier,
requiredTier: tier === "free" ? "premium" : "pro",
},
{ status: 403 }
);
}
// Check daily message limit
const now = new Date();
let dailyMsgCount = dbUser.dailyMsgCount;
let dailyMsgResetAt = dbUser.dailyMsgResetAt;
// Reset count if a new day
if (!dailyMsgResetAt || now.getTime() - dailyMsgResetAt.getTime() > 24 * 60 * 60 * 1000) {
dailyMsgCount = 0;
dailyMsgResetAt = now;
}
if (limits.msgs !== Infinity && dailyMsgCount >= limits.msgs) {
return Response.json(
{
error: "Daily message limit reached",
count: dailyMsgCount,
limit: limits.msgs,
resetAt: dailyMsgResetAt.toISOString(),
},
{ status: 429 }
);
}
// Determine the user's last message for context
const lastUserMessage = messages[messages.length - 1]?.content || "";
// Get recent history from DB for context (up to 10 recent messages)
const recentHistory = await prisma.chatHistory.findMany({
where: { userId: user.userId },
orderBy: { createdAt: "desc" },
take: 10,
});
const historyContext = recentHistory
.reverse()
.map((h) => ({ role: h.role, content: h.content }));
// Save user message(s) to ChatHistory
for (const msg of messages) {
if (msg.role === "user") {
await prisma.chatHistory.create({
data: {
userId: user.userId,
role: "user",
content: msg.content,
metadata: JSON.stringify({ personaId }),
},
});
}
}
// Generate AI response
const aiContent = generateAIReply(personaId, lastUserMessage, historyContext);
// Save AI response to ChatHistory
const aiMessage = await prisma.chatHistory.create({
data: {
userId: user.userId,
role: "assistant",
content: aiContent,
metadata: JSON.stringify({ personaId }),
},
});
// Update daily message count
const newCount = dailyMsgCount + messages.filter((m) => m.role === "user").length;
await prisma.user.update({
where: { id: user.userId },
data: {
dailyMsgCount: newCount,
dailyMsgResetAt: dailyMsgResetAt,
},
});
let updatedUser: any = { dailyMsgCount: newCount };
if (newCount >= limits.msgs - 2 && limits.msgs !== Infinity) {
updatedUser.limitWarning = true;
updatedUser.limitRemaining = limits.msgs - newCount;
}
return Response.json({
message: {
role: "assistant",
content: aiContent,
createdAt: aiMessage.createdAt.toISOString(),
id: aiMessage.id,
},
daily: {
count: newCount,
limit: limits.msgs,
resetAt: dailyMsgResetAt.toISOString(),
},
user: updatedUser,
});
} catch (error) {
console.error("Chat send error:", error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}