dev #1
					 3 changed files with 93 additions and 55 deletions
				
			
		|  | @ -44,8 +44,10 @@ model Base { | |||
| 
 | ||||
| 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 | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
|  | @ -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"> | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue