From bf491fc9cd010b2095bc4f5e2766d927195b250f Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Tue, 8 Sep 2026 15:57:01 -0400 Subject: [PATCH] feat(eod): add author exposure toggle and reduced-XP copy to custom play Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/app/(frontend)/eod/custom/[id]/page.tsx | 10 +++- .../frontend/eod/CreateMinefieldDialog.tsx | 30 ++++++++++- .../frontend/eod/CustomMinefieldArena.tsx | 6 +++ .../frontend/eod/CustomMinefieldBrowser.tsx | 3 +- .../frontend/eod/ExposureRuleToggle.tsx | 54 +++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 src/components/frontend/eod/ExposureRuleToggle.tsx diff --git a/src/app/(frontend)/eod/custom/[id]/page.tsx b/src/app/(frontend)/eod/custom/[id]/page.tsx index a15caa7..b1dfba6 100644 --- a/src/app/(frontend)/eod/custom/[id]/page.tsx +++ b/src/app/(frontend)/eod/custom/[id]/page.tsx @@ -41,6 +41,11 @@ export default async function CustomMinefieldPlayPage({ const mines = Array.isArray(minefield.mines) ? (minefield.mines as { row: number; col: number }[]) : []; + const authorId = + typeof minefield.author === "object" && minefield.author !== null + ? minefield.author.id + : minefield.author; + const isAuthor = user !== null && String(authorId) === String(user.id); return (
@@ -58,7 +63,8 @@ export default async function CustomMinefieldPlayPage({

{minefield.name}

- Custom minefields are unranked and do not affect official stats or XP. + Custom wins award reduced XP (10% of standard payouts); field authors bank 5% of each + payout.

@@ -69,8 +75,10 @@ export default async function CustomMinefieldPlayPage({ rows: minefield.rows, cols: minefield.cols, mines, + exposeEdgeMines: minefield.exposeEdgeMines ?? true, }} currentUserId={user?.id ?? 0} + isAuthor={isAuthor} /> ); diff --git a/src/components/frontend/eod/CreateMinefieldDialog.tsx b/src/components/frontend/eod/CreateMinefieldDialog.tsx index 33c59c3..e78edd7 100644 --- a/src/components/frontend/eod/CreateMinefieldDialog.tsx +++ b/src/components/frontend/eod/CreateMinefieldDialog.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { Bomb, Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -34,6 +35,7 @@ export function CreateMinefieldDialog({ const [rows, setRows] = useState(9); const [cols, setCols] = useState(9); const [mines, setMines] = useState>(new Set()); + const [exposeEdgeMines, setExposeEdgeMines] = useState(true); const [error, setError] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -78,7 +80,13 @@ export function CreateMinefieldDialog({ row: Math.floor(key / cols), col: key % cols, })); - const result = await createMinefield({ name: trimmed, rows, cols, mines: positions }); + const result = await createMinefield({ + name: trimmed, + rows, + cols, + mines: positions, + exposeEdgeMines, + }); setSubmitting(false); if (result.success) { setOpen(false); @@ -189,7 +197,7 @@ export function CreateMinefieldDialog({ role="gridcell" aria-label={`row ${Math.floor(index / cols) + 1} col ${(index % cols) + 1}${painted ? " mine" : ""}`} className={cn( - "size-5 rounded-sm border transition-colors", + "flex size-5 items-center justify-center rounded-sm border transition-colors", painted ? "border-destructive bg-destructive text-destructive-foreground" : "border-emerald-700/60 bg-emerald-900/40 hover:border-emerald-400 hover:bg-emerald-700", @@ -208,6 +216,24 @@ export function CreateMinefieldDialog({

)} +
+ setExposeEdgeMines(value === true)} + className="mt-0.5" + /> +
+ +

+ Failed runs show mines that touch the swept area. Turn off to reveal none of the + remaining charges. +

+
+
+ {error &&

{error}

} diff --git a/src/components/frontend/eod/CustomMinefieldArena.tsx b/src/components/frontend/eod/CustomMinefieldArena.tsx index 65c2ad7..c711532 100644 --- a/src/components/frontend/eod/CustomMinefieldArena.tsx +++ b/src/components/frontend/eod/CustomMinefieldArena.tsx @@ -3,6 +3,7 @@ import { useCallback, useState } from "react"; import { CustomMinesweeperBoard, type CustomMinefieldData } from "./CustomMinesweeperBoard"; import { CustomMinefieldLeaderboard } from "./CustomMinefieldLeaderboard"; +import { ExposureRuleToggle } from "./ExposureRuleToggle"; import { useRealtimePresence } from "@/hooks/useRealtimePresence"; import { useSidebar } from "@/components/ui/sidebar"; import { useEffect } from "react"; @@ -10,9 +11,11 @@ import { useEffect } from "react"; export function CustomMinefieldArena({ minefield, currentUserId, + isAuthor, }: { minefield: CustomMinefieldData; currentUserId: number | string; + isAuthor: boolean; }) { const [refreshKey, setRefreshKey] = useState(0); const [playing, setPlaying] = useState(false); @@ -33,6 +36,9 @@ export function CustomMinefieldArena({ onGameRecorded={() => setRefreshKey((key) => key + 1)} onPhaseChange={onPhaseChange} /> + {isAuthor && ( + + )}

- Community minefields. Playing these does not affect your official stats or XP. + Community minefields. Wins award reduced XP (10% of standard payouts); field authors + bank 5% of each payout.

diff --git a/src/components/frontend/eod/ExposureRuleToggle.tsx b/src/components/frontend/eod/ExposureRuleToggle.tsx new file mode 100644 index 0000000..939b7fc --- /dev/null +++ b/src/components/frontend/eod/ExposureRuleToggle.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Switch } from "@/components/ui/switch"; +import { updateMinefield } from "@/app/(frontend)/eod/custom/actions"; + +export function ExposureRuleToggle({ + minefieldId, + exposeEdgeMines, +}: { + minefieldId: number; + exposeEdgeMines: boolean; +}) { + const router = useRouter(); + const [checked, setChecked] = useState(exposeEdgeMines); + const [pending, setPending] = useState(false); + const [error, setError] = useState(null); + + const onCheckedChange = async (next: boolean) => { + setError(null); + setPending(true); + const result = await updateMinefield({ id: minefieldId, exposeEdgeMines: next }); + setPending(false); + if (result.success) { + setChecked(next); + router.refresh(); + } else { + setError(result.error ?? "Could not update the exposure rule."); + } + }; + + return ( +
+
+ + +
+

+ {checked + ? "Failed runs show mines touching the swept area; everything else stays hidden." + : "Failed runs reveal no remaining charges; swept cells turn gray with a question mark."} +

+ {error &&

{error}

} +
+ ); +}