Heats.jsx (6747B)
1 import { lazy, useEffect, useState } from 'react' 2 import { generatePath, Link } from 'react-router-dom' 3 import { exportHeatsToCSV } from './utils' 4 5 const api_uri = import.meta.env.VITE_API_URI 6 const api_port = import.meta.env.VITE_API_PORT 7 const locale = import.meta.env.VITE_LOCALE 8 9 const Auth = lazy(() => import('./Auth')) 10 11 async function changeVisibility(e, heatId, heatName, session) { 12 e.preventDefault() 13 14 if (window.confirm('Change visibility of heat "' + heatName + '"?')) { 15 const { data, error } = await toggleVisibility(heatId, session) 16 if (error === undefined) { 17 window.location.reload() 18 } else { 19 console.error(error) 20 } 21 } 22 } 23 24 async function toggleVisibility(heatId, session) { 25 try { 26 const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/toggleHeatVisibility`, { 27 method: 'POST', 28 headers: { 29 'Content-Type': 'application/json', 30 'Authorization': `Bearer ${session.auth.token}`, 31 }, 32 body: JSON.stringify({ 33 "heat_id": heatId 34 }), 35 }) 36 const { data, error } = await res.json() 37 return data 38 } catch (error) { 39 throw(error) 40 } 41 } 42 43 export async function addNewHeat(name, heatLocation, plannedStart, privateHeat, session) { 44 try { 45 const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/newHeat`, { 46 method: 'POST', 47 headers: { 48 'Content-Type': 'application/json', 49 'Authorization': `Bearer ${session.auth.token}`, 50 }, 51 body: JSON.stringify({ 52 "name": name, 53 "location": heatLocation, 54 "planned_start": plannedStart, 55 "private": privateHeat 56 }), 57 }) 58 const { data, error } = await res.json() 59 return data 60 } catch (error) { 61 throw(error) 62 } 63 } 64 65 async function addHeat(e, session) { 66 e.preventDefault() 67 68 // Read the form data 69 const formData = new FormData(e.target); 70 const formJson = Object.fromEntries(formData.entries()); 71 72 // create new heat 73 try { 74 const heat = await addNewHeat( 75 formJson.name, 76 formJson.location, 77 // planned_start is an empty string if unset 78 // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time 79 formJson.planned_start === '' ? null : formJson.planned_start, 80 // the default checkbox value is 'on' 81 // https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/checkbox 82 formJson.private === 'on' ? true : false, 83 session 84 ) 85 window.location.reload() 86 } catch (error) { 87 console.error('Failed to create new heat: ' + error.message) 88 } 89 } 90 91 export async function removeHeat(heatId, session) { 92 const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/removeHeat`, { 93 method: 'POST', 94 headers: { 95 'Content-Type': 'application/json', 96 'Authorization': `Bearer ${session.auth.token}`, 97 }, 98 body: JSON.stringify({ 99 "heat_id": heatId 100 }), 101 }) 102 return await res.json() 103 } 104 105 async function deleteHeat(e, heatId, heatName, session) { 106 e.preventDefault() 107 108 if (window.confirm('Do you really want to delete heat "' + heatName + '"?')) { 109 const { data, error } = await removeHeat(heatId, session) 110 if (error === undefined) { 111 window.location.reload() 112 } else { 113 console.error(error) 114 } 115 } 116 } 117 118 // export heats 119 function ExportForm(heats) { 120 return ( 121 <div className='exportForm'> 122 <form method='post' onSubmit={e => exportHeatsToCSV(e, heats)}> 123 <button type='submit'>▿ export</button> 124 </form> 125 </div> 126 ) 127 } 128 129 function HeatForm({session}) { 130 const [loading, setLoading] = useState(false) 131 const [privateHeatChecked, setPrivateHeatChecked] = useState(true); 132 const [heats, setHeats] = useState([]) 133 const dateOptions = { 134 year: "numeric", 135 month: "2-digit", 136 day: "2-digit", 137 } 138 139 useEffect(() => { 140 (async () => { 141 setLoading(true) 142 const res = await fetch(`${api_uri}:${api_port}/v1/leaderboard/allHeats`, { 143 headers: { 144 'Content-Type': 'application/json', 145 'Authorization': `Bearer ${session.auth.token}`, 146 } 147 }) 148 const { data, error } = await res.json() 149 if (error) { 150 console.error(error) 151 } 152 setHeats(data) 153 setLoading(false) 154 })(); 155 }, []) 156 157 return ( 158 <div className='HeatForm'> 159 <button disabled={!loading} className='loading'>↺ loading</button> 160 <form method='post' onSubmit={e => addHeat(e, session)}> 161 <table> 162 <thead> 163 <tr> 164 <th>Created at</th> 165 <th>Name *</th> 166 <th>Location</th> 167 <th>Planned start</th> 168 <th>Private</th> 169 <th>New/delete</th> 170 </tr> 171 </thead> 172 <tbody> 173 {heats.map(h => ( 174 <tr key={h.id}> 175 <td data-title='Created at' className='right'>{new Date(h.created_at).toLocaleDateString(locale, dateOptions)}</td> 176 <td data-title='Name'><Link to={generatePath('/heats/startlist/:heatId', {heatId:h.id})}>{h.name}</Link></td> 177 <td data-title='Location'>{h.location}</td> 178 <td data-title='Planned start'>{h.planned_start}</td> 179 <td data-title='Private' className='inline-block'><input type='checkbox' checked={h.private} onChange={e => changeVisibility(e, h.id, h.name, session)} /></td> 180 <td><button onClick={e => deleteHeat(e, h.id, h.name, session)}>– del</button></td> 181 </tr> 182 ))} 183 <tr className='input'> 184 <td className='right'><i>* required</i></td> 185 <td data-title='Name *'> 186 <input type='text' name='name' /> 187 </td> 188 <td data-title='Location'> 189 <input type='text' name='location' /> 190 </td> 191 <td data-title='Planned start'> 192 <input 193 type='time' 194 name='planned_start' /> 195 </td> 196 <td data-title='Private' className='inline-block'> 197 <input 198 type='checkbox' 199 name='private' 200 checked={privateHeatChecked} 201 onChange={(e) => setPrivateHeatChecked(e.target.checked)} /> 202 </td> 203 <td> 204 <button type='submit'>+ new</button> 205 </td> 206 </tr> 207 </tbody> 208 </table> 209 </form> 210 <ExportForm heats={heats} /> 211 </div> 212 ) 213 } 214 215 function Heats({session}) { 216 return ( 217 <div> 218 {!session.auth ? <Auth /> : <HeatForm session={session} />} 219 </div> 220 ) 221 } 222 223 export default Heats;