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

@ -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 {

View file

@ -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 {

View file

@ -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<HTMLFormElement>) {
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 {
)}
</ul>
</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">
<h3>Defenses</h3>
<ul id="base-detail-defenses" className="list">
@ -1656,43 +1704,6 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
)}
</ul>
</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">
<h3>Defenses</h3>
<ul className="list">