dev #1
2 changed files with 309 additions and 33 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string | null>(null);
|
||||
const [editingDefenseId, setEditingDefenseId] = useState<string | null>(null);
|
||||
const [editingTrophyResetId, setEditingTrophyResetId] = useState<string | null>(null);
|
||||
const [profileSearchTerm, setProfileSearchTerm] = useState('');
|
||||
const [profileResults, setProfileResults] = useState<ProfileSummaryItem[]>([]);
|
||||
const [profileDetail, setProfileDetail] = useState<ProfileDetail | null>(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<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!editingDefenseId || !defenseBeingEdited) {
|
||||
|
|
@ -551,6 +570,42 @@ export default function Page() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleTrophyResetEditSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
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<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setProfileError('');
|
||||
|
|
@ -630,15 +685,17 @@ export default function Page() {
|
|||
|
||||
async function handleTrophyResetSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
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 {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card">
|
||||
<h2>Log Legend League Day Reset</h2>
|
||||
<form id="trophy-reset-form" className="form compact" onSubmit={handleTrophyResetSubmit}>
|
||||
<label>
|
||||
Base
|
||||
<select name="baseId" required defaultValue="">
|
||||
<option value="" disabled>
|
||||
{bases.length ? 'Select a base' : 'Add a base first'}
|
||||
</option>
|
||||
{bases.map((base) => (
|
||||
<option key={base.id} value={base.id}>
|
||||
{base.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Date
|
||||
<input type="date" name="date" required />
|
||||
</label>
|
||||
<label>
|
||||
Trophies
|
||||
<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">
|
||||
Record Reset
|
||||
</button>
|
||||
<p className="form-error" data-for="trophyReset">
|
||||
{errors.trophyReset}
|
||||
</p>
|
||||
</form>
|
||||
<div className="subsection">
|
||||
<details>
|
||||
<summary><h3>Manage Resets</h3></summary>
|
||||
<ul className="list compact">
|
||||
{bases.flatMap((base) => base.trophyResets).length ? (
|
||||
bases.flatMap((base) => base.trophyResets).slice(0, 10).map((reset) => (
|
||||
<li key={reset.id} className="list-item">
|
||||
<div className="defense-header">
|
||||
<div>
|
||||
<strong>{bases.find((b) => b.id === bases.find((b) => b.trophyResets.some((r) => r.id === reset.id))?.id)?.title}</strong>{' '}
|
||||
<span className="badge">{new Date(reset.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<div className="defense-meta">
|
||||
<button
|
||||
type="button"
|
||||
className="ghost small"
|
||||
onClick={() => startEditingTrophyReset(reset.id)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost small"
|
||||
onClick={() => handleDeleteTrophyReset(reset.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="defense-meta">
|
||||
<span>{reset.trophiesAtStart} trophies</span>
|
||||
<span>{formatTrophies(reset.trophiesLost)} lost</span>
|
||||
<span>{reset.numberOfDefenses} defenses</span>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>No resets logged yet.</li>
|
||||
)}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
{editingTrophyResetId && trophyResetBeingEdited && (
|
||||
<div className="subsection">
|
||||
<h3>Edit Reset</h3>
|
||||
<form
|
||||
key={editingTrophyResetId}
|
||||
className="form compact"
|
||||
onSubmit={handleTrophyResetEditSubmit}
|
||||
>
|
||||
<label>
|
||||
Base
|
||||
<select name="baseId" required defaultValue={bases.find(b => b.trophyResets.some(r => r.id === editingTrophyResetId))?.id}>
|
||||
{bases.map((base) => (
|
||||
<option key={base.id} value={base.id}>
|
||||
{base.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Date
|
||||
<input type="date" name="date" required defaultValue={new Date(trophyResetBeingEdited.date).toISOString().split('T')[0]} />
|
||||
</label>
|
||||
<label>
|
||||
Trophies
|
||||
<input type="number" name="trophiesAtStart" min={0} step={1} required className="styled-number" defaultValue={trophyResetBeingEdited.trophiesAtStart} />
|
||||
</label>
|
||||
<label>
|
||||
Trophies Lost
|
||||
<input type="number" name="trophiesLost" step={1} required className="styled-number" defaultValue={trophyResetBeingEdited.trophiesLost} />
|
||||
</label>
|
||||
<label>
|
||||
Number of Defenses
|
||||
<input type="number" name="numberOfDefenses" min={0} step={1} required className="styled-number" defaultValue={trophyResetBeingEdited.numberOfDefenses} />
|
||||
</label>
|
||||
<div className="defense-meta">
|
||||
<button type="submit" className="primary">
|
||||
Save Reset
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={cancelEditingTrophyReset}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<p className="form-error">{errors.trophyReset}</p>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="base-detail-view" className={`view-section ${view !== 'baseDetail' ? 'hidden' : ''}`}>
|
||||
|
|
@ -1545,31 +1730,8 @@ 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>
|
||||
<details open>
|
||||
<summary><h3>Recent Resets</h3></summary>
|
||||
<ul className="list compact">
|
||||
{baseDetailMeta?.trophyResets?.length ? (
|
||||
baseDetailMeta.trophyResets.map((reset) => (
|
||||
|
|
@ -1577,18 +1739,34 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
|
|||
<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>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost small"
|
||||
onClick={() => startEditingTrophyReset(reset.id)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ghost small"
|
||||
onClick={() => handleDeleteTrophyReset(reset.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="defense-meta">
|
||||
<span>{reset.trophiesAtStart} trophies at start</span>
|
||||
<span>{formatTrophies(reset.trophiesLost)} lost</span>
|
||||
<span>{reset.numberOfDefenses} defenses</span>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li>No resets logged yet.</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3>Defenses</h3>
|
||||
|
|
@ -1704,6 +1882,29 @@ function summarizeProfileDefenses(defenses: ProfileDefense[]): Summary {
|
|||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="card">
|
||||
<details>
|
||||
<summary><h3>Recent Resets</h3></summary>
|
||||
<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.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>
|
||||
</details>
|
||||
</div>
|
||||
<div className="card">
|
||||
<h3>Defenses</h3>
|
||||
<ul className="list">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue