Add Prayer Reminder, Dhikr Counter, Qibla Finder, Halal Monitor updates

- Prayer reminder: /prayer page with 5 daily times, countdown, alAdhan API
- Dhikr counter: /dhikr page with 3 presets, custom counter, haptics, DB tracking
- Qibla finder: /qibla page with compass SVG, DeviceOrientation, bearing to Kaaba
- Halal Monitor: Leaflet + Google Places, 26 KL markers, geolocation, distance sort
- BottomNav: added clock icon for prayer
- Prisma: schema updates for DhikrSession, DhikrDay, DailyStreak, XpTransaction
- Geo utility module
- Weighted Traefik routing: Contabo 99% / Synology 1% cold standby
This commit is contained in:
root
2026-06-18 14:51:07 +02:00
parent ed34b186f9
commit 28be776c84
13 changed files with 2822 additions and 104 deletions
+92
View File
@@ -0,0 +1,92 @@
import { requireAuth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";
// ── Helpers ────────────────────────────────────────────────────────────────
function todayStr(): string {
const d = new Date();
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
}
function getDhikrType(raw: string): string {
const allowed = ["subhanallah", "alhamdulillah", "allahuakbar", "custom"];
return allowed.includes(raw) ? raw : "custom";
}
// ── GET /api/dhikr/stats — Today's dhikr stats ────────────────────────────
export async function GET(request: Request) {
const jwt = await requireAuth(request);
if (!jwt) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const today = todayStr();
let dayStats = await prisma.dhikrDay.findUnique({
where: { userId_date: { userId: jwt.userId, date: today } },
});
// Recent sessions (last 20)
const recentSessions = await prisma.dhikrSession.findMany({
where: { userId: jwt.userId },
orderBy: { createdAt: "desc" },
take: 20,
select: { id: true, type: true, count: true, createdAt: true },
});
if (!dayStats) {
dayStats = await prisma.dhikrDay.create({
data: { userId: jwt.userId, date: today },
});
}
return NextResponse.json({ dayStats, recentSessions });
}
// ── POST /api/dhikr/stats — Log a dhikr session ────────────────────────────
export async function POST(request: Request) {
const jwt = await requireAuth(request);
if (!jwt) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: { type?: string; count?: number; target?: number; completed?: boolean };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const type = getDhikrType(body.type || "custom");
const count = Math.min(Math.max(body.count || 1, 1), 9999);
const target = body.target || 33;
const completed = body.completed !== false;
const today = todayStr();
// Create session record
const session = await prisma.dhikrSession.create({
data: { userId: jwt.userId, type, count, target, completed },
});
// Upsert today's aggregate
const dayStats = await prisma.dhikrDay.upsert({
where: { userId_date: { userId: jwt.userId, date: today } },
create: {
userId: jwt.userId,
date: today,
[type]: count,
totalCount: count,
sessionCount: 1,
},
update: {
totalCount: { increment: count },
sessionCount: { increment: 1 },
[type]: { increment: count },
},
});
return NextResponse.json({ session, dayStats });
}