Merge pull request 'Feature: Add Track Form' (#7) from scott/kalkutago:feature/add-track-form into main
Reviewed-on: https://git.tams.tech/TWS/kalkutago/pulls/7
This commit is contained in:
commit
7f2e12e913
|
@ -12,7 +12,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bulma": "^0.9.4",
|
"bulma": "^0.9.4",
|
||||||
"sass": "^1.25.0",
|
"sass": "^1.25.0",
|
||||||
"vue": "^3.2.47"
|
"vue": "^3.2.47",
|
||||||
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"@vitejs/plugin-vue": "^4.1.0",
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Table from "./components/Table.vue";
|
import { RouterView } from 'vue-router'
|
||||||
import { state } from "./state";
|
import NavBar from './components/NavBar.vue'
|
||||||
|
|
||||||
state.populate()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div >
|
||||||
<Table></Table>
|
<NavBar />
|
||||||
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
28
client/src/components/NavBar.vue
Normal file
28
client/src/components/NavBar.vue
Normal 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>
|
|
@ -2,7 +2,9 @@
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<th>Date</th>
|
<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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="date in dates" :key="date.valueOf()">
|
<tr v-for="date in dates" :key="date.valueOf()">
|
||||||
|
@ -18,6 +20,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TickComponent from "./TickComponent.vue";
|
import TickComponent from "./TickComponent.vue";
|
||||||
import { state } from "../state";
|
import { state } from "../state";
|
||||||
|
import TrackIcon from "./TrackIcon.vue";
|
||||||
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const ONE_DAY_MS = 86_400_000
|
const ONE_DAY_MS = 86_400_000
|
||||||
|
|
16
client/src/components/TrackIcon.vue
Normal file
16
client/src/components/TrackIcon.vue
Normal 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>
|
|
@ -1,6 +1,12 @@
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import './style.scss'
|
import './style.scss'
|
||||||
import App from './App.vue'
|
import router from './router'
|
||||||
|
import { state } from "./state";
|
||||||
import 'bulma/css/bulma.css'
|
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
15
client/src/router.ts
Normal 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
|
|
@ -1,5 +1,6 @@
|
||||||
import { reactive } from "vue"
|
import { reactive } from "vue"
|
||||||
import { Track } from "./track"
|
import { Track } from "./track"
|
||||||
|
import { Tick } from './ticks'
|
||||||
import { error } from "./error"
|
import { error } from "./error"
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
|
@ -26,7 +27,7 @@ export const state = reactive({
|
||||||
source.addEventListener('message', event => console.log(event))
|
source.addEventListener('message', event => console.log(event))
|
||||||
source.addEventListener('TickAdded', event => {
|
source.addEventListener('TickAdded', event => {
|
||||||
console.log(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 => {
|
const tracks = this.tracks.map(track => {
|
||||||
if (track.id === tick.track_id) {
|
if (track.id === tick.track_id) {
|
||||||
const ticks = track.ticks ?? []
|
const ticks = track.ticks ?? []
|
||||||
|
@ -38,9 +39,12 @@ export const state = reactive({
|
||||||
})
|
})
|
||||||
this.tracks = tracks
|
this.tracks = tracks
|
||||||
})
|
})
|
||||||
|
source.addEventListener('TrackAdded', ({ data }) => {
|
||||||
|
const track: Track = Track.fromJSON(JSON.parse(data))
|
||||||
|
this.tracks = [track, ...this.tracks]
|
||||||
|
})
|
||||||
source.addEventListener('TickDropped', event => {
|
source.addEventListener('TickDropped', event => {
|
||||||
console.log(event)
|
const tick: Tick = Tick.fromJSON(JSON.parse(event.data))
|
||||||
const tick: Tick = JSON.parse(event.data)
|
|
||||||
const tracks = this.tracks.map(track => {
|
const tracks = this.tracks.map(track => {
|
||||||
if (track.id === tick.track_id) {
|
if (track.id === tick.track_id) {
|
||||||
track.ticks = track.ticks?.filter($tick => $tick.id !== tick.id)
|
track.ticks = track.ticks?.filter($tick => $tick.id !== tick.id)
|
||||||
|
@ -49,6 +53,14 @@ export const state = reactive({
|
||||||
})
|
})
|
||||||
this.tracks = tracks
|
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 => {
|
source.addEventListener('Lagged', event => {
|
||||||
console.log(event)
|
console.log(event)
|
||||||
// Refresh the page, refetching the list of tracks and ticks
|
// Refresh the page, refetching the list of tracks and ticks
|
||||||
|
@ -85,5 +97,19 @@ export const state = reactive({
|
||||||
const { ok, status, statusText } = await fetch(`/api/v1/tracks/${track.id}/all-ticks?${query.toString()}`, { method: 'DELETE' })
|
const { ok, status, statusText } = await fetch(`/api/v1/tracks/${track.id}/all-ticks?${query.toString()}`, { method: 'DELETE' })
|
||||||
if (!ok)
|
if (!ok)
|
||||||
error(`error deleting ticks for ${track.id}: ${statusText} (${status})`)
|
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})`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
interface ITick {
|
export interface ITick {
|
||||||
id: number
|
id: number
|
||||||
track_id?: number
|
track_id?: number
|
||||||
year?: number
|
year?: number
|
||||||
|
@ -10,7 +10,7 @@ interface ITick {
|
||||||
has_time_info?: number
|
has_time_info?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
class Tick implements ITick {
|
export class Tick implements ITick {
|
||||||
id: number
|
id: number
|
||||||
track_id?: number
|
track_id?: number
|
||||||
year?: number
|
year?: number
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { error } from "./error"
|
import { error } from "./error"
|
||||||
|
|
||||||
export interface ITrack {
|
export interface ITrack {
|
||||||
id: number
|
id?: number
|
||||||
name: String
|
name: String
|
||||||
description: String
|
description: String
|
||||||
icon: String
|
icon: String
|
||||||
|
@ -13,7 +13,7 @@ export interface ITrack {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Track implements ITrack {
|
export class Track implements ITrack {
|
||||||
id: number
|
id?: number
|
||||||
name: String
|
name: String
|
||||||
description: String
|
description: String
|
||||||
icon: String
|
icon: String
|
||||||
|
@ -24,7 +24,7 @@ export class Track implements ITrack {
|
||||||
ticks?: Array<Tick>
|
ticks?: Array<Tick>
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number | undefined,
|
||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
icon: String,
|
icon: String,
|
||||||
|
|
91
client/src/views/NewTrackView.vue
Normal file
91
client/src/views/NewTrackView.vue
Normal 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>
|
11
client/src/views/TableView.vue
Normal file
11
client/src/views/TableView.vue
Normal 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>
|
|
@ -213,6 +213,11 @@
|
||||||
"@vue/compiler-dom" "3.3.4"
|
"@vue/compiler-dom" "3.3.4"
|
||||||
"@vue/shared" "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":
|
"@vue/reactivity-transform@3.3.4":
|
||||||
version "3.3.4"
|
version "3.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
|
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz#52908476e34d6a65c6c21cd2722d41ed8ae51929"
|
||||||
|
@ -522,6 +527,13 @@ vite@^4.3.9:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
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:
|
vue-template-compiler@^2.7.14:
|
||||||
version "2.7.14"
|
version "2.7.14"
|
||||||
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1"
|
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1"
|
||||||
|
|
Loading…
Reference in a new issue