diff --git a/src/components/admin-match-analysis.tsx b/src/components/admin-match-analysis.tsx index edb7d1f..411c85c 100644 --- a/src/components/admin-match-analysis.tsx +++ b/src/components/admin-match-analysis.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { useMemo } from "react"; import { ClickableUserId } from "@/components/clickable-user-id"; +import { AnalysisMatchAlertBox } from "@/components/analysis-match-alert-box"; import { MatchAnalysisEmoteChart } from "@/components/match-analysis-emote-chart"; import { MatchAnalysisForceChart } from "@/components/match-analysis-force-chart"; import { buildDashboardHref } from "@/lib/dashboard-search-url"; @@ -269,41 +270,19 @@ export function AdminMatchAnalysis({ - {analysis.mirrorExceptionDisconnectMatchIds.length > 0 ? ( -
- Mirror exception disconnect ( - {analysis.mirrorExceptionDisconnectMatchIds.length.toLocaleString( - "en-US", - )}{" "} - {analysis.mirrorExceptionDisconnectMatchIds.length === 1 - ? "match" - : "matches"} - ) -
-
+
- Not ended ( - {analysis.notEndedMatchIds.length.toLocaleString("en-US")}{" "} - {analysis.notEndedMatchIds.length === 1 ? "match" : "matches"}) -
-- Log file present but no winner PATCH line — the match did not - finish normally. -
-
Every match with a log file includes a winner PATCH line.
diff --git a/src/components/analysis-match-alert-box.tsx b/src/components/analysis-match-alert-box.tsx
new file mode 100644
index 0000000..254abad
--- /dev/null
+++ b/src/components/analysis-match-alert-box.tsx
@@ -0,0 +1,178 @@
+"use client";
+
+import Link from "next/link";
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+ type MouseEvent,
+ type ReactNode,
+} from "react";
+import {
+ EMPTY_HIDDEN_MIRROR,
+ EMPTY_HIDDEN_NOT_ENDED,
+ getHiddenAnalysisMatchIds,
+ hideAnalysisMatchIds,
+ subscribeAnalysisHiddenAlerts,
+ type AnalysisAlertCategory,
+} from "@/lib/analysis-alert-hide-cookies";
+
+function CloseIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+const STYLES: Record<
+ AnalysisAlertCategory,
+ {
+ box: string;
+ body: string;
+ chip: string;
+ dismiss: string;
+ headerDismiss: string;
+ }
+> = {
+ mirror: {
+ box: "rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-950 dark:border-red-800 dark:bg-red-950/50 dark:text-red-100",
+ body: "text-red-800 dark:text-red-200/90",
+ chip: "inline-flex items-center gap-0.5 overflow-hidden rounded-md border border-red-400/80 bg-white shadow-sm dark:border-red-700 dark:bg-red-950/80",
+ dismiss:
+ "shrink-0 p-1 text-red-700 hover:bg-red-100 dark:text-red-200 dark:hover:bg-red-900/80",
+ headerDismiss:
+ "shrink-0 rounded-md p-1.5 text-red-800 hover:bg-red-100 dark:text-red-100 dark:hover:bg-red-900/80",
+ },
+ notEnded: {
+ box: "rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-950 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100",
+ body: "text-amber-900/90 dark:text-amber-200/90",
+ chip: "inline-flex items-center gap-0.5 overflow-hidden rounded-md border border-amber-400/80 bg-white shadow-sm dark:border-amber-700 dark:bg-amber-950/80",
+ dismiss:
+ "shrink-0 p-1 text-amber-800 hover:bg-amber-100 dark:text-amber-100 dark:hover:bg-amber-900/80",
+ headerDismiss:
+ "shrink-0 rounded-md p-1.5 text-amber-900 hover:bg-amber-100 dark:text-amber-50 dark:hover:bg-amber-900/80",
+ },
+};
+
+function emptyHiddenFor(category: AnalysisAlertCategory): number[] {
+ return category === "mirror" ? EMPTY_HIDDEN_MIRROR : EMPTY_HIDDEN_NOT_ENDED;
+}
+
+function useHiddenMatchIds(category: AnalysisAlertCategory): number[] {
+ const [hiddenIds, setHiddenIds] = useState
+ {title} ({visibleIds.length.toLocaleString("en-US")}{" "}
+ {visibleIds.length === 1 ? "match" : "matches"})
+ {description}
+ {visibleIds.map((matchId) => (
+
+