Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 52 additions & 83 deletions src/pages/Report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,58 @@
import {
deleteOldResolvedReports,
sendReport,
fetchMyReports,
} from "@/supabase/reports";
import { CheckCircle2, Clock, RefreshCw } from "lucide-react";
import { Link } from "react-router-dom";

type Report = {
id: string;
message: string;
resolved: boolean;
createdAt: Date | null;
};
import { ArrowLeft } from "lucide-react";

export default function Report() {
const [message, setMessage] = useState("");
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
const [reports, setReports] = useState<Report[]>([]);
const [refreshing, setRefreshing] = useState(false);
const navigate = useNavigate();

const REPORT_COOLDOWN_KEY = "report_cooldown";
const COOLDOWN_TIME = 2 * 60 * 1000;

const [cooldown, setCooldown] = useState(0);

useEffect(() => {
const interval = setInterval(() => {
const lastSubmitted = localStorage.getItem(REPORT_COOLDOWN_KEY);

if (lastSubmitted) {
const diff = Date.now() - Number(lastSubmitted);
const remaining = Math.max(0, COOLDOWN_TIME - diff);
setCooldown(Math.ceil(remaining / 1000));
}
}, 1000);

return () => clearInterval(interval);
}, []);

Check warning on line 36 in src/pages/Report.tsx

View workflow job for this annotation

GitHub Actions / build-and-test (20)

React Hook useEffect has a missing dependency: 'COOLDOWN_TIME'. Either include it or remove the dependency array

Check warning on line 36 in src/pages/Report.tsx

View workflow job for this annotation

GitHub Actions / build-and-test (20)

React Hook useEffect has a missing dependency: 'COOLDOWN_TIME'. Either include it or remove the dependency array

useEffect(() => {
deleteOldResolvedReports();
loadReports().finally(() => setLoading(false));
setLoading(false);
}, []);

const loadReports = async () => {
const data = await fetchMyReports();
setReports(data);
};
const handleSubmit = async () => {
const lastSubmitted = localStorage.getItem(REPORT_COOLDOWN_KEY);

const handleRefresh = async () => {
setRefreshing(true);
await loadReports();
setRefreshing(false);
};
if (lastSubmitted) {
const diff = Date.now() - Number(lastSubmitted);

if (diff < COOLDOWN_TIME) {
const remaining = Math.ceil((COOLDOWN_TIME - diff) / 1000);
toast.error(`Please wait ${remaining}s before submitting again`);
return;
}
}

const handleSubmit = async () => {
if (!message.trim()) {
toast.error("Please describe the issue");
return;
}

if (message.length < 10 || message.length > 500) {
toast.error("Report should be between 10 and 500 characters");
return;
Expand All @@ -57,21 +68,12 @@
setSubmitting(false);

if (id) {
localStorage.setItem(REPORT_COOLDOWN_KEY, Date.now().toString());
setMessage("");
toast.success("Report submitted. We'll look into it :)");
await loadReports();
toast.success("Report submitted. We'll look into it");
}
};

const formatDate = (date: Date | null) => {
if (!date) return "";
return date.toLocaleDateString("en-IN", {
day: "numeric",
month: "short",
year: "numeric",
});
};

if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
Expand All @@ -88,6 +90,16 @@
{/* Main Content */}
<div className="px-4 pt-10 pb-24 sm:pt-12 sm:pb-28 flex flex-col items-center">

<div className="w-full max-w-xl mb-4">
<button
onClick={() => navigate("/", { replace: true })}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition"
>
<ArrowLeft size={18} />
Back to Home
</button>
</div>

{/* Header */}
<div className="w-full max-w-xl text-center mb-8 space-y-2">
<h1 className="text-3xl font-semibold tracking-tight">
Expand Down Expand Up @@ -119,60 +131,17 @@
<Button
className="flex-1 h-11 rounded-xl"
onClick={handleSubmit}
// disabled={submitting}
disabled
disabled={submitting || cooldown > 0}
>
{submitting ? "Submitting…" : "Feature Coming Soon!"}
</Button>

<Button
variant="outline"
className="flex-1 h-11 rounded-xl"
onClick={() => navigate("/", { replace: true })}
>
Back
{submitting
? "Submitting…"
: cooldown > 0
? `Wait ${cooldown}s`
: "Submit Report"}
</Button>
</div>
</div>

{/* Reports */}
{reports.length > 0 && (
<div className="w-full max-w-xl mt-10 space-y-3">
{reports.map((r) => (
<div
key={r.id}
className="bg-card border border-border rounded-xl p-4 flex items-start gap-3"
>
{r.resolved ? (
<CheckCircle2 className="h-4 w-4 text-green-500 mt-1" />
) : (
<Clock className="h-4 w-4 text-muted-foreground mt-1" />
)}

<div className="flex-1 space-y-1">
<p className="text-sm line-clamp-3">{r.message}</p>

<div className="flex items-center gap-2 text-xs">
{r.createdAt && (
<span className="text-muted-foreground">
{formatDate(r.createdAt)}
</span>
)}
<span
className={`px-2 py-0.5 rounded-md ${
r.resolved
? "bg-green-500/10 text-green-600"
: "bg-muted text-muted-foreground"
}`}
>
{r.resolved ? "Resolved" : "Pending"}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>

{/* Footer */}
Expand Down Expand Up @@ -228,4 +197,4 @@
</div>
</div>
);
}
}
158 changes: 92 additions & 66 deletions src/supabase/reports.ts
Original file line number Diff line number Diff line change
@@ -1,99 +1,125 @@
import { supabase } from "@/lib/supabase";

export type Report = {
const DEVICE_ID_KEY = "labxam_device";

function getDeviceId(): string {
let id = localStorage.getItem(DEVICE_ID_KEY);
if(!id) {
id = crypto.randomUUID();
localStorage.setItem(DEVICE_ID_KEY, id);
}
return id;
}

type Report = {
id: string;
message: string;
resolved: boolean;
createdAt: Date | null;
};

export const fetchReports = async (): Promise<Report[]> => {
const { data, error } = await supabase
.from("reports")
.select("id, message, resolved, created_at")
.eq("resolved", false)
.order("created_at", { ascending: false });

if (error || !data) return [];

return data.map((row) => ({
id: row.id,
message: row.message,
resolved: row.resolved,
createdAt: row.created_at ? new Date(row.created_at) : null,
}));
};

export const resolveReport = async (id: string) => {
const { error } = await supabase
.from("reports")
.update({ resolved: true })
.eq("id", id);

return !error;
};

export const submitReport = async (message: string) => {
const { error } = await supabase
.from("reports")
.insert({ message });

return !error;
};

export const sendReport = async (message: string): Promise<string | null> => {
export async function sendReport(message: string): Promise<string | null> {
const { data, error } = await supabase
.from("reports")
.insert({ message })
.select("id")
.single();

if (error || !data) return null;

const existing = JSON.parse(localStorage.getItem("myReportIds") || "[]");
localStorage.setItem("myReportIds", JSON.stringify([...existing, data.id]));
if(error || !data) {
return null;
}

return data.id;
};
const existing = getMyReportIds();
existing.unshift(data.id);
localStorage.setItem("labxam_report_ids", JSON.stringify(existing));
return data.id;
}

export const fetchMyReports = async (): Promise<Report[]> => {
const ids: string[] = JSON.parse(localStorage.getItem("myReportIds") || "[]");
export async function fetchMyReports(): Promise<Report[]> {
const ids = getMyReportIds().slice(0, 10);
if (ids.length === 0) return [];

const { data, error } = await supabase
.from("reports")
.select("id, message, resolved, created_at")
.in("id", ids)
.order("created_at", { ascending: false });

if (error || !data) return [];

return data.map((row) => ({
id: row.id,
message: row.message,
resolved: row.resolved,
createdAt: row.created_at ? new Date(row.created_at) : null,

if (error || !data) {
return [];
}

return data.map((r) => ({
id: r.id,
message: r.message,
resolved: r.resolved ?? false,
createdAt: r.created_at ? new Date(r.created_at) : null,
}));
};
}

export const deleteOldResolvedReports = async () => {
const ids: string[] = JSON.parse(localStorage.getItem("myReportIds") || "[]");
export async function deleteOldResolvedReports(): Promise<void> {
const ids = getMyReportIds();
if (ids.length === 0) return;

const { data } = await supabase
const { data, error } = await supabase
.from("reports")
.select("id, resolved, created_at")
.in("id", ids);

if (error || !data) return;

const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const toRemove = new Set(
data
.filter(
(r) =>
r.resolved &&
r.created_at &&
new Date(r.created_at) < sevenDaysAgo
)
.map((r) => r.id)
);

if (toRemove.size === 0) return;

const cleaned = ids.filter((id) => !toRemove.has(id));
localStorage.setItem("labxam_report_ids", JSON.stringify(cleaned));
}

function getMyReportIds(): string[] {
try {
const raw = localStorage.getItem("labxam_report_ids");
return raw ? JSON.parse(raw) : [];
} catch {
return [];
}
}

export async function fetchReports(): Promise<
{ id: string; message: string; resolved: boolean }[]
> {
const { data, error } = await supabase
.from("reports")
.select("id, message, resolved")
.eq("resolved", false)
.order("created_at", { ascending: false });

if (!data) return;
if (error || !data) {
console.error("fetchReports error:", error);
return [];
}
return data;
}

const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const toRemove = data
.filter((r) => r.resolved && new Date(r.created_at).getTime() < sevenDaysAgo)
.map((r) => r.id);

if (toRemove.length === 0) return;
export async function resolveReport(id: string): Promise<void> {
const { error } = await supabase
.from("reports")
.update({ resolved: true })
.eq("id", id);

const remaining = ids.filter((id) => !toRemove.includes(id));
localStorage.setItem("myReportIds", JSON.stringify(remaining));
};
if (error) throw error;
}
Loading