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 && ( -
-

{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 && ( -
-

{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 && ( -
-

{createError}

-
+ setCreateError(null)} context="create group" /> )}
@@ -490,9 +479,7 @@ export default function GroupsPage() {
{joinError && ( -
-

{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" /> + )}
{/* Name */} diff --git a/src/app/qibla/page.tsx b/src/app/qibla/page.tsx index 9e9267d..9bd161e 100644 --- a/src/app/qibla/page.tsx +++ b/src/app/qibla/page.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useAuth } from "@/lib/AuthContext"; import { useRouter } from "next/navigation"; +import ErrorFeedback from "@/components/ErrorFeedback"; import { Compass, MapPin, Navigation, LocateFixed, Locate, Loader2, AlertTriangle, Info, RefreshCw, @@ -365,14 +366,12 @@ export default function QiblaPage() { {/* Error state */} {!geolocating && positionError && !position && ( -
- -

{positionError}

- -
+ )} {/* Bearing info */} diff --git a/src/app/refer/page.tsx b/src/app/refer/page.tsx index 71ad9a1..7b80254 100644 --- a/src/app/refer/page.tsx +++ b/src/app/refer/page.tsx @@ -13,6 +13,7 @@ import { TrendingUp, Crown, } from "lucide-react"; +import ErrorFeedback from "@/components/ErrorFeedback"; interface ReferralStats { totalReferrals: number; @@ -138,34 +139,19 @@ export default function ReferPage() {
{/* Toast Notification */} - {toast && ( -
- {toast.type === "success" ? ( - - ) : ( - - )} + {toast && toast.type === "success" && ( +
+ {toast.text}
)} + {toast && toast.type === "error" && ( + setToast(null)} context="referral" /> + )} {/* Error State */} {error && !dataLoading && ( -
-

{error}

- -
+ )} {/* Data Loading */} diff --git a/src/app/souq/page.tsx b/src/app/souq/page.tsx index 92c4b85..0180808 100644 --- a/src/app/souq/page.tsx +++ b/src/app/souq/page.tsx @@ -25,6 +25,7 @@ import { Percent, } from "lucide-react"; import PremiumBadge from "@/components/PremiumBadge"; +import ErrorFeedback from "@/components/ErrorFeedback"; const CATEGORIES = ["All", "E-Books", "Courses", "Design", "Audio", "Video", "Software", "Other"]; @@ -320,16 +321,7 @@ export default function SouqPage() {

Loading listings...

) : error ? ( -
- -

{error}

- -
+ ) : listings.length === 0 ? (
@@ -478,9 +470,7 @@ export default function SouqPage() {
{createError && ( -
-

{createError}

-
+ setCreateError(null)} context="create listing" /> )}
@@ -606,12 +596,12 @@ export default function SouqPage() {
{user.flhBalance < showConfirmDialog.listing.priceFlh && ( -
-

- Insufficient balance. You need {showConfirmDialog.listing.priceFlh.toLocaleString()} FLH - but only have {user.flhBalance.toLocaleString()} FLH. -

-
+ {}} + context="purchase" + /> )}
diff --git a/src/app/upgrade/page.tsx b/src/app/upgrade/page.tsx index 7040934..41bdcd1 100644 --- a/src/app/upgrade/page.tsx +++ b/src/app/upgrade/page.tsx @@ -4,6 +4,7 @@ import { useState, useEffect, Suspense } from "react"; import { useAuth } from "@/lib/AuthContext"; import { useRouter, useSearchParams } from "next/navigation"; import { Crown, Zap, Check, ChevronLeft, Loader2 } from "lucide-react"; +import ErrorFeedback from "@/components/ErrorFeedback"; const TIERS = [ { @@ -207,21 +208,25 @@ function UpgradeContent() {
{/* Messages */} - {message && ( + {message && message.type === "success" && (
-
+
{message.text}
)} + {message && message.type === "info" && ( +
+
+ {message.text} +
+
+ )} + {message && message.type === "error" && ( +
+ setMessage(null)} context="upgrade" /> +
+ )} {/* Tier Cards */}
diff --git a/src/app/wallet/page.tsx b/src/app/wallet/page.tsx index 272c7f1..ded2663 100644 --- a/src/app/wallet/page.tsx +++ b/src/app/wallet/page.tsx @@ -17,6 +17,7 @@ import { Sparkles, Info, } from "lucide-react"; +import ErrorFeedback from "@/components/ErrorFeedback"; interface CashoutEntry { id: string; @@ -224,22 +225,15 @@ export default function WalletPage() {
{/* Messages */} - {cashoutMessage && ( -
- {cashoutMessage.type === "success" ? ( - - ) : ( - - )} + {cashoutMessage && cashoutMessage.type === "success" && ( +
+ {cashoutMessage.text}
)} + {cashoutMessage && cashoutMessage.type === "error" && ( + setCashoutMessage(null)} context="wallet" /> + )} {/* Top-Up Section */}
diff --git a/src/components/ErrorFeedback.tsx b/src/components/ErrorFeedback.tsx new file mode 100644 index 0000000..29a63bd --- /dev/null +++ b/src/components/ErrorFeedback.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { AlertTriangle, Send, X, Bug } from "lucide-react"; + +// ── Feedback submission API ── +async function submitFeedback(data: { + url: string; + error: string; + message?: string; + userAgent: string; +}) { + try { + const res = await fetch("/mobile/api/feedback", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + return res.ok; + } catch { + return false; // silently fail — we don't want a feedback-on-feedback loop + } +} + +// ── Friendly error messages map ── +const FRIENDLY_MESSAGES: Record = { + default: { + title: "A small glitch 🛠️", + description: + "Something didn't load quite right. Don't worry — we're on it! Try again or drop us a report so we can fix it faster.", + }, + location: { + title: "Location not available 📡", + description: + "We couldn't detect your location. Make sure location services are enabled, or try refreshing. The compass will work even without location!", + }, + network: { + title: "Connection hiccup 🌐", + description: + "Looks like the signal dropped for a moment. Check your connection and try again. If it persists, let us know!", + }, + auth: { + title: "Session expired 🔑", + description: + "Your session ran out — it happens! Just sign in again and you'll be right back where you were.", + }, + upgrade: { + title: "Premium feature ✨", + description: + "This feature needs an upgrade. Check your plan details to unlock it!", + }, +}; + +type ErrorKind = keyof typeof FRIENDLY_MESSAGES; + +interface ErrorFeedbackProps { + /** The raw error message from the system */ + error: string; + /** Category of error for friendly message mapping */ + kind?: ErrorKind; + /** Callback to retry the action */ + onRetry?: () => void; + /** Additional context for debugging */ + context?: string; +} + +export default function ErrorFeedback({ + error, + kind = "default", + onRetry, + context, +}: ErrorFeedbackProps) { + const [showForm, setShowForm] = useState(false); + const [userMessage, setUserMessage] = useState(""); + const [sent, setSent] = useState(false); + const [sending, setSending] = useState(false); + + const friendly = FRIENDLY_MESSAGES[kind] || FRIENDLY_MESSAGES.default; + + const handleSend = useCallback(async () => { + setSending(true); + const ok = await submitFeedback({ + url: window.location.href, + error: error + (context ? ` [${context}]` : ""), + message: userMessage, + userAgent: navigator.userAgent, + }); + setSent(true); + setSending(false); + if (ok) { + setTimeout(() => setShowForm(false), 2000); + } + }, [error, context, userMessage]); + + return ( +
+
+ {/* ── Friendly header ── */} +
+
+ +
+
+

+ {friendly.title} +

+

+ {friendly.description} +

+
+
+ + {/* ── Actions ── */} +
+ {onRetry && ( + + )} + + +
+ + {/* ── Feedback form (expandable) ── */} + {showForm && ( +
+ {sent ? ( +

+ ✓ Thanks! Your report helps us improve. +

+ ) : ( + <> +

+ What happened? (optional — helps us fix it faster) +

+