import { reactive } from "vue" import { Track } from "./track" import { Tick } from './ticks' import { error } from "./error" enum State { Unfetched, Fetching, Fetched, } function dateQuery(date: Date): URLSearchParams { let query = new URLSearchParams() query.set("year", date.getUTCFullYear().toString()) query.set("month", (date.getUTCMonth() + 1).toString()) // good thing I still had this ^^^^^^^^^^^^^^ in mind when I wrote this 😬 query.set("day", date.getUTCDate().toString()) return query } interface LoggedInUser { name: string } class AppState { tracks: Array state: State user?: LoggedInUser constructor() { this.tracks = new Array this.state = State.Unfetched } streamUpdatesFromServer() { const source = new EventSource("/api/v1/updates") source.addEventListener("open", () => console.debug("opened event source")) source.addEventListener('message', event => console.log(event)) source.addEventListener('TickAdded', event => { const tick: Tick = Tick.fromJSON(JSON.parse(event.data)) const tracks = this.tracks.map(track => { if (track.id === tick.track_id) { const ticks = track.ticks ?? [] ticks.push(tick) track.ticks = ticks } return track }) this.tracks = tracks }) source.addEventListener('TrackAdded', ({ data }) => { const track: Track = Track.fromJSON(JSON.parse(data)) this.tracks = [track, ...this.tracks] }) source.addEventListener('TickDropped', event => { const tick: Tick = Tick.fromJSON(JSON.parse(event.data)) const tracks = this.tracks.map(track => { if (track.id === tick.track_id) { track.ticks = track.ticks?.filter($tick => $tick.id !== tick.id) } return track }) this.tracks = tracks }) source.addEventListener('TrackDropped', ({ data }) => { const track: Track = Track.fromJSON(JSON.parse(data)) this.tracks = this.tracks.filter($track => $track.id !== track.id) }) source.addEventListener('TrackChanged', ({ data }) => { const track: Track = Track.fromJSON(JSON.parse(data)) this.tracks = this.tracks.map($track => $track.id === track.id ? track : $track) }) source.addEventListener('Lagged', event => { console.log(event) // Refresh the page, refetching the list of tracks and ticks window.location = window.location }) source.addEventListener('error', event => { error(event) window.location = window.location }) window.addEventListener('beforeunload', () => source.close()) } async repopulate() { this.state = State.Fetching this.tracks = await Track.fetchAll() } async populate() { if (this.state != State.Unfetched) return await this.repopulate() this.streamUpdatesFromServer() this.state = State.Fetched } async taskCompleted(track: Track, date: Date): Promise { const query = dateQuery(date) const response: Response = await fetch(`/api/v1/tracks/${track.id}/ticked?${query.toString()}`, { method: "PATCH" }) const body = await response.text() if (!response.ok) { error(body) throw new Error(`error setting tick for track ${track.id} ("${track.name}"): ${response.status} ${response.statusText}`) } return JSON.parse(body) } async taskMarkedIncomplete(track: Track, date: Date) { const query = dateQuery(date) const { ok, status, statusText } = await fetch(`/api/v1/tracks/${track.id}/all-ticks?${query.toString()}`, { method: 'DELETE' }) if (!ok) error(`error deleting ticks for ${track.id}: ${statusText} (${status})`) } async addTrack(track: Track): Promise { const response = await fetch('/api/v1/tracks', { method: "POST", body: JSON.stringify(track), headers: { "Content-Type": "application/json" } }) if (!response.ok) error(`error submitting track: ${track}: ${response.statusText} (${response.status})`) return response.ok } async removeTrack(trackID: number) { const response = await fetch(`/api/v1/tracks/${trackID}`, { method: "DELETE" }) if (!response.ok) error(`error deleting track with ID ${trackID}: ${response.statusText} (${response.status})`) } } export const state = reactive(new AppState)