diff --git a/Makefile b/Makefile index f97671d..274c3d3 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,8 @@ client/dist/index.html: build-client: client/dist/index.html start-server: build-client - -mkdir db.mount docker compose up --build -d clean: docker compose down - -rm -r server/public/ client/dist/ - + rm -r server/public/ client/dist/ diff --git a/README.md b/README.md deleted file mode 100644 index 1282a0e..0000000 --- a/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Kalkutago - -A multi-user Tickmate clone for the web - -## Development -### Before you start -The backend uses a postgres server which is not tracked as a part of the -repository, and neither is the password for it. You'll need to generate a -password by doing something like - -```console -openssl rand -base64 36 > server/postgres.pw -``` - -The development environment uses Traefik to reverse-proxy the API and dev -server. In production, the API will serve the client directly, but this system -allows you to run the dev server behind a reverse proxy so that changes are -rendered quickly without having to rebuild the container. Using the vite dev -server also gets us better debugging in the browser. - -However, that means that Traefik needs a hostname to listen for. The way I've -done this is by adding the following entry to my `/etc/hosts` file: - -``` -127.0.0.1 kalkutago -``` - -### Server -The server is written in Rust, using Rocket and SeaOrm. It uses Rust nightly, so -you'll need to `rustup override set nightly`, though it gets built in docker so -you really only need this for IDE support. - -### Client - -The client is a Vue application. It has a central state in -[`state.ts`](client/src/state.ts). On application load, the state is initialized -by fetching the current tracks and ticks via the API, and then subscribes to -updates from the server by opening an EventSource at `/api/v1/updates`. - -The basic flow is: - - a user interaction happens which should trigger a state change - - the action is dispatched to the server (e.g. by making a PATCH request to - `/api/v1/tracks//ticked`) - - the server makes the appropriate change to the database - - an event is dispatched to the event sources subscribed via `/api/v1/updates` - - The client receives the event, the reactive state is updated, and the UI - changes to match the expected state. - -### Running for development - -`docker-compose_dev.yml` is symlinked to `docker-compose.yml` for now, so once -the hostname is set up Just run `docker-compose up --build` and visit -http://kalkutago/ to open the app - -### Populating the database -In order to see the UI components I add a track to the database like - -```console -curl --json '{"name": "test", "description": "test track", "icon": "❓", "enabled": 1}' kalkutago/api/v1/tracks -``` \ No newline at end of file diff --git a/client/src/App.vue b/client/src/App.vue index 5f48482..8e42bdc 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,11 +1,7 @@ diff --git a/client/src/components/NavBar.vue b/client/src/components/NavBar.vue deleted file mode 100644 index 145a817..0000000 --- a/client/src/components/NavBar.vue +++ /dev/null @@ -1,37 +0,0 @@ - - diff --git a/client/src/components/Table.vue b/client/src/components/Table.vue index 1f1ddec..338bd0b 100644 --- a/client/src/components/Table.vue +++ b/client/src/components/Table.vue @@ -2,9 +2,7 @@ - + @@ -20,7 +18,6 @@ \ No newline at end of file diff --git a/client/src/components/TrackIcon.vue b/client/src/components/TrackIcon.vue deleted file mode 100644 index e5aeabf..0000000 --- a/client/src/components/TrackIcon.vue +++ /dev/null @@ -1,16 +0,0 @@ - - \ No newline at end of file diff --git a/client/src/router.ts b/client/src/router.ts index 8d94d4c..062c50d 100644 --- a/client/src/router.ts +++ b/client/src/router.ts @@ -1,13 +1,10 @@ import { createRouter, createWebHistory } from 'vue-router' import TableView from './views/TableView.vue' -import NewTrackView from './views/NewTrackView.vue' const router = createRouter({ history: createWebHistory(), routes: [ - { path: '/', component: TableView }, - { path: '/new-track', component: NewTrackView }, - { path: '/login', component: import('./views/Login.vue') } + { path: '/', component: TableView } // for other pages: // {path: '/', component: import('./views/TableView.vue')} ] diff --git a/client/src/state.ts b/client/src/state.ts index 028ae4b..cc140cd 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -1,9 +1,6 @@ import { reactive } from "vue" import { Track } from "./track" -import { Tick } from './ticks' import { error } from "./error" -import { getCookie } from "./util"; -import router from './router' enum State { Unfetched, @@ -11,44 +8,30 @@ enum State { Fetched, } -interface LoggedInUser { - name: string -} - -class AppState { - tracks: Array - state: State - user?: LoggedInUser - source?: EventSource - - constructor() { - this.tracks = new Array - this.state = State.Unfetched - const name = getCookie("name") - if (name) this.user = { name } - } +export const state = reactive({ + tracks: new Array, + 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)) + console.log(event) + const tick: Tick = 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)) + console.log(event) + const tick: Tick = 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) @@ -57,14 +40,6 @@ class AppState { }) 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 @@ -75,30 +50,34 @@ class AppState { window.location = window.location }) window.addEventListener('beforeunload', () => source.close()) - this.source = source - } + }, async repopulate() { - if (!this.user) { - this.tracks = [] - return - } this.state = State.Fetching this.tracks = await Track.fetchAll() - this.source?.close() - this.streamUpdatesFromServer() - this.state = State.Fetched - } + }, async populate() { if (this.state != State.Unfetched) return await this.repopulate() + this.streamUpdatesFromServer() + this.state = State.Fetched + }, + async taskCompleted(track: Track, date: Date): Promise { + let query = new URLSearchParams() + query.append("year", date.getUTCFullYear().toString()) + query.append("month", (date.getUTCMonth() + 1).toString()) + // good thing I still had this ^^^^^^^^^^^^^^ in mind when I wrote this 😬 + query.append("day", date.getUTCDate().toString()) + 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) { + const { ok, status, statusText } = await fetch(`/api/v1/tracks/${track.id}/all-ticks`, { method: 'DELETE' }) + if (!ok) + error(`error deleting ticks for ${track.id}: ${statusText} (${status})`) } - async logOut() { - const result = await fetch('/api/v1/auth', {method: 'DELETE'}) - if(!result.ok) return error('failed to log out') - this.user = undefined - router.push('/login') - } -} - - -export const state = reactive(new AppState) +}) diff --git a/client/src/ticks.ts b/client/src/ticks.ts index d50b9ba..895682b 100644 --- a/client/src/ticks.ts +++ b/client/src/ticks.ts @@ -1,4 +1,4 @@ -export interface ITick { +interface ITick { id: number track_id?: number year?: number @@ -10,7 +10,7 @@ export interface ITick { has_time_info?: number } -export class Tick implements ITick { +class Tick implements ITick { id: number track_id?: number year?: number diff --git a/client/src/track.ts b/client/src/track.ts index f7525ad..14f7567 100644 --- a/client/src/track.ts +++ b/client/src/track.ts @@ -1,9 +1,7 @@ import { error } from "./error" -import { Tick, ITick } from './ticks' -import { dateQuery } from "./util" export interface ITrack { - id?: number + id: number name: String description: String icon: String @@ -15,7 +13,7 @@ export interface ITrack { } export class Track implements ITrack { - id?: number + id: number name: String description: String icon: String @@ -26,7 +24,7 @@ export class Track implements ITrack { ticks?: Array constructor( - id: number | undefined, + id: number, name: String, description: String, icon: String, @@ -49,34 +47,6 @@ export class Track implements ITrack { this.fetchTicks = this.fetchTicks.bind(this) } - /** - * Add this track to the database. A `TrackAdded` event should have been - * received from the server on the event stream by the time this returns. - * - * @returns whether or not the query succeeded - */ - async create(): Promise { - // note that this.id is expected to be `undefined` here. - const response = await fetch('/api/v1/tracks', { - method: "POST", - body: JSON.stringify(this), - headers: { "Content-Type": "application/json" } - }) - if (!response.ok) - error(`error submitting track ${this.name}: ${response.statusText} (${response.status})`) - return response.ok - } - - async delete() { - const id = this.id - if (id) await Track.deleteById(id) - } - - static async deleteById(id: number) { - const response = await fetch(`/api/v1/tracks/${id}`, { method: "DELETE" }) - if (!response.ok) error(`error deleting track with ID ${id}: ${response.statusText} (${response.status})`) - } - static fromJSON(track: ITrack): Track { return new Track(track.id, track.name, track.description, track.icon, track.enabled, track.multiple_entries_per_day, track.color, track.order) } @@ -127,36 +97,4 @@ export class Track implements ITrack { } return [] } - /** - * Mark this track as being completed on the given date. A `TickAdded` event - * should have been received from the server on the event stream by the time - * this returns. - * - * @param date the date the task was completed - * @returns the decoded server API response - */ - async markComplete(date: Date) { - const query = dateQuery(date) - const response: Response = await fetch(`/api/v1/tracks/${this.id}/ticked?${query.toString()}`, { method: "PATCH" }) - const body = await response.text() - if (!response.ok) { - error(body) - throw new Error(`error setting tick for track ${this.id} ("${this.name}"): ${response.status} ${response.statusText}`) - } - return JSON.parse(body) - } - /** - * Mark this track as being incomplete on the given date. A `TickAdded` event - * should have been received from the server on the event stream by the time - * this returns. - * - * @param date the date the task was completed - * @returns the decoded server API response - */ - async markIncomplete(date: Date) { - const query = dateQuery(date) - const { ok, status, statusText } = await fetch(`/api/v1/tracks/${this.id}/all-ticks?${query.toString()}`, { method: 'DELETE' }) - if (!ok) - error(`error deleting ticks for ${this.id}: ${statusText} (${status})`) - } -} +} \ No newline at end of file diff --git a/client/src/util.ts b/client/src/util.ts deleted file mode 100644 index 023079c..0000000 --- a/client/src/util.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function getCookie(key: string): string | null { - const start = document.cookie.indexOf(key + '=') - if(start === -1) return null - let end: number | undefined = document.cookie.indexOf(';', start) - if(end === -1) - end = undefined - return document.cookie.substring(start + key.length + 1, end) -} - -export 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 -} diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue deleted file mode 100644 index 84d09b7..0000000 --- a/client/src/views/Login.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - diff --git a/client/src/views/NewTrackView.vue b/client/src/views/NewTrackView.vue deleted file mode 100644 index a15e26a..0000000 --- a/client/src/views/NewTrackView.vue +++ /dev/null @@ -1,90 +0,0 @@ - - \ No newline at end of file diff --git a/client/src/views/TableView.vue b/client/src/views/TableView.vue index f5afd39..053df35 100644 --- a/client/src/views/TableView.vue +++ b/client/src/views/TableView.vue @@ -1,10 +1,5 @@
Date - - {{ track.icon }}