diff --git a/src/app/api/feedback/route.ts b/src/app/api/feedback/route.ts
new file mode 100644
index 0000000..1c7f564
--- /dev/null
+++ b/src/app/api/feedback/route.ts
@@ -0,0 +1,106 @@
+import { NextRequest, NextResponse } from "next/server";
+import { mkdir, writeFile, readFile } from "node:fs/promises";
+import { existsSync } from "node:fs";
+import { join } from "node:path";
+
+const DATA_DIR = "/app/data";
+const FEEDBACK_FILE = join(DATA_DIR, "feedback.jsonl");
+
+interface FeedbackEntry {
+ id: string;
+ timestamp: string;
+ url: string;
+ error: string;
+ message: string;
+ userAgent: string;
+}
+
+function generateId(): string {
+ const ts = Date.now().toString(36);
+ const rand = Math.random().toString(36).substring(2, 8);
+ return `fb-${ts}-${rand}`;
+}
+
+function sanitizeUA(ua: string): string {
+ // Strip personal info from UA string
+ return ua.replace(/\(.*?\)/g, "(redacted)").substring(0, 200);
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { url, error, message, userAgent } = body;
+
+ if (!url || !error) {
+ return NextResponse.json(
+ { error: "url and error are required" },
+ { status: 400 }
+ );
+ }
+
+ const entry: FeedbackEntry = {
+ id: generateId(),
+ timestamp: new Date().toISOString(),
+ url: url.substring(0, 500),
+ error: error.substring(0, 1000),
+ message: (message || "").substring(0, 2000),
+ userAgent: sanitizeUA(userAgent || ""),
+ };
+
+ // Ensure data dir exists
+ await mkdir(DATA_DIR, { recursive: true });
+
+ // Append to JSONL file
+ await writeFile(FEEDBACK_FILE, JSON.stringify(entry) + "\n", {
+ flag: "a",
+ });
+
+ return NextResponse.json({ id: entry.id });
+ } catch (err) {
+ // Silent failure — don't throw errors from error-reporting endpoint
+ console.error("Feedback save error:", err);
+ return NextResponse.json({ id: null }, { status: 200 });
+ }
+}
+
+export async function GET(request: NextRequest) {
+ // Admin-only endpoint — return paginated feedback
+ const authHeader = request.headers.get("authorization") || "";
+ const adminToken = process.env.FEEDBACK_ADMIN_TOKEN || "flh-feedback-admin";
+
+ // Simple token auth
+ if (authHeader !== `Bearer ${adminToken}`) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { searchParams } = new URL(request.url);
+ const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 200);
+ const offset = parseInt(searchParams.get("offset") || "0");
+
+ try {
+ if (!existsSync(FEEDBACK_FILE)) {
+ return NextResponse.json({ entries: [], total: 0 });
+ }
+
+ const raw = await readFile(FEEDBACK_FILE, "utf-8");
+ const lines = raw.trim().split("\n").filter(Boolean);
+ const entries = lines
+ .map((line) => {
+ try {
+ return JSON.parse(line) as FeedbackEntry;
+ } catch {
+ return null;
+ }
+ })
+ .filter(Boolean)
+ .reverse(); // newest first
+
+ const total = entries.length;
+ const page = entries.slice(offset, offset + limit);
+
+ return NextResponse.json({ entries: page, total });
+ } catch (err) {
+ console.error("Feedback read error:", err);
+ return NextResponse.json({ entries: [], total: 0 });
+ }
+}
diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx
index 4fde3ca..b3e8526 100644
--- a/src/app/auth/page.tsx
+++ b/src/app/auth/page.tsx
@@ -4,6 +4,7 @@ import { Suspense, useState, FormEvent, useEffect } from "react";
import { useAuth } from "@/lib/AuthContext";
import { useRouter, useSearchParams } from "next/navigation";
import { Mail, ArrowLeft } from "lucide-react";
+import ErrorFeedback from "@/components/ErrorFeedback";
type AuthMode = "login" | "register";
type PageState = "select" | "email-form";
@@ -222,9 +223,7 @@ function AuthPageInner() {
{/* Error */}
{error && (
-
- {error}
-
+ setError("")} context="authentication" />
)}
{/* Form */}
diff --git a/src/app/forum/[threadId]/page.tsx b/src/app/forum/[threadId]/page.tsx
index f0aa101..2fbd943 100644
--- a/src/app/forum/[threadId]/page.tsx
+++ b/src/app/forum/[threadId]/page.tsx
@@ -8,7 +8,6 @@ import {
Crown,
Star,
Clock,
- AlertTriangle,
CheckCircle,
Loader2,
Lock,
@@ -16,6 +15,7 @@ import {
ShieldCheck,
Flag,
} from "lucide-react";
+import ErrorFeedback from "@/components/ErrorFeedback";
interface AuthorInfo {
id: string;
@@ -206,16 +206,7 @@ export default function ThreadDetailPage() {
Loading thread...
) : error ? (
-
-
-
{error}
-
-
+ { fetchThread(); fetchPosts(); }} context="thread posts" />
) : !thread ? (
Thread not found
@@ -384,9 +375,7 @@ export default function ThreadDetailPage() {
{replyError && (
-
+ setReplyError(null)} context="reply" />
)}
)}
diff --git a/src/app/forum/page.tsx b/src/app/forum/page.tsx
index 19ccdff..73b3da6 100644
--- a/src/app/forum/page.tsx
+++ b/src/app/forum/page.tsx
@@ -21,6 +21,7 @@ import {
FileText,
} from "lucide-react";
import PremiumBadge from "@/components/PremiumBadge";
+import ErrorFeedback from "@/components/ErrorFeedback";
interface Category {
id: string;
@@ -375,16 +376,7 @@ export default function ForumPage() {
Loading threads...
) : error ? (
-
-
-
{error}
-
-
+
) : threads.length === 0 ? (
@@ -624,9 +616,7 @@ export default function ForumPage() {
{createError && (
-
+
setCreateError(null)} context="create thread" />
)}
diff --git a/src/app/groups/[id]/page.tsx b/src/app/groups/[id]/page.tsx
index bd6769d..33c71a4 100644
--- a/src/app/groups/[id]/page.tsx
+++ b/src/app/groups/[id]/page.tsx
@@ -13,10 +13,10 @@ import {
Crown,
Shield,
Loader2,
- AlertTriangle,
Clock,
UserMinus,
} from "lucide-react";
+import ErrorFeedback from "@/components/ErrorFeedback";
interface GroupMember {
id: string;
@@ -231,16 +231,7 @@ export default function GroupDetailPage() {
Loading group...
) : error ? (
-
-
-
{error}
-
-
+
) : !group ? (
Group not found
diff --git a/src/app/groups/page.tsx b/src/app/groups/page.tsx
index f3d4a49..830af7b 100644
--- a/src/app/groups/page.tsx
+++ b/src/app/groups/page.tsx
@@ -10,9 +10,9 @@ import {
X,
Crown,
Loader2,
- AlertTriangle,
ChevronRight,
} from "lucide-react";
+import ErrorFeedback from "@/components/ErrorFeedback";
interface GroupMember {
id: string;
@@ -274,16 +274,7 @@ export default function GroupsPage() {
Loading groups...
) : error ? (
-
-
-
{error}
-
-
+
) : groups.length === 0 ? (
@@ -412,9 +403,7 @@ export default function GroupsPage() {
{createError && (
-
+
setCreateError(null)} context="create group" />
)}
@@ -490,9 +479,7 @@ export default function GroupsPage() {
{joinError && (
-
+ setJoinError(null)} context="join group" />
)}
{joinSuccess && (
diff --git a/src/app/nur/page.tsx b/src/app/nur/page.tsx
index f0378ce..f7b69cb 100644
--- a/src/app/nur/page.tsx
+++ b/src/app/nur/page.tsx
@@ -2,6 +2,7 @@
import { useState, useEffect, useRef, Suspense, useCallback } from "react";
import { useAuth } from "@/lib/AuthContext";
+import ErrorFeedback from "@/components/ErrorFeedback";
import { PERSONAS, getPersona } from "@/lib/personas";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -339,16 +340,18 @@ function NurChat() {
{/* ── Error Message ── */}
{error && (
-
-
-
{error}
-
-
+ setError(null)}
+ context="Nur AI chat"
+ />
)}
{/* ── Input Area ── */}
diff --git a/src/app/prayer/page.tsx b/src/app/prayer/page.tsx
index ca0f06f..1011ba1 100644
--- a/src/app/prayer/page.tsx
+++ b/src/app/prayer/page.tsx
@@ -20,6 +20,7 @@ import {
Check,
Navigation,
} from "lucide-react";
+import ErrorFeedback from "@/components/ErrorFeedback";
// ── Types ──
interface PrayerTimings {
@@ -283,12 +284,11 @@ export default function PrayerPage() {
const text = await res.text().catch(() => "Unknown error");
throw new Error(`API error ${res.status}: ${text}`);
}
- const json: PrayerApiResponse = await res.json();
- if (json.code === 200 && json.data) {
- setTimings(json.data.timings);
- const hijri = json.data.date?.hijri;
- if (hijri) {
- setHijriDate(`${hijri.date} ${hijri.month?.en || ""}`);
+ const json = await res.json();
+ if (json.timings) {
+ setTimings(json.timings as PrayerTimings);
+ if (json.hijri) {
+ setHijriDate(json.hijri);
setShowHijri(true);
}
} else {
@@ -421,17 +421,12 @@ export default function PrayerPage() {
{/* ── Error State ── */}
{!loading && error && (
-
-
-
{error}
-
-
-
+
)}
{/* ── Content ── */}
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx
index 253d38a..295e15e 100644
--- a/src/app/profile/page.tsx
+++ b/src/app/profile/page.tsx
@@ -20,6 +20,7 @@ import {
Gift,
} from "lucide-react";
import PremiumBadge from "@/components/PremiumBadge";
+import ErrorFeedback from "@/components/ErrorFeedback";
const VALID_MADHABS = ["Hanafi", "Maliki", "Shafi'i", "Hanbali"];
@@ -232,17 +233,14 @@ export default function ProfilePage() {
Edit Profile
- {saveMessage && (
-
+ {saveMessage === "Profile updated successfully" && (
+
{saveMessage}
)}
+ {saveMessage && saveMessage !== "Profile updated successfully" && (
+
setSaveMessage("")} context="profile" />
+ )}