diff --git a/backend/src/server.js b/backend/src/server.js index e29d6e5..b967572 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -589,6 +589,81 @@ app.post('/bases/:baseId/defenses', requireAuth, async (req, res) => { } }); +app.put('/trophy-resets/:resetId', requireAuth, async (req, res) => { + try { + const { resetId } = req.params; + const { date, trophiesAtStart, trophiesLost, numberOfDefenses, baseId } = req.body || {}; + + const parsedDate = new Date(date); + const parsedTrophiesAtStart = Number(trophiesAtStart); + const parsedTrophiesLost = Number(trophiesLost); + const parsedNumberOfDefenses = Number(numberOfDefenses); + + if (isNaN(parsedDate.getTime())) { + return res.status(400).json({ error: 'Invalid date' }); + } + if (!Number.isFinite(parsedTrophiesAtStart) || parsedTrophiesAtStart < 0) { + return res.status(400).json({ error: 'Trophies at start must be a positive number' }); + } + if (!Number.isFinite(parsedTrophiesLost)) { + return res.status(400).json({ error: 'Trophies lost must be a number' }); + } + if (!Number.isFinite(parsedNumberOfDefenses) || parsedNumberOfDefenses < 0) { + return res.status(400).json({ error: 'Number of defenses must be a positive number' }); + } + if (!baseId) { + return res.status(400).json({ error: 'Base is required' }); + } + + const reset = await prisma.trophyReset.findFirst({ + where: { id: resetId, base: { userId: req.user.id } }, + }); + + if (!reset) { + return res.status(404).json({ error: 'Trophy reset not found' }); + } + + const base = await prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } }); + if (!base) { + return res.status(404).json({ error: 'Base not found' }); + } + + await prisma.trophyReset.update({ + where: { id: reset.id }, + data: { + date: parsedDate, + trophiesAtStart: parsedTrophiesAtStart, + trophiesLost: parsedTrophiesLost, + numberOfDefenses: parsedNumberOfDefenses, + baseId: base.id, + }, + }); + + return res.json({ message: 'Trophy reset updated' }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.delete('/trophy-resets/:resetId', requireAuth, async (req, res) => { + try { + const { resetId } = req.params; + const result = await prisma.trophyReset.deleteMany({ + where: { id: resetId, base: { userId: req.user.id } }, + }); + + if (!result.count) { + return res.status(404).json({ error: 'Trophy reset not found' }); + } + + return res.json({ message: 'Trophy reset deleted' }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + app.put('/defenses/:defenseId', requireAuth, async (req, res) => { try { const { defenseId } = req.params; diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 8accd48..7c4234d 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -36,6 +36,8 @@ const API = { updateBase: (baseId: string) => `${API_BASE}/bases/${baseId}`, deleteBase: (baseId: string) => `${API_BASE}/bases/${baseId}`, addTrophyReset: (baseId: string) => `${API_BASE}/bases/${baseId}/trophy-resets`, + updateTrophyReset: (resetId: string) => `${API_BASE}/trophy-resets/${resetId}`, + deleteTrophyReset: (resetId: string) => `${API_BASE}/trophy-resets/${resetId}`, updateDefense: (defenseId: string) => `${API_BASE}/defenses/${defenseId}`, deleteDefense: (defenseId: string) => `${API_BASE}/defenses/${defenseId}`, profiles: `${API_BASE}/profiles`, @@ -125,6 +127,7 @@ type ProfileBase = { createdAt: string; summary: Summary; defenses: ProfileDefense[]; + trophyResets: TrophyReset[]; }; type ProfileCategorySummary = Summary & { @@ -229,6 +232,7 @@ export default function Page() { const [editImageMode, setEditImageMode] = useState<'keep' | 'upload' | 'url' | 'remove'>('keep'); const [editingBaseId, setEditingBaseId] = useState(null); const [editingDefenseId, setEditingDefenseId] = useState(null); + const [editingTrophyResetId, setEditingTrophyResetId] = useState(null); const [profileSearchTerm, setProfileSearchTerm] = useState(''); const [profileResults, setProfileResults] = useState([]); const [profileDetail, setProfileDetail] = useState(null); @@ -305,6 +309,12 @@ export default function Page() { return defenses.find((defense) => defense.id === editingDefenseId) ?? null; }, [editingDefenseId, defenses]); + const trophyResetBeingEdited = useMemo(() => { + if (!editingTrophyResetId) return null; + const allResets = bases.flatMap((base) => base.trophyResets); + return allResets.find((reset) => reset.id === editingTrophyResetId) ?? null; + }, [editingTrophyResetId, bases]); + const profileSelectedBaseCategories = useMemo(() => { if (!profileSelectedBase) { return [] as ProfileCategorySummary[]; @@ -515,6 +525,15 @@ export default function Page() { setEditingDefenseId(null); } + function startEditingTrophyReset(resetId: string) { + setEditingTrophyResetId(resetId); + setErrors((prev) => ({ ...prev, trophyReset: '' })); + } + + function cancelEditingTrophyReset() { + setEditingTrophyResetId(null); + } + async function handleDefenseEditSubmit(event: FormEvent) { event.preventDefault(); if (!editingDefenseId || !defenseBeingEdited) { @@ -551,6 +570,42 @@ export default function Page() { } } + async function handleTrophyResetEditSubmit(event: FormEvent) { + event.preventDefault(); + if (!editingTrophyResetId) { + return; + } + const form = event.currentTarget; + const formData = new FormData(form); + const payload = Object.fromEntries(formData.entries()); + try { + setErrors((prev) => ({ ...prev, trophyReset: '' })); + await request('PUT', API.updateTrophyReset(editingTrophyResetId), payload); + setEditingTrophyResetId(null); + await refreshData(); + await refreshOwnProfileDetail(); + } catch (error: any) { + setErrors((prev) => ({ ...prev, trophyReset: error.message })); + } + } + + async function handleDeleteTrophyReset(resetId: string) { + const confirmDelete = window.confirm('Delete this reset?'); + if (!confirmDelete) { + return; + } + try { + await request('DELETE', API.deleteTrophyReset(resetId)); + if (editingTrophyResetId === resetId) { + setEditingTrophyResetId(null); + } + await refreshData(); + await refreshOwnProfileDetail(); + } catch (error: any) { + setErrors((prev) => ({ ...prev, trophyReset: error.message })); + } + } + async function handleProfileSearch(event: FormEvent) { event.preventDefault(); setProfileError(''); @@ -630,15 +685,17 @@ export default function Page() { async function handleTrophyResetSubmit(event: FormEvent) { event.preventDefault(); - if (!selectedBaseId) { - return; - } const form = event.currentTarget; const formData = new FormData(form); const payload = Object.fromEntries(formData.entries()); + const baseId = payload.baseId as string; + if (!baseId) { + setErrors((prev) => ({ ...prev, trophyReset: 'Base is required' })); + return; + } try { setErrors((prev) => ({ ...prev, trophyReset: '' })); - await request('POST', API.addTrophyReset(selectedBaseId), payload); + await request('POST', API.addTrophyReset(baseId), payload); form.reset(); await refreshData(); } catch (error: any) { @@ -1466,6 +1523,134 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { )} +
+

Log Legend League Day Reset

+
+ + + + + + +

+ {errors.trophyReset} +

+
+
+
+

Manage Resets

+
    + {bases.flatMap((base) => base.trophyResets).length ? ( + bases.flatMap((base) => base.trophyResets).slice(0, 10).map((reset) => ( +
  • +
    +
    + {bases.find((b) => b.id === bases.find((b) => b.trophyResets.some((r) => r.id === reset.id))?.id)?.title}{' '} + {new Date(reset.date).toLocaleDateString()} +
    +
    + + +
    +
    +
    + {reset.trophiesAtStart} trophies + {formatTrophies(reset.trophiesLost)} lost + {reset.numberOfDefenses} defenses +
    +
  • + )) + ) : ( +
  • No resets logged yet.
  • + )} +
+
+
+ {editingTrophyResetId && trophyResetBeingEdited && ( +
+

Edit Reset

+
+ + + + + +
+ + +
+

{errors.trophyReset}

+
+
+ )} +
@@ -1545,31 +1730,8 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
-

Legend League Day Reset

-
- - - - - -

{errors.trophyReset}

-
-
-

Recent Resets

+
+

Recent Resets

    {baseDetailMeta?.trophyResets?.length ? ( baseDetailMeta.trophyResets.map((reset) => ( @@ -1577,18 +1739,34 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
    {new Date(reset.date).toLocaleDateString()}
    - {reset.trophiesAtStart} trophies at start - {formatTrophies(reset.trophiesLost)} lost - {reset.numberOfDefenses} defenses + +
    +
    + {reset.trophiesAtStart} trophies at start + {formatTrophies(reset.trophiesLost)} lost + {reset.numberOfDefenses} defenses +
    )) ) : (
  • No resets logged yet.
  • )}
-
+

Defenses

@@ -1704,6 +1882,29 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { )}
+
+
+

Recent Resets

+
    + {profileSelectedBase.trophyResets.length ? ( + profileSelectedBase.trophyResets.map((reset) => ( +
  • +
    + {new Date(reset.date).toLocaleDateString()} +
    + {reset.trophiesAtStart} trophies at start + {formatTrophies(reset.trophiesLost)} lost + {reset.numberOfDefenses} defenses +
    +
    +
  • + )) + ) : ( +
  • No resets logged yet.
  • + )} +
+
+

Defenses