dev #1
					 3 changed files with 93 additions and 55 deletions
				
			
		|  | @ -43,12 +43,14 @@ model Base { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model TrophyReset { | model TrophyReset { | ||||||
|   id              String   @id @default(cuid()) |   id                String   @id @default(cuid()) | ||||||
|   trophiesAtStart Int |   date              DateTime | ||||||
|   trophiesLost    Int |   trophiesAtStart   Int | ||||||
|   createdAt       DateTime @default(now()) |   trophiesLost      Int | ||||||
|   base            Base     @relation(fields: [baseId], references: [id]) |   numberOfDefenses  Int | ||||||
|   baseId          String |   createdAt         DateTime @default(now()) | ||||||
|  |   base              Base     @relation(fields: [baseId], references: [id]) | ||||||
|  |   baseId            String | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model Defense { | model Defense { | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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"> | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue