diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9376b7e..cb2924b 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -39,6 +39,18 @@ model Base { user User @relation(fields: [userId], references: [id]) userId String defenses Defense[] + trophyResets TrophyReset[] +} + +model TrophyReset { + 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 { @@ -49,6 +61,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..b967572 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, + })), })), }); }); @@ -480,6 +493,51 @@ app.delete('/bases/:baseId', requireAuth, async (req, res) => { } }); +app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => { + try { + const { baseId } = req.params; + 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) { + return res.status(404).json({ error: 'Base not found' }); + } + + await prisma.trophyReset.create({ + data: { + baseId: base.id, + date: parsedDate, + trophiesAtStart: parsedTrophiesAtStart, + trophiesLost: parsedTrophiesLost, + numberOfDefenses: parsedNumberOfDefenses, + }, + }); + + 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 +547,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 +557,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, @@ -531,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; @@ -539,9 +672,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 +687,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 +763,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 +954,9 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { }, }, }, + trophyResets: { + orderBy: { createdAt: 'desc' }, + }, }, }); @@ -824,7 +968,15 @@ 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, + date: reset.date, + trophiesAtStart: reset.trophiesAtStart, + trophiesLost: reset.trophiesLost, + numberOfDefenses: reset.numberOfDefenses, + createdAt: reset.createdAt, })); return { id: base.id, @@ -836,6 +988,7 @@ app.get('/profiles/:username', requireAuth, async (req, res) => { createdAt: base.createdAt, summary: summarizeDefenses(defenses), defenses, + trophyResets, }; }); diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml index 4292396..17f7cdd 100644 --- a/docker-compose.backend.yml +++ b/docker-compose.backend.yml @@ -5,14 +5,10 @@ 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 - ports: - - "4000:4000" - expose: - - "4000" labels: - traefik.enable=true - traefik.http.routers.backend.rule=Host(`backend.basetracker.lona-development.org`) 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`) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 80ed021..a1de63a 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) { @@ -35,6 +35,9 @@ 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`, + 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`, @@ -64,6 +67,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; @@ -72,6 +84,7 @@ type BaseItem = { imageUrl: string; isPrivate: boolean; createdAt: string; + trophyResets: TrophyReset[]; }; type DefenseItem = { @@ -114,6 +127,7 @@ type ProfileBase = { createdAt: string; summary: Summary; defenses: ProfileDefense[]; + trophyResets: TrophyReset[]; }; type ProfileCategorySummary = Summary & { @@ -165,6 +179,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 +212,7 @@ const initialErrors: ErrorState = { category: '', base: '', defense: '', + trophyReset: '', }; export default function Page() { @@ -216,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); @@ -292,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[]; @@ -300,7 +323,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); }); @@ -502,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) { @@ -538,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(''); @@ -615,6 +683,26 @@ export default function Page() { } } + async function handleTrophyResetSubmit(event: FormEvent) { + event.preventDefault(); + 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(baseId), payload); + form.reset(); + await refreshData(); + } catch (error: any) { + setErrors((prev) => ({ ...prev, trophyReset: error.message })); + } + } + async function handleLogout() { try { await request('POST', API.logout); @@ -663,8 +751,11 @@ export default function Page() { }, [categories]); function formatTrophies(value: number) { - const sign = value > 0 ? '+' : ''; - return `${sign}${value} trophies`; + const sign = value > 0 ? '' : '+'; + let gained = false; + if(sign === '+') gained = true; + let suffix = gained ? 'gained' : 'lost'; + return `${sign}${value} trophies ${suffix}`; } function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary { @@ -985,7 +1076,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 (
  • @@ -1026,32 +1117,34 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {

    -

    Existing Categories

    -
      - {categories.length ? ( - categories.map((category) => ( -
    • -
      - {category.name} -
      - +
      +

      Existing Categories

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

    -

    Manage Bases

    -
      - {bases.length ? ( - bases.map((base) => ( -
    • -
      -
      - {base.title}{' '} - {base.isPrivate ? Private : null} +
      +

      Manage Bases

      +
        + {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 && (
    @@ -1263,9 +1358,9 @@ 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)} + {reset.numberOfDefenses} defenses +
      +
    • + )) + ) : ( +
    • No resets logged yet.
    • + )} +
    +
    +
    + {editingTrophyResetId && trophyResetBeingEdited && ( +
    +

    Edit Reset

    +
    + + + + + +
    + + +
    +

    {errors.trophyReset}

    +
    +
    + )} +
    @@ -1487,8 +1713,9 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
    -

    Army Categories vs This Base

    -
      +
      +

      Army Categories vs This Base

      +
        {baseDetail && baseDetail.categories.length ? ( baseDetail.categories.map((category) => (
      • No army categories have attacked this base yet.
      • )}
      +
    -

    Defenses

    -
      +
      +

      Recent Resets

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

    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) || 'Unknown Army'; + const categoryName = categoryNameMap.get(defense.armyCategoryId) || '(No category)'; return (
    • @@ -1539,6 +1807,7 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
    • No defenses recorded for this base yet.
    • )}
    +
    @@ -1627,26 +1896,51 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
    -

    Defenses

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

      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 +
        -
      -
      - {new Date(defense.createdAt).toLocaleString()} -
      -
    • - )) - ) : ( -
    • No defenses recorded yet.
    • - )} -
    + + )) + ) : ( +
  • No resets logged yet.
  • + )} + + +
    +
    +
    +

    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.
    • + )} +
    +
    ) : ( 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) => {