瀏覽代碼

First release.

Tomi Cvetic 4 年之前
父節點
當前提交
4fb52b1d2e
共有 36 個文件被更改,包括 912 次插入877 次删除
  1. 322 303
      frontend/initial-data.ts
  2. 10 5
      frontend/package-lock.json
  3. 1 0
      frontend/package.json
  4. 12 12
      frontend/pages/_app.tsx
  5. 15 15
      frontend/pages/index.tsx
  6. 5 5
      frontend/pages/timer.tsx
  7. 10 10
      frontend/pages/user.tsx
  8. 4 5
      frontend/src/app/components/Footer.tsx
  9. 10 4
      frontend/src/app/components/Header.tsx
  10. 7 7
      frontend/src/app/components/Logo.tsx
  11. 9 9
      frontend/src/app/components/Meta.tsx
  12. 24 25
      frontend/src/app/components/Nav.tsx
  13. 8 8
      frontend/src/app/components/Page.tsx
  14. 2 5
      frontend/src/lib/__tests__/nestedValues.test.ts
  15. 1 1
      frontend/src/lib/__tests__/regex.test.ts
  16. 3 3
      frontend/src/lib/localState.js
  17. 10 2
      frontend/src/lib/nestedValues.ts
  18. 0 0
      frontend/src/lib/regex.ts
  19. 4 6
      frontend/src/lib/store.tsx
  20. 0 89
      frontend/src/styles/global.js
  21. 84 0
      frontend/src/styles/global.ts
  22. 0 0
      frontend/src/styles/theme.ts
  23. 10 10
      frontend/src/timer/components/AudioPlayer.tsx
  24. 44 43
      frontend/src/timer/components/Countdown.tsx
  25. 0 42
      frontend/src/timer/components/Indicator.tsx
  26. 123 84
      frontend/src/timer/components/Timer.tsx
  27. 36 27
      frontend/src/timer/components/VideoPlayer.tsx
  28. 37 37
      frontend/src/timer/hooks.ts
  29. 2 2
      frontend/src/timer/index.ts
  30. 6 5
      frontend/src/timer/types.ts
  31. 39 39
      frontend/src/timer/utils.ts
  32. 10 10
      frontend/src/training/components/ExerciseComposition.tsx
  33. 13 13
      frontend/src/training/components/Training.tsx
  34. 2 2
      frontend/src/training/index.tsx
  35. 37 37
      frontend/src/training/types.ts
  36. 12 12
      frontend/src/training/utils.ts

文件差異過大導致無法顯示
+ 322 - 303
frontend/initial-data.ts


+ 10 - 5
frontend/package-lock.json

@@ -1381,14 +1381,12 @@
     "@types/prop-types": {
       "version": "15.7.3",
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
-      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
-      "dev": true
+      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
     },
     "@types/react": {
       "version": "16.9.17",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.17.tgz",
       "integrity": "sha512-UP27In4fp4sWF5JgyV6pwVPAQM83Fj76JOcg02X5BZcpSu5Wx+fP9RMqc2v0ssBoQIFvD5JdKY41gjJJKmw6Bg==",
-      "dev": true,
       "requires": {
         "@types/prop-types": "*",
         "csstype": "^2.2.0"
@@ -1418,6 +1416,14 @@
       "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
       "dev": true
     },
+    "@types/styled-jsx": {
+      "version": "2.2.8",
+      "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz",
+      "integrity": "sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg==",
+      "requires": {
+        "@types/react": "*"
+      }
+    },
     "@types/testing-library__dom": {
       "version": "6.11.0",
       "resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.11.0.tgz",
@@ -3507,8 +3513,7 @@
     "csstype": {
       "version": "2.6.8",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
-      "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA==",
-      "dev": true
+      "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
     },
     "cyclist": {
       "version": "1.0.1",

+ 1 - 0
frontend/package.json

@@ -16,6 +16,7 @@
     "@types/howler": "^2.1.2",
     "@types/jest": "^24.0.25",
     "@types/lodash": "^4.14.149",
+    "@types/styled-jsx": "^2.2.8",
     "@types/video.js": "^7.3.4",
     "apollo-boost": "0.4.7",
     "apollo-link": "^1.2.13",

+ 12 - 12
frontend/pages/_app.tsx

@@ -1,26 +1,26 @@
-import App from "next/app";
-import { ApolloProvider } from "@apollo/client";
+import App from 'next/app'
+import { ApolloProvider } from '@apollo/client'
 
-import Page from "../src/app/components/Page";
-import client from "../src/lib/apollo";
-import { StoreProvider } from "../src/lib/store";
+import Page from '../src/app/components/Page'
+import client from '../src/lib/apollo'
+import { StoreProvider } from '../src/lib/store'
 
 class MyApp extends App {
   static async getInitialProps({ Component, ctx }: any) {
-    let pageProps: any = {};
+    let pageProps: any = {}
 
     if (Component.getInitialProps) {
-      pageProps = await Component.getInitialProps(ctx);
+      pageProps = await Component.getInitialProps(ctx)
     }
 
     // Add the query object to the pageProps
     // https://github.com/wesbos/Advanced-React/blob/master/finished-application/frontend/pages/_app.js
-    pageProps.query = ctx.query;
-    return { pageProps };
+    pageProps.query = ctx.query
+    return { pageProps }
   }
 
   render() {
-    const { Component, pageProps } = this.props;
+    const { Component, pageProps } = this.props
 
     return (
       <ApolloProvider client={client}>
@@ -28,8 +28,8 @@ class MyApp extends App {
           <Component {...pageProps} />
         </Page>
       </ApolloProvider>
-    );
+    )
   }
 }
 
-export default MyApp;
+export default MyApp

+ 15 - 15
frontend/pages/index.tsx

@@ -1,10 +1,10 @@
-import Link from "next/link";
+import Link from 'next/link'
 
-import initialData from "../initial-data";
-import { useTrainingsQuery } from "../src/gql";
-import { Training } from "../src/training";
+import initialData from '../initial-data'
+import { useTrainingsQuery } from '../src/gql'
+import { Training } from '../src/training'
 
-console.log(initialData);
+console.log(initialData)
 
 const Home = () => {
   //const { data, error, loading } = useTrainingsQuery();
@@ -15,18 +15,18 @@ const Home = () => {
         <h1>Stay in Shape with u-fit</h1>
         <p>u-fit is a high intensity interval training offered by u-blox.</p>
         <aside>
-          <div className="info">
-            <span className="caption">When</span>
-            <span className="data">Tuesdays, 11:45-12:30</span>
+          <div className='info'>
+            <span className='caption'>When</span>
+            <span className='data'>Tuesdays, 11:45-12:30</span>
           </div>
-          <div className="info">
-            <span className="caption">Equipment</span>
-            <span className="data">Towel, water, optional: yoga mat</span>
+          <div className='info'>
+            <span className='caption'>Equipment</span>
+            <span className='data'>Towel, water, optional: yoga mat</span>
           </div>
         </aside>
       </section>
 
-      <section id="nextTraining">
+      <section id='nextTraining'>
         <Training training={initialData.trainings[0]} />
       </section>
 
@@ -40,7 +40,7 @@ const Home = () => {
         `}
       </style>
     </>
-  );
-};
+  )
+}
 
-export default Home;
+export default Home

+ 5 - 5
frontend/pages/timer.tsx

@@ -1,8 +1,8 @@
-import initialData from "../initial-data";
-import { Timer } from "../src/timer";
+import initialData from '../initial-data'
+import { Timer } from '../src/timer'
 
 const TimerPage = () => {
-  return <Timer training={initialData.trainings[0]} />;
-};
+  return <Timer training={initialData.trainings[0]} />
+}
 
-export default TimerPage;
+export default TimerPage

+ 10 - 10
frontend/pages/user.tsx

@@ -1,6 +1,6 @@
-import { withRouter } from "next/router";
+import { withRouter } from 'next/router'
 
-import { useCurrentUserQuery } from "../src/gql";
+import { useCurrentUserQuery } from '../src/gql'
 
 import {
   SignupForm,
@@ -10,15 +10,15 @@ import {
   ResetPassword,
   UserDetails,
   DeleteUserButton
-} from "../src/user";
+} from '../src/user'
 
 const UserPage = () => {
-  const { data, loading, error } = useCurrentUserQuery();
-  console.log("UserPage", data, loading, error && error.message);
+  const { data, loading, error } = useCurrentUserQuery()
+  console.log('UserPage', data, loading, error && error.message)
   //const user = data && data.me
 
-  if (loading) return <p>Loading user data...</p>;
-  if (error) return <p>Error loading user data.</p>;
+  if (loading) return <p>Loading user data...</p>
+  if (error) return <p>Error loading user data.</p>
 
   return (
     <>
@@ -31,7 +31,7 @@ const UserPage = () => {
   {user && <DeleteUserButton user={user} />*/}
       <p>nothing here.</p>
     </>
-  );
-};
+  )
+}
 
-export default withRouter(UserPage);
+export default withRouter(UserPage)

+ 4 - 5
frontend/src/app/components/Footer.tsx

@@ -1,17 +1,16 @@
-import theme from "../../../src/styles/theme";
+import theme from '../../../src/styles/theme'
 
 const Footer = () => (
   <footer>
-    <p>u-fit &copy; 2020 Tomi Cvetic</p>
+    <p>u-fit &copy; 2020</p>
     <style jsx>{`
       footer {
         text-align: center;
         background-color: ${theme.colors.darkgrey};
-        min-height: 75px;
         color: ${theme.colors.offWhite};
       }
     `}</style>
   </footer>
-);
+)
 
-export default Footer;
+export default Footer

+ 10 - 4
frontend/src/app/components/Header.tsx

@@ -1,10 +1,16 @@
-import Logo from "./Logo";
-import Link from "next/link";
+import Logo from './Logo'
+import Link from 'next/link'
 
 const Header = () => (
   <header>
     <Logo />
+
+    <style jsx>{`
+      header {
+        padding: 20px;
+      }
+    `}</style>
   </header>
-);
+)
 
-export default Header;
+export default Header

+ 7 - 7
frontend/src/app/components/Logo.tsx

@@ -1,11 +1,11 @@
-import Link from "next/link";
+import Link from 'next/link'
 
 const Logo = () => (
-  <Link href="/">
+  <Link href='/'>
     <a>
-      <div id="logo">
-        <span id="circle">˙u</span>
-        <span id="text">fit</span>
+      <div id='logo'>
+        <span id='circle'>˙u</span>
+        <span id='text'>fit</span>
       </div>
       <style jsx>
         {`
@@ -33,6 +33,6 @@ const Logo = () => (
       </style>
     </a>
   </Link>
-);
+)
 
-export default Logo;
+export default Logo

+ 9 - 9
frontend/src/app/components/Meta.tsx

@@ -1,15 +1,15 @@
-import Head from "next/head";
+import Head from 'next/head'
 
 const Meta = () => (
   <Head>
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <meta charSet="utf-8" />
-    <link rel="shortcut icon" href="/favicon.ico" />
-    <link rel="stylesheet" type="text/css" href="/nprogress.css" />
-    <link rel="stylesheet" type="text/css" href="/normalize.css" />
-    <link rel="stylesheet" type="text/css" href="/video.css" />
+    <meta name='viewport' content='width=device-width, initial-scale=1' />
+    <meta charSet='utf-8' />
+    <link rel='shortcut icon' href='/favicon.ico' />
+    <link rel='stylesheet' type='text/css' href='/nprogress.css' />
+    <link rel='stylesheet' type='text/css' href='/normalize.css' />
+    <link rel='stylesheet' type='text/css' href='/video.css' />
     <title>u-fit</title>
   </Head>
-);
+)
 
-export default Meta;
+export default Meta

+ 24 - 25
frontend/src/app/components/Nav.tsx

@@ -33,37 +33,36 @@ const Nav = () => (
 
     <style jsx>
       {`
-ul {
-  display: grid;
-}
+        ul {
+          display: grid;
+        }
 
-li {
-  padding: 0 0.5em;
-  border-bottom: 1px solid ${theme.colors.lightgrey};
-}
+        li {
+          padding: 0 0.5em;
+          border-bottom: 1px solid ${theme.colors.lightgrey};
+        }
 
-@media (min-width: 500px) {
-  ul {
-    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
-    border-bottom: 1px solid ${theme.colors.lightgrey};
-  }
-  li {
-    display: inline;
-    border-bottom: none;
-  }
-}
+        @media (min-width: 500px) {
+          ul {
+            grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+            border-bottom: 1px solid ${theme.colors.lightgrey};
+          }
+          li {
+            display: inline;
+            border-bottom: none;
+          }
+        }
 
-#search {
-  grid-column-end: -2;
-}
+        #search {
+          grid-column-end: -2;
+        }
 
-#user {
-  grid-column-end: -1;
-}
-  `}
+        #user {
+          grid-column-end: -1;
+        }
+      `}
     </style>
   </nav>
-
 )
 
 export default Nav

+ 8 - 8
frontend/src/app/components/Page.tsx

@@ -1,9 +1,9 @@
-import Head from "next/head";
-import Header from "./Header";
-import Meta from "./Meta";
-import Nav from "./Nav";
-import Footer from "./Footer";
-import GlobalStyle from "../../styles/global";
+import Head from 'next/head'
+import Header from './Header'
+import Meta from './Meta'
+import Nav from './Nav'
+import Footer from './Footer'
+import GlobalStyle from '../../styles/global'
 
 const Page = (props: any) => (
   <>
@@ -20,6 +20,6 @@ const Page = (props: any) => (
       {GlobalStyle}
     </style>
   </>
-);
+)
 
-export default Page;
+export default Page

+ 2 - 5
frontend/src/lib/__tests__/nestedValues.test.ts

@@ -16,10 +16,7 @@ const newUser = {
   interests: ['work', 'life']
 }
 const testMe = {
-  users: [
-    testUser1,
-    testUser2
-  ]
+  users: [testUser1, testUser2]
 }
 
 describe('get item', () => {
@@ -52,4 +49,4 @@ describe('set item', () => {
   })
 })
 
-export default true
+export default true

+ 1 - 1
frontend/src/lib/__tests__/regex.test.ts

@@ -10,4 +10,4 @@ describe('email regex', () => {
     expect(simpleEmail).not.toBe(null)
     expect(simpleEmail && simpleEmail[0]).toBe('tomi@cvetic.ch')
   })
-})
+})

+ 3 - 3
frontend/src/lib/localState.js

@@ -20,12 +20,12 @@ const TOGGLE_MENU_MUTATION = gql`
 
 export const resolvers = {
   Mutation: {
-    toggleMenu(_, variables, {chache}) {
-      const {menuOpen} = cache.readQuery({
+    toggleMenu(_, variables, { chache }) {
+      const { menuOpen } = cache.readQuery({
         query: LOCAL_STATE_QUERY
       })
       const data = {
-        data: {menuOpen: !menuOpen}
+        data: { menuOpen: !menuOpen }
       }
       cache.writeData(data)
       return data

+ 10 - 2
frontend/src/lib/nestedValues.ts

@@ -9,7 +9,11 @@ function getValue<ValueObject>(container: ValueObject, path: string) {
   return obj
 }
 
-function recursiveSet<ValueObject>(object: ValueObject, path: string[], value: any): any {
+function recursiveSet<ValueObject>(
+  object: ValueObject,
+  path: string[],
+  value: any
+): any {
   if (path.length === 0) {
     return value
   }
@@ -29,7 +33,11 @@ function recursiveSet<ValueObject>(object: ValueObject, path: string[], value: a
   }
 }
 
-function setValue<ValueObject>(container: ValueObject, path: string, value: any) {
+function setValue<ValueObject>(
+  container: ValueObject,
+  path: string,
+  value: any
+) {
   const elements = toPath(path)
   return recursiveSet(container, elements, value)
 }

+ 0 - 0
frontend/src/lib/regex.js → frontend/src/lib/regex.ts


+ 4 - 6
frontend/src/lib/store.tsx

@@ -1,8 +1,6 @@
 import { createContext, useReducer, ReactChildren } from 'react'
 
-type Action =
-  | { type: 'beHappy' }
-  | { type: 'addReason', reason: string }
+type Action = { type: 'beHappy' } | { type: 'addReason'; reason: string }
 
 type State = {
   isHappy: boolean
@@ -10,7 +8,7 @@ type State = {
 }
 
 type Context = {
-  state: State,
+  state: State
   dispatch: React.Dispatch<Action>
 }
 
@@ -40,7 +38,7 @@ const initialState: State = {
 
 const initialContext: Context = {
   state: initialState,
-  dispatch: () => { }
+  dispatch: () => {}
 }
 
 const Store = createContext(initialContext)
@@ -54,7 +52,7 @@ const StoreProvider = (props: ProviderProps) => {
   return (
     <Store.Provider value={{ state, dispatch }}>
       {props.children}
-    </Store.Provider >
+    </Store.Provider>
   )
 }
 

+ 0 - 89
frontend/src/styles/global.js

@@ -1,89 +0,0 @@
-import theme from './theme'
-import css from 'styled-jsx/css'
-
-const GlobalStyle = css.global`
-/* normalize.css is imported in meta.js */
-
-@import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300|Roboto:300,900&display=swap');
-
-/* Use border-box sizing instead of context-box.
-   This includes border and padding in the calculation.
-
-   Also define a default font. */
-html {
-  box-sizing: border-box;
-  font-family: 'Roboto', sans-serif;
-  font-weight: 300;
-  /*font-size: 12px;*/
-}
-
-*, *:before, *:after {
-  box-sizing: inherit;
-}
-
-#__next {
-  display: grid;
-  grid-template-areas:
-    "header"
-    "nav"
-    "main"
-    "footer";
-  grid-template-rows: auto auto 1fr minmax(180px, auto);
-
-  max-width: ${theme.maxWidth};
-  min-height: 100vh;
-  margin: 0 auto;
-
-  background: ${theme.colors.offWhite};
-  color: ${theme.colors.black};
-  box-shadow: ${theme.bs};
-}
-
-@media (min-width: 500px) {
-  body #__next {
-    grid-template-areas:
-      "header nav"
-      "main main"
-      "footer footer";
-    grid-template-columns: auto 1fr;
-    grid-template-rows: auto 1fr minmax(180px, auto);
-  }
-}
-
-header {  grid-area: header; }
-nav {  grid-area: nav; }
-main {  grid-area: main; }
-footer {  grid-area: footer; }
-
-
-/* Use bold font for headers */
-h1, h2, h3, h4, h5, h6 {
-  font-weight: 900;
-}
-
-/* Use monospace font for pre */
-pre {
-  font-family: 'Roboto Mono', monospace;
-}
-
-/*
-button {
-  font-weight: 900;
-  background: ${theme.colors.darkblue};
-  color: ${theme.colors.lighterblue};
-  border: 1px solid ${theme.colors.darkerblue};
-  padding: 0.3em 1.8em;
-  cursor: pointer;
-  box-shadow: ${theme.bsSmall};
-}
-
-input,
-textarea {
-  border: 1px solid ${theme.colors.lightgrey};
-  padding: 6px;
-  margin: 0 8px;
-  background: transparent;
-}*/
-`
-
-export default GlobalStyle

+ 84 - 0
frontend/src/styles/global.ts

@@ -0,0 +1,84 @@
+import theme from './theme'
+import css from 'styled-jsx/css'
+
+const GlobalStyle = css.global`
+  /* normalize.css is imported in meta.js */
+
+  @import url('https://fonts.googleapis.com/css?family=Roboto+Mono:300|Roboto:300,900&display=swap');
+
+  /* Use border-box sizing instead of context-box.
+   This includes border and padding in the calculation.
+
+   Also define a default font. */
+  html {
+    box-sizing: border-box;
+    font-family: 'Roboto', sans-serif;
+    font-weight: 300;
+    /*font-size: 12px;*/
+  }
+
+  *,
+  *:before,
+  *:after {
+    box-sizing: inherit;
+  }
+
+  #__next {
+    display: grid;
+    grid-template-areas:
+      'header'
+      'nav'
+      'main'
+      'footer';
+    grid-template-rows: auto auto 1fr minmax(180px, auto);
+
+    max-width: ${theme.maxWidth};
+    min-height: 100vh;
+    margin: 0 auto;
+
+    background: ${theme.colors.offWhite};
+    color: ${theme.colors.black};
+    box-shadow: ${theme.bs};
+  }
+
+  @media (min-width: 500px) {
+    body #__next {
+      grid-template-areas:
+        'header nav'
+        'main main'
+        'footer footer';
+      grid-template-columns: auto 1fr;
+      grid-template-rows: auto 1fr minmax(75px, auto);
+    }
+  }
+
+  header {
+    grid-area: header;
+  }
+  nav {
+    grid-area: nav;
+  }
+  main {
+    grid-area: main;
+  }
+  footer {
+    grid-area: footer;
+  }
+
+  /* Use bold font for headers */
+  h1,
+  h2,
+  h3,
+  h4,
+  h5,
+  h6 {
+    font-weight: 900;
+  }
+
+  /* Use monospace font for pre */
+  pre {
+    font-family: 'Roboto Mono', monospace;
+  }
+`
+
+export default GlobalStyle

+ 0 - 0
frontend/src/styles/theme.js → frontend/src/styles/theme.ts


+ 10 - 10
frontend/src/timer/components/AudioPlayer.tsx

@@ -1,16 +1,16 @@
-import { useEffect } from "react";
-import { Howl } from "howler";
+import { useEffect } from 'react'
+import { Howl } from 'howler'
 
 const AudioPlayer = ({ setAudio }: { setAudio: any }) => {
   useEffect(() => {
     const sound = new Howl({
-      src: ["/media/06_Better_As_One.mp3"]
-    });
-    console.log(sound);
-    setAudio(sound);
-  }, []);
+      src: ['/media/06_Better_As_One.mp3']
+    })
+    console.log(sound)
+    setAudio(sound)
+  }, [])
 
-  return <div>Audio Player</div>;
-};
+  return <div>Audio Player</div>
+}
 
-export default AudioPlayer;
+export default AudioPlayer

+ 44 - 43
frontend/src/timer/components/Countdown.tsx

@@ -1,13 +1,13 @@
-import { formatTime } from "../../training/utils";
-import { useEffect, useState, useRef } from "react";
-import { Howl } from "howler";
-import { describeArc, limit } from "../utils";
+import { formatTime } from '../../training/utils'
+import { useEffect, useState, useRef } from 'react'
+import { Howl } from 'howler'
+import { describeArc, limit } from '../utils'
 
 interface ICountdown {
-  seconds: number;
-  totalPercent: number;
-  exercisePercent: number;
-  onClick: () => void;
+  seconds: number
+  totalPercent: number
+  exercisePercent: number
+  onClick: () => void
 }
 
 const Countdown = ({
@@ -16,60 +16,60 @@ const Countdown = ({
   exercisePercent,
   onClick
 }: ICountdown) => {
-  const [color, setColor] = useState("rgba(55,55,55,1)");
-  const [intSeconds, setIntSeconds] = useState(0);
+  const [color, setColor] = useState('rgba(55,55,55,1)')
+  const [intSeconds, setIntSeconds] = useState(0)
 
   useEffect(() => {
-    setIntSeconds(Math.ceil(seconds));
-  }, [seconds]);
+    setIntSeconds(Math.ceil(seconds))
+  }, [seconds])
 
   useEffect(() => {
     if (intSeconds <= 5) {
-      fadeIn();
-      setTimeout(fadeOut, 300);
+      fadeIn()
+      setTimeout(fadeOut, 300)
     }
-  }, [intSeconds]);
+  }, [intSeconds])
 
   function fadeIn() {
-    setColor("rgba(127,0,0,1)");
+    setColor('rgba(127,0,0,1)')
   }
   function fadeOut() {
-    setColor("rgba(55,55,55,1)");
+    setColor('rgba(55,55,55,1)')
   }
 
   return (
-    <div id="timer" style={{ color }} onClick={onClick}>
+    <div id='timer' style={{ color }} onClick={onClick}>
       <svg>
         <text
-          textAnchor="middle"
-          alignmentBaseline="central"
-          y="185"
-          x="150"
+          textAnchor='middle'
+          alignmentBaseline='central'
+          y='185'
+          x='150'
           fill={color}
         >
           {formatTime(intSeconds)}
         </text>
         <circle
-          cx="150"
-          cy="150"
-          r="140"
-          fill="none"
-          stroke="#aa333344"
-          strokeWidth="5"
+          cx='150'
+          cy='150'
+          r='140'
+          fill='none'
+          stroke='#aa333344'
+          strokeWidth='5'
         />
         <circle
-          cx="150"
-          cy="150"
-          r="130"
-          fill="none"
-          stroke="#3333aa44"
-          strokeWidth="5"
+          cx='150'
+          cy='150'
+          r='130'
+          fill='none'
+          stroke='#3333aa44'
+          strokeWidth='5'
         />
         <path
           d={describeArc(150, 150, 140, 0, limit(totalPercent * 360, 0, 359))}
-          fill="none"
-          stroke="#aa3333ff"
-          strokeWidth="8"
+          fill='none'
+          stroke='#aa3333ff'
+          strokeWidth='8'
         />
         <path
           d={describeArc(
@@ -79,9 +79,9 @@ const Countdown = ({
             0,
             limit(exercisePercent * 360, 0, 359)
           )}
-          fill="none"
-          stroke="#3333aaff"
-          strokeWidth="8"
+          fill='none'
+          stroke='#3333aaff'
+          strokeWidth='8'
         />
       </svg>
 
@@ -93,11 +93,12 @@ const Countdown = ({
             font-size: 100px;
             font-weight: 900;
             transition: color 300ms ease-out;
+            cursor: pointer;
           }
         `}
       </style>
     </div>
-  );
-};
+  )
+}
 
-export default Countdown;
+export default Countdown

+ 0 - 42
frontend/src/timer/components/Indicator.tsx

@@ -1,42 +0,0 @@
-import { formatTime } from "../../training/utils";
-
-const Indicator = ({
-  time,
-  duration,
-  id
-}: {
-  time: number;
-  duration: number;
-  id: string;
-}) => {
-  const width = `${(100 * (duration - time)) / duration}%`;
-  return (
-    <div className="indicator">
-      {/*<div>{formatTime(duration - time)}</div>*/}
-      <div className="bar" style={{ width }} id={id}></div>
-      <style jsx>
-        {`
-          .indicator {
-            height: 20px;
-            display: grid;
-            grid-template-columns: 1fr;
-          }
-
-          .bar {
-            transition: all ${1000}ms linear;
-          }
-
-          #current-indicator {
-            background-color: blue;
-          }
-
-          #total-indicator {
-            background-color: red;
-          }
-        `}
-      </style>
-    </div>
-  );
-};
-
-export default Indicator;

+ 123 - 84
frontend/src/timer/components/Timer.tsx

@@ -1,27 +1,28 @@
-import { useState, useEffect, useRef } from "react";
-import { VideoJsPlayer } from "video.js";
+import { useState, useEffect, useRef } from 'react'
+import { VideoJsPlayer } from 'video.js'
 
-import { ITraining } from "../../training/types";
-import { getExerciseList, getTrainingTime, getPosition } from "../utils";
+import { ITraining } from '../../training/types'
+import { getExerciseList, getTrainingTime, getPosition } from '../utils'
 
-import Countdown from "./Countdown";
-import VideoPlayer from "./VideoPlayer";
-import AudioPlayer from "./AudioPlayer";
-import { IExerciseItem } from "../types";
-import { Howl } from "howler";
-import { useTimer } from "../hooks";
+import Countdown from './Countdown'
+import VideoPlayer from './VideoPlayer'
+import AudioPlayer from './AudioPlayer'
+import { IExerciseItem } from '../types'
+import { Howl } from 'howler'
+import { useTimer } from '../hooks'
+import theme from '../../styles/theme'
 
 const Timer = ({ training }: { training: ITraining }) => {
-  const [time, timer] = useTimer({ tickPeriod: 100 });
+  const [time, timer] = useTimer({ tickPeriod: 100 })
 
   const [state, setState] = useState({
     exerciseList: [] as IExerciseItem[],
     totalTime: 0
-  });
+  })
 
   const rosie = useRef(
     new Howl({
-      src: ["/media/ROSIE.mp3"],
+      src: ['/media/ROSIE.mp3'],
       sprite: {
         ttt: [114, 2956 - 114],
         five: [10211, 10883 - 10211],
@@ -36,16 +37,16 @@ const Timer = ({ training }: { training: ITraining }) => {
         thirty: [27027, 28250 - 27027]
       }
     })
-  );
+  )
 
   useEffect(() => {
     //console.log("effect 1");
-    rosie.current.play("ttt");
-    const exerciseList = getExerciseList(training.blocks);
-    console.log(exerciseList);
-    const totalTime = getTrainingTime(exerciseList);
-    setState({ ...state, exerciseList, totalTime });
-  }, [training]);
+    rosie.current.play('ttt')
+    const exerciseList = getExerciseList(training.blocks)
+    console.log(exerciseList)
+    const totalTime = getTrainingTime(exerciseList)
+    setState({ ...state, exerciseList, totalTime })
+  }, [training])
 
   //console.log("is it over?", time, state.totalTime, trainingOver);
 
@@ -54,85 +55,70 @@ const Timer = ({ training }: { training: ITraining }) => {
     previousExercise,
     nextExercise,
     exerciseTime
-  } = getPosition(state.exerciseList, timer.time);
+  } = getPosition(state.exerciseList, timer.time)
   //console.log("aaa", time, currentExercise);
 
   useEffect(() => {
-    if (time > state.totalTime) stopTimer();
+    if (time > state.totalTime) stopTimer()
     const countdown = currentExercise
       ? currentExercise.duration + currentExercise.offset - timer.intTime + 1
-      : 0;
+      : 0
     if (timer.running && rosie.current && !rosie.current.playing()) {
       if (exerciseTime < 1) {
-        if (currentExercise && currentExercise.exercise !== "Rest")
-          rosie.current.play("go");
-        else rosie.current.play("rest");
-      } else if (countdown === 90) rosie.current.play("ninety");
-      else if (countdown === 60) rosie.current.play("sixty");
-      else if (countdown === 30) rosie.current.play("thirty");
-      else if (countdown === 5) rosie.current.play("five");
-      else if (countdown === 4) rosie.current.play("four");
-      else if (countdown === 3) rosie.current.play("three");
-      else if (countdown === 2) rosie.current.play("two");
-      else if (countdown === 1) rosie.current.play("one");
+        if (currentExercise && currentExercise.exercise !== 'Rest')
+          rosie.current.play('go')
+        else rosie.current.play('rest')
+      } else if (countdown === 90) rosie.current.play('ninety')
+      else if (countdown === 60) rosie.current.play('sixty')
+      else if (countdown === 30) rosie.current.play('thirty')
+      else if (countdown === 5) rosie.current.play('five')
+      else if (countdown === 4) rosie.current.play('four')
+      else if (countdown === 3) rosie.current.play('three')
+      else if (countdown === 2) rosie.current.play('two')
+      else if (countdown === 1) rosie.current.play('one')
     }
-  }, [time, timer.running]);
+  }, [time, timer.running])
 
-  const video: { current: VideoJsPlayer | undefined } = useRef();
+  const video: { current: VideoJsPlayer | undefined } = useRef()
   //const audio: { current: Howl | undefined } = useRef();
 
   function startTimer() {
-    if (time >= state.totalTime) return;
-    timer.start();
-    if (video.current) video.current.play();
+    if (time >= state.totalTime) return
+    timer.start()
+    if (video.current) video.current.play()
     //if (audio.current) audio.current.play();
     //console.log("Timer started.");
   }
 
   function stopTimer() {
-    timer.stop();
-    if (video.current) video.current.pause();
+    timer.stop()
+    if (video.current) video.current.pause()
     //if (audio.current) audio.current.pause();
     //console.log("stopped");
   }
 
   function forward() {
     if (nextExercise) {
-      timer.setTime(nextExercise.offset);
+      timer.setTime(nextExercise.offset)
     }
   }
 
   function back() {
     if (previousExercise) {
-      timer.setTime(previousExercise.offset);
+      timer.setTime(previousExercise.offset)
     }
   }
 
   const videoSrc =
     currentExercise && currentExercise.video
       ? currentExercise.video
-      : "/media/block0.mp4";
+      : '/media/block0.mp4'
   //console.log("current state:", currentExercise, state);
 
   return (
-    <>
-      <div>
-        {/*<label htmlFor="rest">Rest</label>
-        <input type="number" min="25" max="60" step="5" defaultValue="25" />*/}
-        <button onClick={back} disabled={timer.running}>
-          back
-        </button>
-        <button onClick={timer.running ? stopTimer : startTimer}>
-          {timer.running ? "stop" : "start"}
-        </button>
-        <button onClick={forward} disabled={timer.running}>
-          forward
-        </button>
-      </div>
-      <div className="content">
-        <h1>
-          {(currentExercise && currentExercise.toplevelBlock) || "Torture"}
-        </h1>
+    <div id='timer'>
+      <h1>{(currentExercise && currentExercise.toplevelBlock) || 'Torture'}</h1>
+      <div id='flow'>
         <Countdown
           seconds={
             currentExercise ? currentExercise.duration - exerciseTime : 0
@@ -143,59 +129,112 @@ const Timer = ({ training }: { training: ITraining }) => {
           }
           onClick={timer.running ? stopTimer : startTimer}
         />
-        <div className="header">current exercise</div>
-        <div className="exercise">
-          {currentExercise ? currentExercise.exercise : "😎"}
-        </div>
-        <div className="header">next up</div>
-        <div className="exercise">
-          {nextExercise ? nextExercise.exercise : "😎"}
+        <div id='controls'>
+          {/*<label htmlFor="rest">Rest</label>
+        <input type="number" min="25" max="60" step="5" defaultValue="25" />*/}
+          <button onClick={back} disabled={timer.running}>
+            back
+          </button>
+          <button onClick={timer.running ? stopTimer : startTimer}>
+            {timer.running ? 'stop' : 'start'}
+          </button>
+          <button onClick={forward} disabled={timer.running}>
+            forward
+          </button>
         </div>
+        <h2>current exercise</h2>
+        <p className='exercise'>
+          {currentExercise ? currentExercise.exercise : '😎'}
+        </p>
+        <h2>next up</h2>
+        <p className='exercise'>
+          {nextExercise ? nextExercise.exercise : '😎'}
+        </p>
+      </div>
+      <div id='description'>
         <VideoPlayer
           src={videoSrc}
           getVideoHandle={(videoHandle: VideoJsPlayer) =>
             (video.current = videoHandle)
           }
         />
+        <p className='description'>
+          {currentExercise && currentExercise.description}
+        </p>
       </div>
 
       <style jsx>{`
-        .content {
+        #timer {
           text-align: center;
         }
 
-        .header {
+        #timer h1 {
+          margin: 0 auto 0.8rem auto;
+        }
+
+        #flow h2 {
           margin-top: 0.4em;
           font-size: 90%;
+          font-weight: 300;
+          text-transform: uppercase;
+          margin: auto;
         }
 
-        .exercise {
+        #flow p {
           font-size: 110%;
           font-weight: 900;
+          margin: 0 auto 0.8em auto;
+        }
+
+        #controls {
+          margin-bottom: 0.4rem;
         }
 
-        :global(.content .video-js) {
-          margin: 20px auto;
+        button {
+          margin: 0.3rem;
+          padding: 0.3rem;
+          min-width: 6rem;
+          border: none;
+          background-color: ${theme.colors.darkblue};
+          color: ${theme.colors.offWhite};
+          cursor: pointer;
         }
 
-        @media (min-width: 700px) {
-          .content {
-            text-align: center;
+        @media (min-width: 768px) {
+          #timer {
             display: grid;
-            grid-auto-flow: dense;
-            grid-template-columns: 1fr auto;
+            grid-template-columns: 1fr 1fr;
           }
-          .content h1 {
+          h1 {
+            margin: 0 auto 1.4rem auto;
             grid-column: 1/3;
           }
+
+          #flow h2 {
+            margin-bottom: 0.6rem;
+          }
+
+          #controls,
+          #flow p {
+            margin-bottom: 1.4rem;
+          }
+
+          button {
+            padding: 0.6rem;
+            margin: 0.6rem;
+          }
+
           .header,
           .exercise {
             grid-column: 1/2;
           }
+          .description {
+            grid-column: 2/3;
+          }
         }
       `}</style>
-    </>
-  );
-};
+    </div>
+  )
+}
 
-export default Timer;
+export default Timer

+ 36 - 27
frontend/src/timer/components/VideoPlayer.tsx

@@ -1,42 +1,51 @@
-import { useRef, useEffect } from "react";
-import videojs, { VideoJsPlayer } from "video.js";
+import { useRef, useEffect } from 'react'
+import videojs, { VideoJsPlayer } from 'video.js'
 
-const VideoPlayer = ({
-  getVideoHandle,
-  src
-}: {
-  getVideoHandle: (videoHandle: VideoJsPlayer) => void;
-  src: string;
-}) => {
-  const videoPlayerRef = useRef();
-  const video: { current: VideoJsPlayer | undefined } = useRef();
+interface IVideoPlayer {
+  src: string
+  getVideoHandle: (videoHandle: VideoJsPlayer) => void
+}
 
+const VideoPlayer = ({ getVideoHandle, src }: IVideoPlayer) => {
+  const videoPlayerRef = useRef()
+  const video: { current: VideoJsPlayer | undefined } = useRef()
+
+  /**
+   * Initial effect creates the player and returns a handle
+   * to the
+   */
   useEffect(() => {
-    const tmp = videojs(videoPlayerRef.current, {
+    const videoPlayer = videojs(videoPlayerRef.current, {
       width: 512,
       loop: true,
-      controls: true
-    });
-    getVideoHandle(tmp);
-    video.current = tmp;
+      controls: true,
+      muted: true
+    })
+    getVideoHandle(videoPlayer)
+    video.current = videoPlayer
     return () => {
-      if (tmp) tmp.dispose();
-    };
-  }, []);
+      if (videoPlayer) videoPlayer.dispose()
+    }
+  }, [])
 
   useEffect(() => {
     if (video.current) {
-      const paused = video.current.paused();
-      video.current.src(src);
-      if (!paused) video.current.play();
+      const paused = video.current.paused()
+      video.current.src(src)
+      if (!paused) video.current.play()
     }
-  }, [src]);
+  }, [src])
 
   return (
     <div data-vjs-player>
-      <video ref={videoPlayerRef as any} className="video-js vjs-16-9" />
+      <video ref={videoPlayerRef as any} className='video-js vjs-16-9' />
+      <style jsx>{`
+        div {
+          max-width: 100vw;
+        }
+      `}</style>
     </div>
-  );
-};
+  )
+}
 
-export default VideoPlayer;
+export default VideoPlayer

+ 37 - 37
frontend/src/timer/hooks.ts

@@ -1,63 +1,63 @@
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect, useRef } from 'react'
 
 interface ITimer {
-  tickPeriod?: number;
+  tickPeriod?: number
 }
 
 interface ITimerHandler {
-  running: boolean;
-  time: number;
-  intTime: number;
-  start: () => void;
-  stop: () => void;
-  reset: () => void;
-  setTime: (value: number) => void;
+  running: boolean
+  time: number
+  intTime: number
+  start: () => void
+  stop: () => void
+  reset: () => void
+  setTime: (value: number) => void
 }
 
 interface ITimeoutRef {
-  current: ReturnType<typeof setTimeout> | undefined;
+  current: ReturnType<typeof setTimeout> | undefined
 }
 
 export const useTimer = (args?: ITimer): [number, ITimerHandler] => {
-  const { tickPeriod } = { tickPeriod: 100, ...args };
+  const { tickPeriod } = { tickPeriod: 100, ...args }
 
-  const [running, setRunning] = useState(false);
-  const [time, setTime] = useState(0);
-  const [intTime, setIntTime] = useState(0);
+  const [running, setRunning] = useState(false)
+  const [time, setTime] = useState(0)
+  const [intTime, setIntTime] = useState(0)
 
-  const timeout: ITimeoutRef = useRef();
-  const lastTick = useRef(Date.now());
-  const timeBuffer = useRef(0);
+  const timeout: ITimeoutRef = useRef()
+  const lastTick = useRef(Date.now())
+  const timeBuffer = useRef(0)
 
   useEffect(() => {
-    if (!running) return;
-    timeout.current = setTimeout(tick, tickPeriod - timeBuffer.current);
-    timeBuffer.current = 0;
-  }, [time, running]);
+    if (!running) return
+    timeout.current = setTimeout(tick, tickPeriod - timeBuffer.current)
+    timeBuffer.current = 0
+  }, [time, running])
 
   function tick() {
-    const elapsedTime = Date.now() - lastTick.current;
-    lastTick.current = Date.now();
-    const newTime = (time * 1000 + elapsedTime) / 1000;
-    setTime(newTime);
-    setIntTime(Math.ceil(newTime));
+    const elapsedTime = Date.now() - lastTick.current
+    lastTick.current = Date.now()
+    const newTime = (time * 1000 + elapsedTime) / 1000
+    setTime(newTime)
+    setIntTime(Math.ceil(newTime))
   }
 
   function start() {
-    console.log("start");
-    lastTick.current = Date.now();
-    setRunning(true);
+    console.log('start')
+    lastTick.current = Date.now()
+    setRunning(true)
   }
 
   function stop() {
-    console.log("stop");
-    if (timeout.current) clearTimeout(timeout.current);
-    timeBuffer.current = Date.now() - lastTick.current;
-    setRunning(false);
+    console.log('stop')
+    if (timeout.current) clearTimeout(timeout.current)
+    timeBuffer.current = Date.now() - lastTick.current
+    setRunning(false)
   }
 
   function reset() {
-    setTime(0);
+    setTime(0)
   }
 
   const handler = {
@@ -68,7 +68,7 @@ export const useTimer = (args?: ITimer): [number, ITimerHandler] => {
     stop,
     reset,
     setTime
-  };
+  }
 
-  return [intTime, handler];
-};
+  return [intTime, handler]
+}

+ 2 - 2
frontend/src/timer/index.ts

@@ -1,3 +1,3 @@
-import Timer from "./components/Timer";
+import Timer from './components/Timer'
 
-export { Timer };
+export { Timer }

+ 6 - 5
frontend/src/timer/types.ts

@@ -1,7 +1,8 @@
 export interface IExerciseItem {
-  exercise: string;
-  duration: number;
-  offset: number;
-  video?: string;
-  toplevelBlock?: string;
+  exercise: string
+  duration: number
+  offset: number
+  video?: string
+  toplevelBlock?: string
+  description?: string
 }

+ 39 - 39
frontend/src/timer/utils.ts

@@ -1,6 +1,6 @@
-import { IBlock } from "../training/types";
-import { calculateDuration } from "../training/utils";
-import { IExerciseItem } from "./types";
+import { IBlock } from '../training/types'
+import { calculateDuration } from '../training/utils'
+import { IExerciseItem } from './types'
 
 /**
  * Find the right exercise given a certain time.
@@ -10,24 +10,24 @@ import { IExerciseItem } from "./types";
 export function getPosition(exerciseList: IExerciseItem[], time: number) {
   const index = exerciseList.findIndex(
     exercise => time < exercise.offset + exercise.duration
-  );
+  )
 
-  const previousExercise = index >= 1 ? exerciseList[index - 1] : null;
-  const currentExercise = index >= 0 ? exerciseList[index] : null;
+  const previousExercise = index >= 1 ? exerciseList[index - 1] : null
+  const currentExercise = index >= 0 ? exerciseList[index] : null
   const nextExercise =
     index >= 0 && index < exerciseList.length - 1
       ? exerciseList[index + 1]
-      : null;
+      : null
   const values = {
     currentExercise,
     nextExercise,
     previousExercise,
     exerciseTime: 0
-  };
+  }
   if (currentExercise !== null) {
-    values.exerciseTime = Math.max(time - currentExercise.offset, 0);
+    values.exerciseTime = Math.max(time - currentExercise.offset, 0)
   }
-  return values;
+  return values
 }
 
 /**
@@ -40,38 +40,38 @@ export function getExerciseList(
   initialOffset = 0,
   toplevelBlock: undefined | string = undefined
 ): IExerciseItem[] {
-  let offset = initialOffset;
+  let offset = initialOffset
   return blocks
     .map(block => {
       if (block.blocks) {
-        const blockArray = [];
+        const blockArray = []
         for (let i = 0; i < (block.repetitions || 1); i++) {
           const subBlocks = getExerciseList(
             block.blocks,
             offset,
             toplevelBlock || block.title
-          );
-          const lastItem = subBlocks[subBlocks.length - 1];
-          offset = lastItem.offset + lastItem.duration;
+          )
+          const lastItem = subBlocks[subBlocks.length - 1]
+          offset = lastItem.offset + lastItem.duration
           if (block.rest) {
-            if (lastItem.exercise === "Rest") {
-              lastItem.duration += block.rest;
-              offset += block.rest;
+            if (lastItem.exercise === 'Rest') {
+              lastItem.duration += block.rest
+              offset += block.rest
             } else {
               subBlocks.push({
-                exercise: "Rest",
+                exercise: 'Rest',
                 toplevelBlock: toplevelBlock || block.title,
                 duration: block.rest,
                 offset
-              });
-              offset += block.rest;
+              })
+              offset += block.rest
             }
           }
-          blockArray.push(subBlocks);
+          blockArray.push(subBlocks)
         }
-        return blockArray.flat();
+        return blockArray.flat()
       } else if (block.exercises) {
-        const blockArray = [];
+        const blockArray: IExerciseItem[] = []
         const newItem = {
           exercise: block.exercises
             .map(exercise =>
@@ -79,28 +79,28 @@ export function getExerciseList(
                 ? `${exercise.repetitions}x ${exercise.name}`
                 : exercise.name
             )
-            .join(" - "),
+            .join(' - '),
           duration: calculateDuration(block),
           video: block.video,
           description: block.description,
           toplevelBlock: toplevelBlock || block.title,
           offset
-        };
+        }
         for (let i = 0; i < (block.repetitions || 1); i++) {
-          blockArray.push({ ...newItem, offset });
-          offset += newItem.duration;
+          blockArray.push({ ...newItem, offset })
+          offset += newItem.duration
         }
-        return blockArray.flat();
+        return blockArray.flat()
       }
     })
-    .flat();
+    .flat()
 }
 
 export function getTrainingTime(exercises: IExerciseItem[]) {
   return exercises.reduce(
     (accumulator, exercise) => accumulator + exercise.duration,
     0
-  );
+  )
 }
 
 export function polarToCartesian(
@@ -109,12 +109,12 @@ export function polarToCartesian(
   radius: number,
   angleInDegrees: number
 ) {
-  const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
+  const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0
 
   return {
     x: centerX + radius * Math.cos(angleInRadians),
     y: centerY + radius * Math.sin(angleInRadians)
-  };
+  }
 }
 
 export function describeArc(
@@ -124,16 +124,16 @@ export function describeArc(
   startAngle: number,
   endAngle: number
 ) {
-  const start = polarToCartesian(x, y, radius, endAngle);
-  const end = polarToCartesian(x, y, radius, startAngle);
+  const start = polarToCartesian(x, y, radius, endAngle)
+  const end = polarToCartesian(x, y, radius, startAngle)
 
-  const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
+  const largeArcFlag = endAngle - startAngle <= 180 ? '0' : '1'
 
-  const arcString = `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArcFlag} 0 ${end.x} ${end.y}`;
+  const arcString = `M ${start.x} ${start.y} A ${radius} ${radius} 0 ${largeArcFlag} 0 ${end.x} ${end.y}`
 
-  return arcString;
+  return arcString
 }
 
 export function limit(value: number, min: number, max: number) {
-  return Math.min(Math.max(min, value), max);
+  return Math.min(Math.max(min, value), max)
 }

+ 10 - 10
frontend/src/training/components/ExerciseComposition.tsx

@@ -1,18 +1,18 @@
-import { formatTime, printExercises } from "../utils";
-import { IExercise } from "../types";
+import { formatTime, printExercises } from '../utils'
+import { IExercise } from '../types'
 
 export interface IExerciseComposition {
-  exercises: IExercise[];
-  duration: number;
+  exercises: IExercise[]
+  duration: number
 }
 
 const ExerciseComposition = ({ exercises, duration }: IExerciseComposition) => {
-  const exerciseString = printExercises(exercises);
+  const exerciseString = printExercises(exercises)
 
   return (
-    <div className="exercise-composition">
+    <div className='exercise-composition'>
       <span>{exerciseString}</span>
-      <span className="exercise-time">{formatTime(duration)}</span>
+      <span className='exercise-time'>{formatTime(duration)}</span>
 
       <style jsx>
         {`
@@ -26,7 +26,7 @@ const ExerciseComposition = ({ exercises, duration }: IExerciseComposition) => {
         `}
       </style>
     </div>
-  );
-};
+  )
+}
 
-export default ExerciseComposition;
+export default ExerciseComposition

+ 13 - 13
frontend/src/training/components/Training.tsx

@@ -1,9 +1,9 @@
-import theme from "../../styles/theme";
+import theme from '../../styles/theme'
 
-import TrainingBlock from "./TrainingBlock";
-import Link from "next/link";
-import { ITraining } from "../types";
-import TrainingMeta from "./TrainingMeta";
+import TrainingBlock from './TrainingBlock'
+import Link from 'next/link'
+import { ITraining } from '../types'
+import TrainingMeta from './TrainingMeta'
 
 const Training = ({ training }: { training: ITraining }) => {
   return (
@@ -14,7 +14,7 @@ const Training = ({ training }: { training: ITraining }) => {
 
       <section>
         <h2>Program</h2>
-        <Link href="/timer">
+        <Link href='/timer'>
           <button>Start Timer</button>
         </Link>
         {training.blocks &&
@@ -28,11 +28,11 @@ const Training = ({ training }: { training: ITraining }) => {
           article {
             display: grid;
             grid-template-areas:
-              "title title"
-              "information placeholder"
-              "content content";
+              'title title'
+              'information placeholder'
+              'content content';
             grid-template-columns: 1fr 2fr;
-            background-image: url("media/man_working_out.jpg");
+            background-image: url('media/man_working_out.jpg');
             background-size: auto 400px;
             background-repeat: no-repeat;
             margin: 2em 0;
@@ -70,7 +70,7 @@ const Training = ({ training }: { training: ITraining }) => {
         `}
       </style>
     </article>
-  );
-};
+  )
+}
 
-export default Training;
+export default Training

+ 2 - 2
frontend/src/training/index.tsx

@@ -1,3 +1,3 @@
-import Training from "./components/Training";
+import Training from './components/Training'
 
-export { Training };
+export { Training }

+ 37 - 37
frontend/src/training/types.ts

@@ -1,52 +1,52 @@
 export interface ITraining {
-  id: string;
-  title: string;
+  id: string
+  title: string
   type: {
-    id: string;
-    name: string;
-    description: string;
-  };
-  createdAt: string;
-  trainingDate: string;
-  location: string;
-  registrations: string[];
-  attendance: number;
-  ratings: IRating[];
-  published: boolean;
-  blocks: IBlock[];
+    id: string
+    name: string
+    description: string
+  }
+  createdAt: string
+  trainingDate: string
+  location: string
+  registrations: string[]
+  attendance: number
+  ratings: IRating[]
+  published: boolean
+  blocks: IBlock[]
 }
 
 export interface IBlock {
-  id: string;
-  sequence?: number;
-  title?: string;
-  comment?: string;
-  description?: string;
-  duration?: number;
-  repetitions?: number;
-  rest?: number;
-  format?: IFormat;
-  blocks?: IBlock[];
-  exercises?: IExercise[];
-  video?: string;
+  id: string
+  sequence?: number
+  title?: string
+  comment?: string
+  description?: string
+  duration?: number
+  repetitions?: number
+  rest?: number
+  format?: IFormat
+  blocks?: IBlock[]
+  exercises?: IExercise[]
+  video?: string
 }
 
 export interface IFormat {}
 
 export interface IExercise {
-  id: string;
-  name: string;
-  description: string;
-  repetitions: number;
-  videos: string[];
-  pictures: string[];
-  targets: string[];
+  id: string
+  name: string
+  description: string
+  repetitions: number
+  videos: string[]
+  pictures: string[]
+  targets: string[]
   baseExercise: {
-    id: string;
-    name: string;
-  };
+    id: string
+    name: string
+  }
 }
 
 export interface IRating {
-  value: number;
+  value: number
 }

+ 12 - 12
frontend/src/training/utils.ts

@@ -1,22 +1,22 @@
-import { IBlock, IExercise, IRating } from "./types";
+import { IBlock, IExercise, IRating } from './types'
 
 /**
  * Takes a block of exercises and calculates the duration in seconds.
  * @param block
  */
 export function calculateDuration(block: IBlock): number {
-  if (block.duration) return block.duration;
-  const repetitions = block.repetitions || 1;
-  const rest = block.rest || 0;
+  if (block.duration) return block.duration
+  const repetitions = block.repetitions || 1
+  const rest = block.rest || 0
   if (block.blocks) {
     const subblockDuration = block.blocks.reduce(
       (accumulator, block) =>
         accumulator + (block.duration || calculateDuration(block)),
       0
-    );
-    return repetitions * (subblockDuration + rest);
+    )
+    return repetitions * (subblockDuration + rest)
   } else {
-    return 0;
+    return 0
   }
 }
 
@@ -27,7 +27,7 @@ export function calculateDuration(block: IBlock): number {
 export function formatTime(seconds: number) {
   return `${Math.floor(seconds / 60)}:${(seconds % 60)
     .toString()
-    .padStart(2, "0")}`;
+    .padStart(2, '0')}`
 }
 
 /**
@@ -42,7 +42,7 @@ export function printExercises(exercises: IExercise[]) {
         ? `${exercise.repetitions}x ${exercise.name}`
         : exercise.name
     )
-    .join(" - ");
+    .join(' - ')
 }
 
 /**
@@ -50,10 +50,10 @@ export function printExercises(exercises: IExercise[]) {
  * @param ratings
  */
 export function calculateRating(ratings: IRating[]) {
-  const numberOfRatings = ratings.length;
+  const numberOfRatings = ratings.length
   const sumOfRatings = ratings.reduce(
     (accumulator, rating) => accumulator + rating.value,
     0
-  );
-  return numberOfRatings ? sumOfRatings / numberOfRatings : "-";
+  )
+  return numberOfRatings ? sumOfRatings / numberOfRatings : '-'
 }

部分文件因文件數量過多而無法顯示