aboutsummaryrefslogtreecommitdiff
path: root/src/view
diff options
context:
space:
mode:
authorJoris2021-05-13 14:50:51 +0200
committerJoris2021-05-13 14:58:26 +0200
commit221b6451fb4f8559a10e7fefebd13ce125ef29d0 (patch)
tree3ab337b7b2d40e8235f887046a580b0850540f11 /src/view
parent5c636f11cdfed82634ee572645d765b704941b68 (diff)
Rewrite in TypeScript
BuckleScript is no longer maintained. Choose a widely used techno that will still be maintained in the following years.
Diffstat (limited to 'src/view')
-rw-r--r--src/view/form.ts47
-rw-r--r--src/view/timer.ts158
2 files changed, 205 insertions, 0 deletions
diff --git a/src/view/form.ts b/src/view/form.ts
new file mode 100644
index 0000000..60e5f08
--- /dev/null
+++ b/src/view/form.ts
@@ -0,0 +1,47 @@
+import * as Config from 'config'
+import h from 'h'
+import * as Router from 'router'
+import * as Duration from 'duration'
+
+function labelledInput(
+ labelValue: string,
+ min: number,
+ value: number,
+ update: (n: number) => void
+): Element {
+ return h('label',
+ { className: 'g-Form__Label',
+ oninput: (e: Event) => {
+ if (e.target !== null) {
+ const target = e.target as HTMLInputElement
+ update(parseInt(target.value) || 0)
+ }
+ }
+ },
+ labelValue,
+ h('input', { className: 'g-Form__Input', type: 'number', min, value }))
+}
+
+export function view(config: Config.Config, showPage: (route: Router.Route) => void) {
+ const duration = document.createTextNode(Duration.prettyPrint(Config.getDuration(config)))
+ const wd = () => duration.textContent = Duration.prettyPrint(Config.getDuration(config))
+ return h('div',
+ { className: 'g-Layout__Page' },
+ h('header', { className: 'g-Layout__Header' }, 'Tabata timer'),
+ h('form',
+ { className: 'g-Form'
+ , onsubmit: (e: Event) => {
+ e.preventDefault()
+ const timerRoute = { kind: Router.Kind.Timer, config }
+ history.pushState({}, '', Router.toString(timerRoute))
+ showPage(timerRoute)
+ }},
+ labelledInput('prepare', 0, config.prepare, n => { config.prepare = n; wd()}),
+ labelledInput('tabatas', 1, config.tabatas, n => { config.tabatas = n; wd()}),
+ labelledInput('cycles', 1, config.cycles, n => { config.cycles = n; wd()}),
+ labelledInput('work', 5, config.work, n => { config.work = n; wd()}),
+ labelledInput('rest', 5, config.rest, n => { config.rest = n; wd()}),
+ h('div', { className: 'g-Form__Duration' }, 'duration', h('div', {}, duration)),
+ h('button', { className: 'g-Form__Start' }, 'start'))
+ )
+}
diff --git a/src/view/timer.ts b/src/view/timer.ts
new file mode 100644
index 0000000..ddcea71
--- /dev/null
+++ b/src/view/timer.ts
@@ -0,0 +1,158 @@
+import * as Config from 'config'
+import * as State from 'state'
+import * as Arc from 'arc'
+import * as Router from 'router'
+import * as Audio from 'audio'
+import h from 'h'
+
+let interval: number | undefined = undefined
+
+export function clearInterval() {
+ if (interval !== undefined) {
+ window.clearInterval(interval)
+ interval = undefined
+ }
+}
+
+export function view(config: Config.Config, showPage: (route: Router.Route) => void) {
+
+ const formUrl = `${Router.toString({ kind: Router.Kind.Form, config })}`
+ const duration = Config.getDuration(config)
+
+ // State
+ let isPlaying = true
+ let elapsed = 0
+ let state = State.getAt(config, elapsed)
+
+ // Elements
+ const section = h('section', { className: timerClass(state.step) })
+ const stepElt = document.createTextNode(State.prettyPrintStep(state.step))
+ const stepCountElt = document.createTextNode(stepCount(state))
+ const arcPathElt = h('path', { class: 'g-Timer__ArcProgress' })
+
+ const updateDom = () => {
+ const angle = elapsed / duration * 360
+ arcPathElt.setAttribute("d", Arc.describe(0, 0, 90, 0, angle))
+ section.className = timerClass(state.step)
+ stepElt.textContent = State.prettyPrintStep(state.step)
+ stepCountElt.textContent = stepCount(state)
+ Audio.playFromStep(config, state)
+ }
+
+ const quit = () => {
+ const formRoute = { kind: Router.Kind.Form, config }
+ history.pushState({}, '', Router.toString(formRoute))
+ showPage(formRoute)
+ }
+
+ const update = () => {
+ if (isPlaying) {
+ elapsed = elapsed + 1
+ state = State.getAt(config, elapsed)
+ elapsed > duration
+ ? quit()
+ : updateDom()
+ }
+ }
+
+ // Start timer
+ if (interval !== undefined) {
+ window.clearInterval(interval)
+ interval = undefined
+ }
+ interval = window.setInterval(update, 1000)
+
+ section.append(
+ h('div',
+ { className: 'g-Timer__Dial' },
+ h('svg',
+ { class: 'g-Timer__Arc',
+ viewBox: '-100 -100 200 200'
+ },
+ h('path',
+ { class: 'g-Timer__ArcTotal',
+ d: Arc.describe(0, 0, 90.0, 0, 359.999)
+ }
+ ),
+ ...arcPaths(config),
+ arcPathElt
+ ),
+ h('div', { className: 'g-Timer__Step' }, stepElt),
+ h('div', {}, stepCountElt)
+ ),
+ h('div',
+ { className: 'g-Timer__Buttons' },
+ h('button',
+ { className: 'g-Timer__Button',
+ onclick: (e: MouseEvent) => {
+ isPlaying = !isPlaying
+ const elt = e.target as HTMLElement
+ elt.textContent = isPlaying
+ ? 'pause'
+ : 'resume'
+ elt.className = isPlaying
+ ? 'g-Timer__Button'
+ : 'g-Timer__Button g-Timer__Button--Active'
+ }
+ },
+ 'pause'
+ ),
+ h('a',
+ { className: 'g-Timer__Button',
+ href: formUrl
+ },
+ 'quit'
+ )
+ )
+ )
+
+ return section
+}
+
+function arcPaths(config: Config.Config): Element[] {
+ const paths = []
+
+ let t = 0
+ const totalDuration = Config.getDuration(config)
+
+ let arc = (kind: string, duration: number): Element => {
+ const startAngle = 360 * t / totalDuration
+ const endAngle = 360 * (t + duration) / totalDuration
+
+ t += duration
+
+ return h('path',
+ { class: `g-Timer__Arc${kind}`,
+ d: Arc.describe(0, 0, 90.0, startAngle, endAngle)
+ }
+ )
+ }
+
+ for (let tabata = 0; tabata < config.tabatas; tabata++) {
+ paths.push(arc('Prepare', config.prepare))
+ for (let cycle = 0; cycle < config.cycles; cycle++) {
+ paths.push(arc('Work', config.work))
+ paths.push(arc('Rest', config.rest))
+ }
+ }
+
+ return paths
+}
+
+function timerClass(step: State.Step): string {
+ if (step === State.Step.Work) {
+ return 'g-Layout__Page g-Timer g-Timer--Work'
+ } else if (step === State.Step.Rest) {
+ return 'g-Layout__Page g-Timer g-Timer--Rest'
+ } else {
+ return 'g-Layout__Page g-Timer g-Timer--Prepare'
+ }
+}
+
+function stepCount(state: State.State): string {
+ if (state.step === State.Step.Work || state.step === State.Step.Rest) {
+ return `#${state.tabata.toString()}.${state.cycle.toString()}`
+ } else {
+ return `#${state.tabata.toString()}`
+ }
+}