1
0
Fork 0
forked from TWS/kalkutago

Compare commits

...

7 commits

16 changed files with 286 additions and 49 deletions

View file

@ -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",

View file

@ -1,14 +1,11 @@
<script setup lang="ts">
import Table from "./components/Table.vue";
import { state } from "./state";
state.populate()
import { RouterView } from 'vue-router'
import NavBar from './components/NavBar.vue'
</script>
<template>
<div class="container">
<Table></Table>
<div >
<NavBar />
<RouterView />
</div>
</template>
<style scoped></style>

View file

@ -0,0 +1,28 @@
<script setup lang="ts">
import { RouterLink } from 'vue-router';
</script>
<template>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<h1 class="title navbar-item">Kalkutago</h1>
</div>
<div class="navbar-menu"></div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<RouterLink to="/" v-if="$route.path === '/new-track'">
<button class="button is-info">
Go Back
</button>
</RouterLink>
<RouterLink to="/new-track" v-else>
<button class="button is-primary">
Add Track
</button>
</RouterLink>
</div>
</div>
</div>
</nav>
</template>

View file

@ -2,7 +2,9 @@
<table class="table">
<thead>
<th>Date</th>
<th v-for="track in state.tracks" :key="track.id">{{ track.icon }}</th>
<th v-for="track in state.tracks" :key="track.id">
<TrackIcon :icon="track.icon" :id="track.id" />
</th>
</thead>
<tbody>
<tr v-for="date in dates" :key="date.valueOf()">
@ -18,6 +20,7 @@
<script setup lang="ts">
import TickComponent from "./TickComponent.vue";
import { state } from "../state";
import TrackIcon from "./TrackIcon.vue";
const today = new Date()
const ONE_DAY_MS = 86_400_000

View file

@ -0,0 +1,16 @@
<script setup lang="ts">
import { state } from '../state';
const props = defineProps<{icon: String, id: number|undefined}>()
const del = () => {
if(props.id)
if(confirm("are you sure you want to delete this track?"))
state.removeTrack(props.id)
}
</script>
<template>
<div @click=del>
{{props.icon}}
</div>
</template>

View file

@ -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()

15
client/src/router.ts Normal file
View file

@ -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

View file

@ -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<boolean> {
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})`)
}
})

View file

@ -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

View file

@ -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<Tick>
constructor(
id: number,
id: number | undefined,
name: String,
description: String,
icon: String,

View file

@ -0,0 +1,91 @@
<script setup lang="ts">
import { RouterLink, useRouter } from 'vue-router';
import { Track } from '../track';
import { computed, ref } from 'vue';
import { state } from '../state';
const props = defineProps<{ initialState?: Track }>()
const router = useRouter()
const name = ref(props.initialState?.name ?? "")
const description = ref(props.initialState?.description?.toString() ?? "")
const icon = ref(props.initialState?.icon ?? "")
const enabled = ref(props.initialState?.enabled ?? true)
const multipleEntriesPerDay = ref(props.initialState?.multiple_entries_per_day ?? false)
const color = ref(props.initialState?.color ?? undefined)
const order = ref<any>(props.initialState?.order ?? undefined)
const submittingNow = ref(false)
const submitButtonClass = computed(() => 'button is-primary' + (submittingNow.value ? ' is-loading' : ''))
const submit = async () => {
submittingNow.value = true
// if you make a change to order then erase the value in the box it's ""
if (order.value === "") order.value = undefined
if (order.value instanceof String || typeof order.value === 'string') order.value = Number(order.value)
const track = new Track(undefined, name.value, description.value,
icon.value, Number(enabled.value), Number(multipleEntriesPerDay.value),
color.value, order.value)
if (await state.addTrack(track))
router.push('/')
}
</script>
<template>
<section class="section">
<div class="field">
<label for="name" class="label">Name</label>
<div class="control">
<input type="text" name="name" class="input" v-model="name" />
</div>
</div>
<div class="field">
<label for="description" class="label">Description</label>
<div class="control">
<textarea name="description" cols="30" rows="5" v-model="description"></textarea>
</div>
</div>
<div class="field">
<label for="icon" class="label">Icon</label>
<div class="control">
<input type="text" name="icon" class="input" v-model="icon" />
</div>
</div>
<div class="field is-grouped">
<div class="control">
<label for="enabled" class="label">
<input type="checkbox" name="enabled" class="checkbox" v-model="enabled" />
Enabled?
</label>
</div>
<div class="control">
<label for="multiple-entries" class="label">
<input type="checkbox" name="multiple-entries" class="checkbox" v-model="multipleEntriesPerDay" />
Multiple Entries per Day?
</label>
</div>
</div>
<div class="field">
<div class="control">
TODO color choice
</div>
</div>
<div class="field">
<div class="control">
<label for="order" class="label">
<input type="number" name="order" class="input" v-model="order" />
Order
</label>
</div>
</div>
<div class="buttons">
<RouterLink to="/">
<button class="button is-danger">
Cancel
</button>
</RouterLink>
<button :class="submitButtonClass" @click="submit">
Save
</button>
</div>
</section>
</template>

View file

@ -0,0 +1,11 @@
<script setup lang="ts">
import Table from "../components/Table.vue";
</script>
<template>
<div class="container">
<Table></Table>
</div>
</template>
<style scoped></style>

View file

@ -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"

View file

@ -8,10 +8,7 @@ use crate::error::Error;
use super::error::ApiResult;
#[post("/dump", data = "<sql_dump>")]
pub(crate) async fn sql_dump(
db: &State<DatabaseConnection>,
sql_dump: &str,
) -> ApiResult<Status> {
pub(crate) async fn sql_dump(db: &State<DatabaseConnection>, sql_dump: &str) -> ApiResult<Status> {
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="<sqlite_db>")]
pub(crate) async fn db_file(
db: &State<DatabaseConnection>,
sqlite_db: &[u8],
) -> ApiResult<Status> {
#[post("/", data = "<sqlite_db>")]
pub(crate) async fn db_file(db: &State<DatabaseConnection>, sqlite_db: &[u8]) -> ApiResult<Status> {
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())
}
}

View file

@ -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 = "<track>")]
pub(super) async fn insert_track(
db: &State<DatabaseConnection>,
tx: &State<Sender<Update>>,
track: Json<serde_json::Value>,
) -> ApiResult<Json<tracks::Model>> {
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 = "<track>")]
pub(super) async fn update_track(
db: &State<DatabaseConnection>,
tx: &State<Sender<Update>>,
track: Json<serde_json::Value>,
) -> ApiResult<Json<tracks::Model>> {
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("/<id>")]
pub(super) async fn delete_track(db: &State<DatabaseConnection>, id: i32) -> ApiResult<Status> {
pub(super) async fn delete_track(
db: &State<DatabaseConnection>,
tx: &State<Sender<Update>>,
id: i32,
) -> ApiResult<Status> {
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)
}

View file

@ -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:?}")),
}
}