This commit is contained in:
Hymmel 2025-10-15 14:06:49 +02:00
parent aac31071bf
commit 650acd8a72
3 changed files with 93 additions and 55 deletions

View file

@ -44,8 +44,10 @@ model Base {
model TrophyReset { model TrophyReset {
id String @id @default(cuid()) id String @id @default(cuid())
date DateTime
trophiesAtStart Int trophiesAtStart Int
trophiesLost Int trophiesLost Int
numberOfDefenses Int
createdAt DateTime @default(now()) createdAt DateTime @default(now())
base Base @relation(fields: [baseId], references: [id]) base Base @relation(fields: [baseId], references: [id])
baseId String baseId String

View file

@ -271,6 +271,11 @@ app.get('/bases', requireAuth, async (req, res) => {
const bases = await prisma.base.findMany({ const bases = await prisma.base.findMany({
where: { userId: req.user.id }, where: { userId: req.user.id },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
include: {
trophyResets: {
orderBy: { date: 'desc' },
},
},
}); });
res.json({ res.json({
bases: bases.map((base) => ({ bases: bases.map((base) => ({
@ -281,6 +286,14 @@ app.get('/bases', requireAuth, async (req, res) => {
imageUrl: buildImageUrl(base), imageUrl: buildImageUrl(base),
isPrivate: base.isPrivate, isPrivate: base.isPrivate,
createdAt: base.createdAt, 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) => { app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => {
try { try {
const { baseId } = req.params; 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 parsedTrophiesAtStart = Number(trophiesAtStart);
const parsedTrophiesLost = Number(trophiesLost); 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) { if (!Number.isFinite(parsedTrophiesAtStart) || parsedTrophiesAtStart < 0) {
return res.status(400).json({ error: 'Trophies at start must be a positive number' }); return res.status(400).json({ error: 'Trophies at start must be a positive number' });
} }
if (!Number.isFinite(parsedTrophiesLost)) { if (!Number.isFinite(parsedTrophiesLost)) {
return res.status(400).json({ error: 'Trophies lost must be a number' }); 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 } }); const base = await prisma.base.findFirst({ where: { id: baseId, userId: req.user.id } });
if (!base) { if (!base) {
@ -503,8 +524,10 @@ app.post('/bases/:baseId/trophy-resets', requireAuth, async (req, res) => {
await prisma.trophyReset.create({ await prisma.trophyReset.create({
data: { data: {
baseId: base.id, baseId: base.id,
date: parsedDate,
trophiesAtStart: parsedTrophiesAtStart, trophiesAtStart: parsedTrophiesAtStart,
trophiesLost: parsedTrophiesLost, trophiesLost: parsedTrophiesLost,
numberOfDefenses: parsedNumberOfDefenses,
}, },
}); });
@ -874,8 +897,10 @@ app.get('/profiles/:username', requireAuth, async (req, res) => {
})); }));
const trophyResets = base.trophyResets.map((reset) => ({ const trophyResets = base.trophyResets.map((reset) => ({
id: reset.id, id: reset.id,
date: reset.date,
trophiesAtStart: reset.trophiesAtStart, trophiesAtStart: reset.trophiesAtStart,
trophiesLost: reset.trophiesLost, trophiesLost: reset.trophiesLost,
numberOfDefenses: reset.numberOfDefenses,
createdAt: reset.createdAt, createdAt: reset.createdAt,
})); }));
return { return {

View file

@ -65,6 +65,15 @@ type ArmyCategory = {
createdAt: string; createdAt: string;
}; };
type TrophyReset = {
id: string;
date: string;
trophiesAtStart: number;
trophiesLost: number;
numberOfDefenses: number;
createdAt: string;
};
type BaseItem = { type BaseItem = {
id: string; id: string;
title: string; title: string;
@ -73,6 +82,7 @@ type BaseItem = {
imageUrl: string; imageUrl: string;
isPrivate: boolean; isPrivate: boolean;
createdAt: string; createdAt: string;
trophyResets: TrophyReset[];
}; };
type DefenseItem = { type DefenseItem = {
@ -105,13 +115,6 @@ type ProfileDefense = {
armyCategoryName: string; armyCategoryName: string;
}; };
type TrophyReset = {
id: string;
trophiesAtStart: number;
trophiesLost: number;
createdAt: string;
};
type ProfileBase = { type ProfileBase = {
id: string; id: string;
title: string; title: string;
@ -122,7 +125,6 @@ type ProfileBase = {
createdAt: string; createdAt: string;
summary: Summary; summary: Summary;
defenses: ProfileDefense[]; defenses: ProfileDefense[];
trophyResets: TrophyReset[];
}; };
type ProfileCategorySummary = Summary & { type ProfileCategorySummary = Summary & {
@ -628,7 +630,7 @@ export default function Page() {
async function handleTrophyResetSubmit(event: FormEvent<HTMLFormElement>) { async function handleTrophyResetSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault(); event.preventDefault();
if (!profileSelectedBase) { if (!selectedBaseId) {
return; return;
} }
const form = event.currentTarget; const form = event.currentTarget;
@ -636,9 +638,9 @@ export default function Page() {
const payload = Object.fromEntries(formData.entries()); const payload = Object.fromEntries(formData.entries());
try { try {
setErrors((prev) => ({ ...prev, trophyReset: '' })); setErrors((prev) => ({ ...prev, trophyReset: '' }));
await request('POST', API.addTrophyReset(profileSelectedBase.id), payload); await request('POST', API.addTrophyReset(selectedBaseId), payload);
form.reset(); form.reset();
await refreshOwnProfileDetail(); await refreshData();
} catch (error: any) { } catch (error: any) {
setErrors((prev) => ({ ...prev, trophyReset: error.message })); setErrors((prev) => ({ ...prev, trophyReset: error.message }));
} }
@ -1542,6 +1544,52 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
)} )}
</ul> </ul>
</div> </div>
<div className="card">
<h3>Legend League Day Reset</h3>
<form className="form compact" onSubmit={handleTrophyResetSubmit}>
<label>
Date
<input type="date" name="date" required />
</label>
<label>
Trophies at Start
<input type="number" name="trophiesAtStart" min={0} step={1} required className="styled-number" />
</label>
<label>
Trophies Lost
<input type="number" name="trophiesLost" step={1} required className="styled-number" />
</label>
<label>
Number of Defenses
<input type="number" name="numberOfDefenses" min={0} step={1} required className="styled-number" />
</label>
<button type="submit" className="primary">
Add Reset
</button>
<p className="form-error">{errors.trophyReset}</p>
</form>
<div className="subsection">
<h4>Recent Resets</h4>
<ul className="list compact">
{baseDetailMeta?.trophyResets?.length ? (
baseDetailMeta.trophyResets.map((reset) => (
<li key={reset.id} className="list-item">
<div className="defense-header">
<span>{new Date(reset.date).toLocaleDateString()}</span>
<div className="defense-meta">
<span>{reset.trophiesAtStart} trophies at start</span>
<span>{formatTrophies(reset.trophiesLost)} lost</span>
<span>{reset.numberOfDefenses} defenses</span>
</div>
</div>
</li>
))
) : (
<li>No resets logged yet.</li>
)}
</ul>
</div>
</div>
<div className="card"> <div className="card">
<h3>Defenses</h3> <h3>Defenses</h3>
<ul id="base-detail-defenses" className="list"> <ul id="base-detail-defenses" className="list">
@ -1656,43 +1704,6 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
)} )}
</ul> </ul>
</div> </div>
<div className="card">
<h3>Legend League Day Reset</h3>
<form className="form compact" onSubmit={handleTrophyResetSubmit}>
<label>
Trophies at Start
<input type="number" name="trophiesAtStart" min={0} step={1} required className="styled-number" />
</label>
<label>
Trophies Lost
<input type="number" name="trophiesLost" step={1} required className="styled-number" />
</label>
<button type="submit" className="primary">
Add Reset
</button>
<p className="form-error">{errors.trophyReset}</p>
</form>
<div className="subsection">
<h4>Recent Resets</h4>
<ul className="list compact">
{profileSelectedBase.trophyResets.length ? (
profileSelectedBase.trophyResets.map((reset) => (
<li key={reset.id} className="list-item">
<div className="defense-header">
<span>{new Date(reset.createdAt).toLocaleDateString()}</span>
<div className="defense-meta">
<span>{reset.trophiesAtStart} trophies at start</span>
<span>{formatTrophies(reset.trophiesLost)} lost</span>
</div>
</div>
</li>
))
) : (
<li>No resets logged yet.</li>
)}
</ul>
</div>
</div>
<div className="card"> <div className="card">
<h3>Defenses</h3> <h3>Defenses</h3>
<ul className="list"> <ul className="list">