From b466e585a1ba27c89e04b5d3e5c705db845e6b78 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 13:33:56 +0200 Subject: [PATCH 01/10] Army category optional. add Trophy Reset --- backend/prisma/schema.prisma | 14 ++++- backend/src/server.js | 103 ++++++++++++++++++++++++++--------- frontend/app/page.tsx | 85 ++++++++++++++++++++++++++--- 3 files changed, 166 insertions(+), 36 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9376b7e..4195f09 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -39,6 +39,16 @@ model Base { user User @relation(fields: [userId], references: [id]) userId String defenses Defense[] + trophyResets TrophyReset[] +} + +model TrophyReset { + id String @id @default(cuid()) + trophiesAtStart Int + trophiesLost Int + createdAt DateTime @default(now()) + base Base @relation(fields: [baseId], references: [id]) + baseId String } model Defense { @@ -49,6 +59,6 @@ model Defense { createdAt DateTime @default(now()) base Base @relation(fields: [baseId], references: [id]) baseId String - armyCategory ArmyCategory @relation(fields: [armyCategoryId], references: [id]) - armyCategoryId String + armyCategory ArmyCategory? @relation(fields: [armyCategoryId], references: [id]) + armyCategoryId String? } diff --git a/backend/src/server.js b/backend/src/server.js index ea2746a..e2e7240 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -480,6 +480,41 @@ app.delete('/bases/:baseId', requireAuth, async (req, res) => { } }); +app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => { + try { + const { baseId } = req.params; + const { trophiesAtStart, trophiesLost } = req.body || {}; + + const parsedTrophiesAtStart = Number(trophiesAtStart); + const parsedTrophiesLost = Number(trophiesLost); + + 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' }); + } + + 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.create({ + data: { + baseId: base.id, + trophiesAtStart: parsedTrophiesAtStart, + trophiesLost: parsedTrophiesLost, + }, + }); + + return res.status(201).json({ message: 'Trophy reset logged' }); + } catch (error) { + console.error(error); + return res.status(500).json({ error: 'Internal server error' }); + } +}); + app.post('/bases/:baseId/defenses', requireAuth, async (req, res) => { try { const { baseId } = req.params; @@ -489,9 +524,6 @@ app.post('/bases/:baseId/defenses', requireAuth, async (req, res) => { const parsedPercent = Number(percent); const parsedTrophies = Number(trophies ?? 0); - if (!armyCategoryId) { - return res.status(400).json({ error: 'Army category is required' }); - } if (!Number.isFinite(parsedStars) || parsedStars < 0 || parsedStars > 3) { return res.status(400).json({ error: 'Stars must be between 0 and 3' }); } @@ -502,22 +534,25 @@ app.post('/bases/:baseId/defenses', requireAuth, async (req, res) => { return res.status(400).json({ error: 'Trophies must be between -200 and 200' }); } - const [base, category] = await Promise.all([ - prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } }), - prisma.armyCategory.findFirst({ where: { id: armyCategoryId, userId: req.user.id } }), - ]); - + const base = await prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } }); if (!base) { return res.status(404).json({ error: 'Base not found' }); } - if (!category) { - return res.status(404).json({ error: 'Army category not found' }); + + let category = null; + if (armyCategoryId) { + category = await prisma.armyCategory.findFirst({ + where: { id: armyCategoryId, userId: req.user.id }, + }); + if (!category) { + return res.status(404).json({ error: 'Army category not found' }); + } } await prisma.defense.create({ data: { baseId: base.id, - armyCategoryId: category.id, + armyCategoryId: category ? category.id : null, stars: parsedStars, percent: parsedPercent, trophies: parsedTrophies, @@ -539,9 +574,6 @@ app.put('/defenses/:defenseId', requireAuth, async (req, res) => { if (!baseId) { return res.status(400).json({ error: 'Base is required' }); } - if (!armyCategoryId) { - return res.status(400).json({ error: 'Army category is required' }); - } const parsedStars = Number(stars); const parsedPercent = Number(percent); @@ -557,29 +589,34 @@ app.put('/defenses/:defenseId', requireAuth, async (req, res) => { return res.status(400).json({ error: 'Trophies must be between -200 and 200' }); } - const [defense, base, category] = await Promise.all([ - prisma.defense.findFirst({ - where: { id: defenseId, base: { userId: req.user.id } }, - }), - prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } }), - prisma.armyCategory.findFirst({ where: { id: armyCategoryId, userId: req.user.id } }), - ]); + const defense = await prisma.defense.findFirst({ + where: { id: defenseId, base: { userId: req.user.id } }, + }); if (!defense) { return res.status(404).json({ error: 'Defense 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' }); } - if (!category) { - return res.status(404).json({ error: 'Army category not found' }); + + let category = null; + if (armyCategoryId) { + category = await prisma.armyCategory.findFirst({ + where: { id: armyCategoryId, userId: req.user.id }, + }); + if (!category) { + return res.status(404).json({ error: 'Army category not found' }); + } } await prisma.defense.update({ where: { id: defense.id }, data: { baseId: base.id, - armyCategoryId: category.id, + armyCategoryId: armyCategoryId ? category.id : null, stars: parsedStars, percent: parsedPercent, trophies: parsedTrophies, @@ -628,10 +665,16 @@ app.get('/defenses', requireAuth, async (req, res) => { const categoryLookup = new Map( user.armyCategories.map((category) => [category.id, category.name]) ); + categoryLookup.set(null, '(No category)'); const defenses = []; const baseBuckets = new Map(); const categoryBuckets = new Map(); + categoryBuckets.set(null, { + name: '(No category)', + items: [], + bases: new Map(), + }); user.bases.forEach((base) => { baseBuckets.set(base.id, { @@ -813,6 +856,9 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { }, }, }, + trophyResets: { + orderBy: { createdAt: 'desc' }, + }, }, }); @@ -824,7 +870,13 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { trophies: defense.trophies, createdAt: defense.createdAt, armyCategoryId: defense.armyCategoryId, - armyCategoryName: defense.armyCategory?.name || 'Unknown Army', + armyCategoryName: defense.armyCategory?.name || '(No category)', + })); + const trophyResets = base.trophyResets.map((reset) => ({ + id: reset.id, + trophiesAtStart: reset.trophiesAtStart, + trophiesLost: reset.trophiesLost, + createdAt: reset.createdAt, })); return { id: base.id, @@ -836,6 +888,7 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { createdAt: base.createdAt, summary: summarizeDefenses(defenses), defenses, + trophyResets, }; }); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 80ed021..16eea77 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -35,6 +35,7 @@ const API = { deleteCategory: (categoryId: string) => `${API_BASE}/army-categories/${categoryId}`, updateBase: (baseId: string) => `${API_BASE}/bases/${baseId}`, deleteBase: (baseId: string) => `${API_BASE}/bases/${baseId}`, + addTrophyReset: (baseId: string) => `${API_BASE}/bases/${baseId}/trophy-resets`, updateDefense: (defenseId: string) => `${API_BASE}/defenses/${defenseId}`, deleteDefense: (defenseId: string) => `${API_BASE}/defenses/${defenseId}`, profiles: `${API_BASE}/profiles`, @@ -104,6 +105,13 @@ type ProfileDefense = { armyCategoryName: string; }; +type TrophyReset = { + id: string; + trophiesAtStart: number; + trophiesLost: number; + createdAt: string; +}; + type ProfileBase = { id: string; title: string; @@ -114,6 +122,7 @@ type ProfileBase = { createdAt: string; summary: Summary; defenses: ProfileDefense[]; + trophyResets: TrophyReset[]; }; type ProfileCategorySummary = Summary & { @@ -165,6 +174,7 @@ type ErrorState = { category: string; base: string; defense: string; + trophyReset: string; }; async function request(method: string, url: string, body?: any, options?: { isForm?: boolean }) { @@ -197,6 +207,7 @@ const initialErrors: ErrorState = { category: '', base: '', defense: '', + trophyReset: '', }; export default function Page() { @@ -300,7 +311,7 @@ export default function Page() { profileSelectedBase.defenses.forEach((defense) => { const key = defense.armyCategoryId || defense.armyCategoryName; if (!buckets.has(key)) { - buckets.set(key, { name: defense.armyCategoryName || 'Unknown Army', items: [] }); + buckets.set(key, { name: defense.armyCategoryName || '(No category)', items: [] }); } buckets.get(key)!.items.push(defense); }); @@ -615,6 +626,24 @@ export default function Page() { } } + async function handleTrophyResetSubmit(event: FormEvent) { + event.preventDefault(); + if (!profileSelectedBase) { + return; + } + const form = event.currentTarget; + const formData = new FormData(form); + const payload = Object.fromEntries(formData.entries()); + try { + setErrors((prev) => ({ ...prev, trophyReset: '' })); + await request('POST', API.addTrophyReset(profileSelectedBase.id), payload); + form.reset(); + await refreshOwnProfileDetail(); + } catch (error: any) { + setErrors((prev) => ({ ...prev, trophyReset: error.message })); + } + } + async function handleLogout() { try { await request('POST', API.logout); @@ -985,7 +1014,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { {defenses.length ? ( defenses.map((defense) => { const date = new Date(defense.createdAt); - const categoryName = categoryNameMap.get(defense.armyCategoryId) || 'Unknown Army'; + const categoryName = categoryNameMap.get(defense.armyCategoryId) || '(No category)'; return (
  • @@ -1263,9 +1292,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
  • @@ -1370,7 +1399,8 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
  • @@ -1626,6 +1656,43 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { )}
    +
    +

    Legend League Day Reset

    +
    + + + +

    {errors.trophyReset}

    +
    +
    +

    Recent Resets

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

    Defenses

      @@ -1633,7 +1700,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { profileSelectedBase.defenses.map((defense) => (
    • - {defense.armyCategoryName || 'Unknown Army'} + {defense.armyCategoryName || '(No category)'}
      {defense.stars}★ • {defense.percent}% • {formatTrophies(defense.trophies)}
      -- 2.45.3 From dcb0090196bc94f8eddb5e05ad82074ebf3ce35d Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 13:44:25 +0200 Subject: [PATCH 02/10] goofy --- docker-compose.backend.yml | 4 ---- frontend/app/page.tsx | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml index 4292396..f847594 100644 --- a/docker-compose.backend.yml +++ b/docker-compose.backend.yml @@ -9,10 +9,6 @@ services: COOKIE_SECURE: "true" volumes: - backend-uploads:/app/uploads - ports: - - "4000:4000" - expose: - - "4000" labels: - traefik.enable=true - traefik.http.routers.backend.rule=Host(`backend.basetracker.lona-development.org`) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 16eea77..fe40b2b 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -5,7 +5,7 @@ import { FormEvent, useEffect, useMemo, useRef, useState } from 'react'; const defaultApiBase = process.env.NODE_ENV === 'production' ? 'https://backend.basetracker.lona-development.org' - : 'http://localhost:4100'; + : 'https://backend.dev.basetracker.lona-development.org'; // Normalize backend URL so production always uses HTTPS while keeping local HTTP. function normalizeApiBase(url: string) { -- 2.45.3 From f6f5e474a60c8e135aa2860844f9c935ffd8408b Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 13:46:37 +0200 Subject: [PATCH 03/10] good --- docker-compose.frontend.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.frontend.yml b/docker-compose.frontend.yml index c1077ce..95690ba 100644 --- a/docker-compose.frontend.yml +++ b/docker-compose.frontend.yml @@ -6,10 +6,6 @@ services: NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-https://backend.basetracker.lona-development.org} environment: NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-https://backend.basetracker.lona-development.org} - ports: - - "3100:3000" - expose: - - "31000" labels: - traefik.enable=true - traefik.http.routers.frontend.rule=Host(`basetracker.lona-development.org`) -- 2.45.3 From a7d32bd4c1551150c2ef72d03ae3fe4e982c93f9 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 13:53:40 +0200 Subject: [PATCH 04/10] w --- docker-compose.backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml index f847594..17f7cdd 100644 --- a/docker-compose.backend.yml +++ b/docker-compose.backend.yml @@ -5,7 +5,7 @@ services: environment: DATABASE_URL: ${DATABASE_URL:?Set DATABASE_URL in Dokploy secrets} JWT_SECRET: ${JWT_SECRET:?Set JWT_SECRET in Dokploy secrets} - FRONTEND_ORIGIN: https://basetracker.lona-development.org + FRONTEND_ORIGINS: https://basetracker.lona-development.org,https://dev.basetracker.lona-development.org COOKIE_SECURE: "true" volumes: - backend-uploads:/app/uploads -- 2.45.3 From aac31071bfe484dab1c3f63aae1786e00cb09bfc Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 13:57:00 +0200 Subject: [PATCH 05/10] serviceWorker --- frontend/app/service-worker-provider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/app/service-worker-provider.tsx b/frontend/app/service-worker-provider.tsx index ff571a0..93fb01a 100644 --- a/frontend/app/service-worker-provider.tsx +++ b/frontend/app/service-worker-provider.tsx @@ -28,7 +28,9 @@ export function ServiceWorkerProvider() { } }; - if (process.env.NODE_ENV === 'production') { + const isProduction = window.location.hostname === 'basetracker.lona-development.org'; + + if (isProduction) { registerServiceWorker(); } else { navigator.serviceWorker.getRegistration(SW_PATH).then((registration) => { -- 2.45.3 From 650acd8a72dd5db82b14c6d8e1ea660441445c5b Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 14:06:49 +0200 Subject: [PATCH 06/10] resets --- backend/prisma/schema.prisma | 14 +++-- backend/src/server.js | 27 ++++++++- frontend/app/page.tsx | 107 +++++++++++++++++++---------------- 3 files changed, 93 insertions(+), 55 deletions(-) diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 4195f09..cb2924b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -43,12 +43,14 @@ model Base { } model TrophyReset { - id String @id @default(cuid()) - trophiesAtStart Int - trophiesLost Int - createdAt DateTime @default(now()) - base Base @relation(fields: [baseId], references: [id]) - baseId String + id String @id @default(cuid()) + date DateTime + trophiesAtStart Int + trophiesLost Int + numberOfDefenses Int + createdAt DateTime @default(now()) + base Base @relation(fields: [baseId], references: [id]) + baseId String } model Defense { diff --git a/backend/src/server.js b/backend/src/server.js index e2e7240..e29d6e5 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -271,6 +271,11 @@ app.get('/bases', requireAuth, async (req, res) => { const bases = await prisma.base.findMany({ where: { userId: req.user.id }, orderBy: { createdAt: 'desc' }, + include: { + trophyResets: { + orderBy: { date: 'desc' }, + }, + }, }); res.json({ bases: bases.map((base) => ({ @@ -281,6 +286,14 @@ app.get('/bases', requireAuth, async (req, res) => { imageUrl: buildImageUrl(base), isPrivate: base.isPrivate, createdAt: base.createdAt, + trophyResets: base.trophyResets.map((reset) => ({ + id: reset.id, + date: reset.date, + trophiesAtStart: reset.trophiesAtStart, + trophiesLost: reset.trophiesLost, + numberOfDefenses: reset.numberOfDefenses, + createdAt: reset.createdAt, + })), })), }); }); @@ -483,17 +496,25 @@ app.delete('/bases/:baseId', requireAuth, async (req, res) => { app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => { try { const { baseId } = req.params; - const { trophiesAtStart, trophiesLost } = req.body || {}; + const { date, trophiesAtStart, trophiesLost, numberOfDefenses } = 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' }); + } const base = await prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } }); if (!base) { @@ -503,8 +524,10 @@ app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => { await prisma.trophyReset.create({ data: { baseId: base.id, + date: parsedDate, trophiesAtStart: parsedTrophiesAtStart, trophiesLost: parsedTrophiesLost, + numberOfDefenses: parsedNumberOfDefenses, }, }); @@ -874,8 +897,10 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { })); const trophyResets = base.trophyResets.map((reset) => ({ id: reset.id, + date: reset.date, trophiesAtStart: reset.trophiesAtStart, trophiesLost: reset.trophiesLost, + numberOfDefenses: reset.numberOfDefenses, createdAt: reset.createdAt, })); return { diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index fe40b2b..8accd48 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -65,6 +65,15 @@ type ArmyCategory = { createdAt: string; }; +type TrophyReset = { + id: string; + date: string; + trophiesAtStart: number; + trophiesLost: number; + numberOfDefenses: number; + createdAt: string; +}; + type BaseItem = { id: string; title: string; @@ -73,6 +82,7 @@ type BaseItem = { imageUrl: string; isPrivate: boolean; createdAt: string; + trophyResets: TrophyReset[]; }; type DefenseItem = { @@ -105,13 +115,6 @@ type ProfileDefense = { armyCategoryName: string; }; -type TrophyReset = { - id: string; - trophiesAtStart: number; - trophiesLost: number; - createdAt: string; -}; - type ProfileBase = { id: string; title: string; @@ -122,7 +125,6 @@ type ProfileBase = { createdAt: string; summary: Summary; defenses: ProfileDefense[]; - trophyResets: TrophyReset[]; }; type ProfileCategorySummary = Summary & { @@ -628,7 +630,7 @@ export default function Page() { async function handleTrophyResetSubmit(event: FormEvent) { event.preventDefault(); - if (!profileSelectedBase) { + if (!selectedBaseId) { return; } const form = event.currentTarget; @@ -636,9 +638,9 @@ export default function Page() { const payload = Object.fromEntries(formData.entries()); try { setErrors((prev) => ({ ...prev, trophyReset: '' })); - await request('POST', API.addTrophyReset(profileSelectedBase.id), payload); + await request('POST', API.addTrophyReset(selectedBaseId), payload); form.reset(); - await refreshOwnProfileDetail(); + await refreshData(); } catch (error: any) { setErrors((prev) => ({ ...prev, trophyReset: error.message })); } @@ -1542,6 +1544,52 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { )}
    +
    +

    Legend League Day Reset

    +
    + + + + + +

    {errors.trophyReset}

    +
    +
    +

    Recent Resets

    +
      + {baseDetailMeta?.trophyResets?.length ? ( + baseDetailMeta.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

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

    Legend League Day Reset

    -
    - - - -

    {errors.trophyReset}

    -
    -
    -

    Recent Resets

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

    Defenses

      -- 2.45.3 From 88403c10170cc5b7e4ae07facded9980b79b4ff5 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 14:18:28 +0200 Subject: [PATCH 07/10] wwww --- backend/src/server.js | 75 ++++++++++++ frontend/app/page.tsx | 267 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 309 insertions(+), 33 deletions(-) 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

      -- 2.45.3 From 59c865632887c0caf0d59f9b2f27ba18c6e611f0 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 14:27:40 +0200 Subject: [PATCH 08/10] REEEEEEEEEE --- frontend/app/page.tsx | 67 ++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 7c4234d..0f354cd 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1114,8 +1114,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

      -

      Existing Categories

      -
        +
        +

        Existing Categories

        +
          {categories.length ? ( categories.map((category) => (
        • @@ -1198,8 +1199,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

          -

          Manage Bases

          -
            +
            +

            Manage Bases

            +
              {bases.length ? ( bases.map((base) => (
            • @@ -1391,8 +1393,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

              -

              Manage Attacks

              -
                +
                +

                Manage Attacks

                +
                  {defenses.length ? ( defenses.slice(0, 10).map((defense) => { const categoryName = @@ -1704,8 +1707,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
          -

          Army Categories vs This Base

          -
            +
            +

            Army Categories vs This Base

            +
              {baseDetail && baseDetail.categories.length ? ( baseDetail.categories.map((category) => (
          -

          Defenses

          -
            +
            +

            Defenses

            +
              {defenses.filter((defense) => defense.baseId === selectedBaseId).length ? ( defenses .filter((defense) => defense.baseId === selectedBaseId) @@ -1892,7 +1897,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
              {new Date(reset.date).toLocaleDateString()}
              - {reset.trophiesAtStart} trophies at start + {reset.trophiesAtStart} trophies {formatTrophies(reset.trophiesLost)} lost {reset.numberOfDefenses} defenses
              @@ -1906,26 +1911,28 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
          -

          Defenses

          -
            - {profileSelectedBase.defenses.length ? ( - profileSelectedBase.defenses.map((defense) => ( -
          • -
            - {defense.armyCategoryName || '(No category)'} -
            - {defense.stars}★ • {defense.percent}% • {formatTrophies(defense.trophies)} +
            +

            Defenses

            +
              + {profileSelectedBase.defenses.length ? ( + profileSelectedBase.defenses.map((defense) => ( +
            • +
              + {defense.armyCategoryName || '(No category)'} +
              + {defense.stars}★ • {defense.percent}% • {formatTrophies(defense.trophies)} +
              -
            -
            - {new Date(defense.createdAt).toLocaleString()} -
            -
          • - )) - ) : ( -
          • No defenses recorded yet.
          • - )} -
          +
          + {new Date(defense.createdAt).toLocaleString()} +
          +
        • + )) + ) : ( +
        • No defenses recorded yet.
        • + )} +
        +
      ) : ( -- 2.45.3 From e59f917c46933bf474f137acf962a8df5957f113 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 14:35:50 +0200 Subject: [PATCH 09/10] RAAAAAAAAAAAAAAAAAAAH --- frontend/app/page.tsx | 292 +++++++++++++++++++++--------------------- 1 file changed, 149 insertions(+), 143 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 0f354cd..f3f15d9 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -751,7 +751,8 @@ export default function Page() { }, [categories]); function formatTrophies(value: number) { - const sign = value > 0 ? '+' : ''; + const sign = value > 0 ? '' : '+'; + return `${sign}${value} trophies`; } @@ -1117,30 +1118,31 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

      Existing Categories

        - {categories.length ? ( - categories.map((category) => ( -
      • -
        - {category.name} -
        - + {categories.length ? ( + categories.map((category) => ( +
      • +
        + {category.name} +
        + +
        -
    -
    - {new Date(category.createdAt).toLocaleDateString()} -
    -
  • - )) - ) : ( -
  • No categories yet.
  • - )} - +
    + {new Date(category.createdAt).toLocaleDateString()} +
    + + )) + ) : ( +
  • No categories yet.
  • + )} + +
    @@ -1202,40 +1204,41 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

    Manage Bases

      - {bases.length ? ( - bases.map((base) => ( -
    • -
      -
      - {base.title}{' '} - {base.isPrivate ? Private : null} + {bases.length ? ( + bases.map((base) => ( +
    • +
      +
      + {base.title}{' '} + {base.isPrivate ? Private : null} +
      +
      + + +
      - - + {new Date(base.createdAt).toLocaleDateString()}
      -
    -
    - {new Date(base.createdAt).toLocaleDateString()} -
    - - )) - ) : ( -
  • No bases yet.
  • - )} - + + )) + ) : ( +
  • No bases yet.
  • + )} + + {baseBeingEdited && (
    @@ -1396,50 +1399,51 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

    Manage Attacks

      - {defenses.length ? ( - defenses.slice(0, 10).map((defense) => { - const categoryName = - categoryNameMap.get(defense.armyCategoryId) || defense.categoryName || '(No category)'; - return ( -
    • -
      -
      - {defense.baseTitle}{' '} - {categoryName} + {defenses.length ? ( + defenses.slice(0, 10).map((defense) => { + const categoryName = + categoryNameMap.get(defense.armyCategoryId) || defense.categoryName || '(No category)'; + return ( +
    • +
      +
      + {defense.baseTitle}{' '} + {categoryName} +
      +
      + + +
      - - + {defense.stars}★ + {defense.percent}% + {formatTrophies(defense.trophies)} + {new Date(defense.createdAt).toLocaleString()}
      -
    -
    - {defense.stars}★ - {defense.percent}% - {formatTrophies(defense.trophies)} - {new Date(defense.createdAt).toLocaleString()} -
    - - ); - }) - ) : ( -
  • No attacks logged yet.
  • - )} - - {defenses.length > 10 ? ( -

    Showing the latest 10 entries.

    - ) : null} + + ); + }) + ) : ( +
  • No attacks logged yet.
  • + )} + + {defenses.length > 10 ? ( +

    Showing the latest 10 entries.

    + ) : null} + {defenseBeingEdited && (
    @@ -1710,28 +1714,29 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

    Army Categories vs This Base

      - {baseDetail && baseDetail.categories.length ? ( - baseDetail.categories.map((category) => ( -
    • openCategoryDetail(category.categoryId)} - > -
      - {category.name} - {category.count} attacks -
      -
      - {category.averageStars}★ avg - {category.averagePercent}% avg - {formatTrophies(category.averageTrophies)} avg -
      -
    • - )) - ) : ( -
    • No army categories have attacked this base yet.
    • - )} -
    + {baseDetail && baseDetail.categories.length ? ( + baseDetail.categories.map((category) => ( +
  • openCategoryDetail(category.categoryId)} + > +
    + {category.name} + {category.count} attacks +
    +
    + {category.averageStars}★ avg + {category.averagePercent}% avg + {formatTrophies(category.averageTrophies)} avg +
    +
  • + )) + ) : ( +
  • No army categories have attacked this base yet.
  • + )} + +
    @@ -1776,30 +1781,31 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

    Defenses

      - {defenses.filter((defense) => defense.baseId === selectedBaseId).length ? ( - defenses - .filter((defense) => defense.baseId === selectedBaseId) - .map((defense) => { - const date = new Date(defense.createdAt); - const categoryName = categoryNameMap.get(defense.armyCategoryId) || '(No category)'; - return ( -
    • -
      - {categoryName} -
      - {defense.stars}★ • {defense.percent}% • {formatTrophies(defense.trophies)} + {defenses.filter((defense) => defense.baseId === selectedBaseId).length ? ( + defenses + .filter((defense) => defense.baseId === selectedBaseId) + .map((defense) => { + const date = new Date(defense.createdAt); + const categoryName = categoryNameMap.get(defense.armyCategoryId) || '(No category)'; + return ( +
    • +
      + {categoryName} +
      + {defense.stars}★ • {defense.percent}% • {formatTrophies(defense.trophies)} +
      -
    -
    - {date.toLocaleString()} -
    - - ); - }) - ) : ( -
  • No defenses recorded for this base yet.
  • - )} - +
    + {date.toLocaleString()} +
    + + ); + }) + ) : ( +
  • No defenses recorded for this base yet.
  • + )} + + @@ -1888,7 +1894,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
    -
    +

    Recent Resets

      {profileSelectedBase.trophyResets.length ? ( @@ -1897,7 +1903,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
      {new Date(reset.date).toLocaleDateString()}
      - {reset.trophiesAtStart} trophies + {reset.trophiesAtStart} trophies at start {formatTrophies(reset.trophiesLost)} lost {reset.numberOfDefenses} defenses
      -- 2.45.3 From f01d77fde7f70f54611158ae56e8096b14c19ef6 Mon Sep 17 00:00:00 2001 From: Hymmel Date: Wed, 15 Oct 2025 14:38:14 +0200 Subject: [PATCH 10/10] a --- frontend/app/page.tsx | 188 +++++++++++++++++++++--------------------- 1 file changed, 95 insertions(+), 93 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index f3f15d9..a1de63a 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -752,8 +752,10 @@ export default function Page() { function formatTrophies(value: number) { const sign = value > 0 ? '' : '+'; - - return `${sign}${value} trophies`; + let gained = false; + if(sign === '+') gained = true; + let suffix = gained ? 'gained' : 'lost'; + return `${sign}${value} trophies ${suffix}`; } function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { @@ -1399,50 +1401,50 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

      Manage Attacks

        - {defenses.length ? ( - defenses.slice(0, 10).map((defense) => { - const categoryName = - categoryNameMap.get(defense.armyCategoryId) || defense.categoryName || '(No category)'; - return ( -
      • -
        -
        - {defense.baseTitle}{' '} - {categoryName} -
        -
        - - -
        + {defenses.length ? ( + defenses.slice(0, 10).map((defense) => { + const categoryName = + categoryNameMap.get(defense.armyCategoryId) || defense.categoryName || '(No category)'; + return ( +
      • +
        +
        + {defense.baseTitle}{' '} + {categoryName}
        - {defense.stars}★ - {defense.percent}% - {formatTrophies(defense.trophies)} - {new Date(defense.createdAt).toLocaleString()} + +
        -
      • - ); - }) - ) : ( -
      • No attacks logged yet.
      • - )} -
      - {defenses.length > 10 ? ( -

      Showing the latest 10 entries.

      - ) : null} +
      +
      + {defense.stars}★ + {defense.percent}% + {formatTrophies(defense.trophies)} + {new Date(defense.createdAt).toLocaleString()} +
      + + ); + }) + ) : ( +
    • No attacks logged yet.
    • + )} +
    + {defenses.length > 10 ? ( +

    Showing the latest 10 entries.

    + ) : null}
    {defenseBeingEdited && ( @@ -1555,7 +1557,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {