Live location tracking and playback for the game "manhunt"

Tweak layout for mobile, format

bwc9876.dev a90bc069 0c840d95

verified
+55 -46
-1
TODO.md
··· 1 1 # TODO 2 2 3 - - [ ] Start on game settings form 4 3 - [ ] Setup screen
+3 -3
frontend/src/components/LobbyScreen.tsx
··· 179 179 deco="seeker" 180 180 /> 181 181 <div className="frame"> 182 - <button onClick={onLeaveLobby} className="fab left"> 182 + <button onClick={onLeaveLobby} className="fab tl"> 183 183 <IconArrowBigLeftLinesFilled size="1.5em" /> 184 184 Leave 185 185 </button> 186 186 {lobbyState.is_host && ( 187 - <button onClick={onOpenSettings} className="fab center"> 187 + <button onClick={onOpenSettings} className="fab bl"> 188 188 <IconSettingsFilled size="1.5em" /> 189 189 Rules 190 190 </button> 191 191 )} 192 192 {lobbyState.is_host && ( 193 - <button disabled={!canStart} onClick={onStartGame} className="fab right"> 193 + <button disabled={!canStart} onClick={onStartGame} className="fab br"> 194 194 <IconFlagFilled size="1.5em" /> 195 195 Start 196 196 </button>
+14 -7
frontend/src/components/game-settings/GameSettingsModal.tsx
··· 19 19 const [pingInterval, setPingInterval] = useState(gameSettings.ping_minutes_interval); 20 20 21 21 const onSaveClick = () => { 22 - const newSettings = {...gameSettings, hiding_time_seconds: hidingTime, ping_start: pingStart, ping_minutes_interval: pingInterval}; 23 - onSave(newSettings); 22 + const newSettings = { 23 + ...gameSettings, 24 + hiding_time_seconds: hidingTime, 25 + ping_start: pingStart, 26 + ping_minutes_interval: pingInterval 27 + }; 28 + onSave(newSettings); 24 29 }; 25 30 26 31 const HiderIcon = iconForDecor("hider"); ··· 28 33 return ( 29 34 <div className="screen-cover"> 30 35 <dialog className="settings-popup"> 31 - <h2><IconSettingsFilled/> Game Rules</h2> 36 + <h2> 37 + <IconSettingsFilled /> Game Rules 38 + </h2> 32 39 <div> 33 - <SettingsSection icon={<HiderIcon/>} title="Hiding"> 40 + <SettingsSection icon={<HiderIcon />} title="Hiding"> 34 41 <SettingsAdmo> 35 42 Hiders will have a period of time to hide. 36 43 <br /> 37 44 After this, seekers are allowed to begin searching. 38 45 </SettingsAdmo> 39 46 <SliderField 40 - label="Hiding Time" 47 + label="Hide For" 41 48 displayValue={`${hidingTime / 60} minutes`} 42 49 value={hidingTime / 60} 43 50 min={2} ··· 47 54 }} 48 55 /> 49 56 </SettingsSection> 50 - <SettingsSection icon={<IconMapPinFilled/>} title="Pings"> 57 + <SettingsSection icon={<IconMapPinFilled />} title="Pings"> 51 58 <SettingsAdmo> 52 59 Pings are when hider locations are shared with everyone. 53 60 <br /> 54 61 They start after seekers are released and continue at an interval. 55 62 </SettingsAdmo> 56 63 <StartConditionField 57 - label="Pings Will Start" 64 + label="Pings Start" 58 65 value={pingStart} 59 66 onChange={(val) => setPingStart(val)} 60 67 />
+2 -5
frontend/src/components/game-settings/SettingsAdmo.tsx
··· 1 1 import { IconInfoCircleFilled } from "@tabler/icons-react"; 2 2 import React, { PropsWithChildren } from "react"; 3 3 4 - export default function SettingsAdmo({children}: PropsWithChildren<{}>) { 5 - return <p> 6 - {children} 7 - </p>; 4 + export default function SettingsAdmo({ children }: PropsWithChildren<{}>) { 5 + return <p>{children}</p>; 8 6 } 9 -
+1 -5
frontend/src/components/game-settings/SettingsField.tsx
··· 2 2 3 3 export type SettingsFieldProps = PropsWithChildren<{ label: string; displayValue: string }>; 4 4 5 - export default function SettingsField({ 6 - label, 7 - displayValue, 8 - children 9 - }: SettingsFieldProps) { 5 + export default function SettingsField({ label, displayValue, children }: SettingsFieldProps) { 10 6 return ( 11 7 <label> 12 8 <span>{label}</span>
+13 -7
frontend/src/components/game-settings/SettingsSection.tsx
··· 1 1 import React, { PropsWithChildren, ReactNode } from "react"; 2 2 3 - 4 - export default function SettingsSection({title, children, icon}: PropsWithChildren<{title: string, icon: ReactNode}>) { 5 - return <fieldset> 6 - <h3>{icon} {title}</h3> 7 - {children} 8 - </fieldset>; 3 + export default function SettingsSection({ 4 + title, 5 + children, 6 + icon 7 + }: PropsWithChildren<{ title: string; icon: ReactNode }>) { 8 + return ( 9 + <fieldset> 10 + <h3> 11 + {icon} {title} 12 + </h3> 13 + {children} 14 + </fieldset> 15 + ); 9 16 } 10 -
+1 -1
frontend/src/components/game-settings/SliderField.tsx
··· 1 1 import React, { HTMLProps } from "react"; 2 2 import SettingsField, { SettingsFieldProps } from "./SettingsField"; 3 3 4 - type Props = Omit<Omit<HTMLProps<HTMLInputElement>,"type">, "label"> & 4 + type Props = Omit<Omit<HTMLProps<HTMLInputElement>, "type">, "label"> & 5 5 Omit<SettingsFieldProps, "children">; 6 6 7 7 export default function SliderField({ label, displayValue, ...rest }: Props) {
+2 -2
frontend/src/components/game-settings/StartConditionField.tsx
··· 11 11 const startConditionOptions = { 12 12 instant: "Instantly", 13 13 minutes: "After _ Minutes", 14 - players: "After _ Players Caught" 14 + players: "After _ Caught" 15 15 }; 16 16 17 17 const startConditionDefaults: Record<keyof typeof startConditionOptions, PingStartCondition> = { ··· 98 98 ))} 99 99 </select> 100 100 </SettingsField> 101 - {(sliderVal !== null) && ( 101 + {sliderVal !== null && ( 102 102 <input 103 103 className="extra" 104 104 min={1}
+19 -15
frontend/src/style.css
··· 225 225 font-family: "Bungee"; 226 226 border-bottom: solid 2px #aaa; 227 227 display: flex; 228 - gap: .2em; 228 + gap: 0.2em; 229 229 align-items: center; 230 230 justify-content: center; 231 231 } ··· 235 235 font-family: "Bungee"; 236 236 margin: 0; 237 237 display: flex; 238 - gap: .2em; 238 + gap: 0.2em; 239 239 align-items: center; 240 240 } 241 241 ··· 273 273 border: none; 274 274 border-radius: 5px; 275 275 margin: 0; 276 - padding: var(--1); 276 + padding: 0 var(--1); 277 277 width: 100%; 278 278 height: 100%; 279 279 } 280 280 } 281 281 282 282 select { 283 - padding: .5em 0; 283 + padding: 0.5em 0; 284 284 } 285 285 286 286 input { ··· 421 421 align-items: center; 422 422 justify-content: center; 423 423 gap: 0.3em; 424 - font-size: 14pt; 424 + font-size: 12pt; 425 425 padding: var(--2); 426 426 position: absolute; 427 - bottom: var(--small); 428 427 429 428 &:disabled { 430 429 background-color: #999; ··· 432 431 box-shadow: 0 0 5px #4445 inset; 433 432 } 434 433 435 - &.left { 436 - left: var(--small); 434 + &.bl { 435 + left: 0.5em; 436 + bottom: var(--small); 437 437 } 438 438 439 - &.right { 440 - right: var(--small); 439 + &.br { 440 + right: 0.5em; 441 + bottom: var(--small); 441 442 } 442 443 443 - &.center { 444 - max-width: min-content; 445 - margin: auto; 446 - left: 0; 447 - right: 0; 444 + &.tr { 445 + right: 0.5em; 446 + top: var(--small); 447 + } 448 + 449 + &.tl { 450 + left: 0.5em; 451 + top: var(--small); 448 452 } 449 453 }