diff --git a/src/components/newPdfViewer.tsx b/src/components/newPdfViewer.tsx index 32577da..68ead11 100644 --- a/src/components/newPdfViewer.tsx +++ b/src/components/newPdfViewer.tsx @@ -467,6 +467,20 @@ export default function PDFViewer({ const [isReadingMode, setIsReadingMode] = useState(false); const [showReadingCoachmark, setShowReadingCoachmark] = useState(false); const viewerRef = useRef(null); + const [urlStatus, setUrlStatus] = useState<"checking" | "ok" | "error">("checking"); + + useEffect(() => { + let isMounted = true; + fetch(url, { method: "HEAD" }) + .then(res => { + if (isMounted) setUrlStatus(res.ok ? "ok" : "error"); + }) + .catch(() => { + if (isMounted) setUrlStatus("error"); + }); + return () => { isMounted = false; }; + }, [url]); + const effectiveBackgroundColor = backgroundColor ?? (resolvedTheme === "light" ? "#F3F5FF" : "#070114"); const loaderTextColor = @@ -571,7 +585,19 @@ export default function PDFViewer({ }), ], [url, name]); -if (isLoading || !engine) { + if (urlStatus === "error") { + return ( +
+ + Paper Not Found + + This paper has been deleted or is missing from the server. + +
+ ); + } + + if (isLoading || !engine || urlStatus === "checking") { return ( } - {({ isLoaded }) => ( + {({ isLoaded, isError }) => ( <>
- + {isError ? ( +
+ + Paper Not Found + + This paper has been deleted or is missing from the server. + +
+ ) : ( + + )}
= ({ ).map((id) => selectedPapers.find((paper) => paper._id === id)) as IPaper[]; let failedCount = 0; + // Different papers can share a generated name (e.g. same subject/exam/slot/year + // from different campuses). JSZip overwrites entries with the same path, so + // append " (1)", " (2)", ... to keep every paper in the zip. + const usedNames = new Map(); + const getUniqueName = (name: string): string => { + const count = usedNames.get(name) ?? 0; + usedNames.set(name, count + 1); + if (count === 0) return name; + const dot = name.lastIndexOf("."); + return dot > 0 + ? `${name.slice(0, dot)} (${count})${name.slice(dot)}` + : `${name} (${count})`; + }; await Promise.all( uniquePapers.map(async (paper) => { @@ -171,7 +184,7 @@ export const FilterProvider: React.FC = ({ throw new Error(`HTTP ${response.status}`); } const blob = await response.blob(); - const filename = generateFileName(paper); + const filename = getUniqueName(generateFileName(paper)); zip.file(filename, blob); } catch (err) { failedCount += 1; @@ -202,7 +215,7 @@ export const FilterProvider: React.FC = ({ const a = document.createElement("a"); a.href = url; - a.download = getDownloadName(searchParams, "subject"); + a.download = `${getDownloadName(searchParams, "subject")}.zip`; document.body.appendChild(a); a.click(); @@ -292,7 +305,7 @@ export const FilterProvider: React.FC = ({ () => Math.ceil( (appliedFilters ? filteredPapers.length : papers.length) / - papersPerPage, + papersPerPage, ), [appliedFilters, filteredPapers.length, papers.length, papersPerPage], ); diff --git a/src/lib/posthog-server.ts b/src/lib/posthog-server.ts index fe4f3e2..d7e0bf0 100644 --- a/src/lib/posthog-server.ts +++ b/src/lib/posthog-server.ts @@ -1,8 +1,17 @@ -import { PostHog } from "posthog-node"; +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + +import type { PostHog } from "posthog-node"; let posthogClient: PostHog | null = null; export function getPostHogClient(): PostHog | null { + if (posthogClient) return posthogClient; + const token = process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN; if (!token) { if (process.env.NODE_ENV !== "production") { @@ -12,7 +21,11 @@ export function getPostHogClient(): PostHog | null { } return null; } - posthogClient ??= new PostHog(token, { + + const phModule = require("posthog-node"); + const PHClass = phModule.PostHog || phModule.default?.PostHog || phModule.default || phModule; + + posthogClient ??= new PHClass(token, { host: process.env.NEXT_PUBLIC_POSTHOG_HOST, flushAt: 1, flushInterval: 0, diff --git a/src/lib/utils/download.ts b/src/lib/utils/download.ts index e72072a..5d02d10 100644 --- a/src/lib/utils/download.ts +++ b/src/lib/utils/download.ts @@ -1,6 +1,7 @@ import { type IPaper } from "../../interface"; import { extractBracketContent } from "./string"; import axios from "axios"; +import toast from "react-hot-toast"; export const getSecureUrl = (url: string): string => url.startsWith("http://") ? url.replace("http://", "https://") : url; @@ -23,6 +24,11 @@ export const downloadFile = async ( link.click(); window.URL.revokeObjectURL(link.href); } catch (error) { - console.error("Download failed:", error); + console.error("Download failed (File missing or network error):", error); + if (typeof navigator !== "undefined" && !navigator.onLine) { + toast.error("You are offline. Please check your internet connection."); + } else { + toast.error("This paper is no longer available on the server."); + } } }; \ No newline at end of file