diff --git a/client/package.json b/client/package.json index f410ede..a7858e3 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,8 @@ "dependencies": { "bulma": "^0.9.4", "sass": "^1.25.0", - "vue": "^3.2.47" + "vue": "^3.2.47", + "vue-router": "4" }, "devDependencies": { "@vitejs/plugin-vue": "^4.1.0", diff --git a/client/src/App.vue b/client/src/App.vue index b7adb44..5f48482 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -1,14 +1,11 @@ - - diff --git a/client/src/components/NavBar.vue b/client/src/components/NavBar.vue new file mode 100644 index 0000000..e11af91 --- /dev/null +++ b/client/src/components/NavBar.vue @@ -0,0 +1,28 @@ + + \ No newline at end of file diff --git a/client/src/components/Table.vue b/client/src/components/Table.vue index 338bd0b..1f1ddec 100644 --- a/client/src/components/Table.vue +++ b/client/src/components/Table.vue @@ -2,7 +2,9 @@ - + @@ -18,6 +20,7 @@ + \ No newline at end of file diff --git a/client/src/main.ts b/client/src/main.ts index 26bcf20..90cdedc 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -1,6 +1,12 @@ import { createApp } from 'vue' import './style.scss' -import App from './App.vue' +import router from './router' +import { state } from "./state"; import 'bulma/css/bulma.css' +import App from './App.vue' -createApp(App).mount('#app') + +const app = createApp(App) +app.use(router) +app.mount('#app') +state.populate() diff --git a/client/src/router.ts b/client/src/router.ts new file mode 100644 index 0000000..806e60d --- /dev/null +++ b/client/src/router.ts @@ -0,0 +1,15 @@ +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 } + // for other pages: + // {path: '/', component: import('./views/TableView.vue')} + ] +}) + +export default router \ No newline at end of file diff --git a/client/src/state.ts b/client/src/state.ts index cc140cd..cb842b6 100644 --- a/client/src/state.ts +++ b/client/src/state.ts @@ -1,5 +1,6 @@ import { reactive } from "vue" import { Track } from "./track" +import { Tick } from './ticks' import { error } from "./error" enum State { @@ -17,7 +18,7 @@ export const state = reactive({ source.addEventListener('message', event => console.log(event)) source.addEventListener('TickAdded', event => { console.log(event) - const tick: Tick = JSON.parse(event.data) + 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 ?? [] @@ -29,9 +30,12 @@ export const state = reactive({ }) this.tracks = tracks }) + source.addEventListener('TrackAdded', ({ data }) => { + const track: Track = Track.fromJSON(JSON.parse(data)) + this.tracks = [track, ...this.tracks] + }) source.addEventListener('TickDropped', event => { - console.log(event) - const tick: Tick = JSON.parse(event.data) + 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) @@ -40,6 +44,14 @@ export const state = reactive({ }) 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 @@ -79,5 +91,19 @@ export const state = reactive({ 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 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})`) } }) diff --git a/client/src/ticks.ts b/client/src/ticks.ts index 895682b..d50b9ba 100644 --- a/client/src/ticks.ts +++ b/client/src/ticks.ts @@ -1,4 +1,4 @@ -interface ITick { +export interface ITick { id: number track_id?: number year?: number @@ -10,7 +10,7 @@ interface ITick { has_time_info?: number } -class Tick implements ITick { +export class Tick implements ITick { id: number track_id?: number year?: number diff --git a/client/src/track.ts b/client/src/track.ts index 14f7567..83947a3 100644 --- a/client/src/track.ts +++ b/client/src/track.ts @@ -1,7 +1,7 @@ import { error } from "./error" export interface ITrack { - id: number + id?: number name: String description: String icon: String @@ -13,7 +13,7 @@ export interface ITrack { } export class Track implements ITrack { - id: number + id?: number name: String description: String icon: String @@ -24,7 +24,7 @@ export class Track implements ITrack { ticks?: Array constructor( - id: number, + id: number | undefined, name: String, description: String, icon: String, diff --git a/client/src/views/NewTrackView.vue b/client/src/views/NewTrackView.vue new file mode 100644 index 0000000..8b2ed62 --- /dev/null +++ b/client/src/views/NewTrackView.vue @@ -0,0 +1,91 @@ + + \ No newline at end of file diff --git a/client/src/views/TableView.vue b/client/src/views/TableView.vue new file mode 100644 index 0000000..053df35 --- /dev/null +++ b/client/src/views/TableView.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/client/yarn.lock b/client/yarn.lock index 47e9ec4..a06ffcf 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -213,6 +213,11 @@ "@vue/compiler-dom" "3.3.4" "@vue/shared" "3.3.4" +"@vue/devtools-api@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" + integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== + "@vue/reactivity-transform@3.3.4": version "3.3.4" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929" @@ -522,6 +527,13 @@ vite@^4.3.9: optionalDependencies: fsevents "~2.3.2" +vue-router@4: + version "4.2.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.2.tgz#b0097b66d89ca81c0986be03da244c7b32a4fd81" + integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ== + dependencies: + "@vue/devtools-api" "^6.5.0" + vue-template-compiler@^2.7.14: version "2.7.14" resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1" diff --git a/server/src/api/import.rs b/server/src/api/import.rs index 956de57..462930b 100644 --- a/server/src/api/import.rs +++ b/server/src/api/import.rs @@ -8,10 +8,7 @@ use crate::error::Error; use super::error::ApiResult; #[post("/dump", data = "")] -pub(crate) async fn sql_dump( - db: &State, - sql_dump: &str, -) -> ApiResult { +pub(crate) async fn sql_dump(db: &State, sql_dump: &str) -> ApiResult { for line in sql_dump.lines() { let line = line.to_ascii_lowercase(); if line.starts_with("insert into") @@ -25,11 +22,8 @@ pub(crate) async fn sql_dump( Ok(Status::Ok) } -#[post("/", data="")] -pub(crate) async fn db_file( - db: &State, - sqlite_db: &[u8], -) -> ApiResult { +#[post("/", data = "")] +pub(crate) async fn db_file(db: &State, sqlite_db: &[u8]) -> ApiResult { use std::{ io::Write, process::{Command, Stdio}, @@ -51,8 +45,6 @@ pub(crate) async fn db_file( if result.status.success() { sql_dump(db, &String::from_utf8(result.stdout).map_err(Error::from)?).await } else { - Err(Error::SqliteCommandError(String::from_utf8_lossy( - &result.stderr, - ).to_string()).into()) + Err(Error::SqliteCommandError(String::from_utf8_lossy(&result.stderr).to_string()).into()) } } diff --git a/server/src/api/tracks.rs b/server/src/api/tracks.rs index a292ea7..c14789b 100644 --- a/server/src/api/tracks.rs +++ b/server/src/api/tracks.rs @@ -5,7 +5,6 @@ use either::Either::{self, Left, Right}; use rocket::http::Status; use rocket::{serde::json::Json, State}; use sea_orm::{prelude::*, DatabaseConnection}; -use std::default::default; use tokio::sync::broadcast::Sender; use super::update::Update; @@ -54,37 +53,46 @@ pub(super) async fn ticks_for_track( #[post("/", format = "application/json", data = "")] pub(super) async fn insert_track( db: &State, + tx: &State>, track: Json, ) -> ApiResult> { let track = track.0; let db = db as &DatabaseConnection; - let mut model: tracks::ActiveModel = default(); - model.set_from_json(track).map_err(Error::from)?; - Ok(Json(model.insert(db).await.map_err(Error::from)?)) + let model = tracks::ActiveModel::from_json(track).map_err(Error::from)?; + let track = model.insert(db).await.map_err(Error::from)?; + tx.send(Update::track_added(track.clone())) + .map_err(Error::from)?; + Ok(Json(track)) } #[put("/", format = "application/json", data = "")] pub(super) async fn update_track( db: &State, + tx: &State>, track: Json, ) -> ApiResult> { let db = db as &DatabaseConnection; - Ok(Json( - tracks::ActiveModel::from_json(track.0) - .map_err(Error::from)? - .update(db) - .await - .map_err(Error::from)?, - )) + let track = tracks::ActiveModel::from_json(track.0) + .map_err(Error::from)? + .update(db) + .await + .map_err(Error::from)?; + tx.send(Update::track_changed(track.clone())) + .map_err(Error::from)?; + Ok(Json(track)) } #[delete("/")] -pub(super) async fn delete_track(db: &State, id: i32) -> ApiResult { +pub(super) async fn delete_track( + db: &State, + tx: &State>, + id: i32, +) -> ApiResult { let db = db as &DatabaseConnection; - Tracks::delete_by_id(id) - .exec(db) - .await - .map_err(Error::from)?; + let Some(track) = Tracks::find_by_id(id).one(db).await.map_err(Error::from)? else { + return Ok(Status::NotFound); + }; + tx.send(Update::track_removed(track)).map_err(Error::from)?; Ok(Status::Ok) } diff --git a/server/src/api/update.rs b/server/src/api/update.rs index 0b6adba..1135c18 100644 --- a/server/src/api/update.rs +++ b/server/src/api/update.rs @@ -4,7 +4,10 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::sync::broadcast::Sender; -use crate::{entities::ticks, error::Result}; +use crate::{ + entities::{ticks, tracks}, + error::Result, +}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Update { @@ -16,6 +19,10 @@ pub enum Update { kind: UpdateType, count: u64, }, + TrackChanged { + kind: UpdateType, + track: tracks::Model, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -23,6 +30,9 @@ pub enum Update { pub enum UpdateType { TickAdded, TickDropped, + TrackAdded, + TrackChanged, + TrackDropped, Error, } @@ -48,6 +58,26 @@ impl Update { } } + pub fn track_added(track: tracks::Model) -> Self { + Self::TrackChanged { + kind: UpdateType::TrackAdded, + track, + } + } + + pub fn track_removed(track: tracks::Model) -> Self { + Self::TrackChanged { + kind: UpdateType::TrackDropped, + track, + } + } + + pub fn track_changed(track: tracks::Model) -> Self { + Self::TrackChanged { + kind: UpdateType::TrackChanged, + track, + } + } pub fn to_event(&self) -> Event { use Update::*; match self { @@ -56,6 +86,7 @@ impl Update { Event::json(&json! {{"message": "error: lagged", "count": count}}) .event(format!("{kind:?}")) } + TrackChanged { kind, track } => Event::json(track).event(format!("{kind:?}")), } }
Date{{ track.icon }} + +