Sfoglia il codice sorgente

worked on the training input forms

Tomi Cvetic 4 anni fa
parent
commit
e95a275008

+ 62 - 0
frontend/src/training/__tests__/utils.test.ts

@@ -0,0 +1,62 @@
+import { diffDB } from '../utils'
+const testDate1 = new Date(2020, 1, 1)
+const testDate2 = new Date(2020, 2, 2)
+const typesFalsy = {
+  number: 0,
+  boolean: false,
+  string: '',
+  //date: testDate1,
+  array: [],
+  object: {},
+  undefined: undefined,
+  null: null
+}
+const expectedFalsy = {
+  number: 0,
+  boolean: false,
+  string: ''
+  //date: testDate1
+}
+const typesTruthy = {
+  number: 12,
+  boolean: true,
+  string: 'hello!',
+  //date: testDate2,
+  array: [1, 2, 3],
+  object: { a: 1, b: 2 },
+  undefined: 'not undefined',
+  null: 45
+}
+const dbId = 'dbid'
+const createId = '++createId'
+const updateId = '@@updateId'
+const deleteId = '--deleteId'
+
+const dbItem = {
+  id: dbId,
+  ...typesTruthy,
+  child: {
+    id: dbId,
+    ...typesTruthy
+  }
+}
+
+const updateItem = {
+  id: dbId,
+  ...typesTruthy
+}
+
+describe('diffDB: Find differences of current state to database', () => {
+  it('returns undefined if there are no differences', () => {
+    const truthyNoDiffs = diffDB(typesTruthy, typesTruthy)
+    expect(truthyNoDiffs).toBe(undefined)
+    const falsyNoDiffs = diffDB(typesFalsy, typesFalsy)
+    expect(falsyNoDiffs).toBe(undefined)
+  })
+  it('diffs types correctly', () => {
+    const truthyDiff = diffDB(typesTruthy, typesFalsy)
+    expect(truthyDiff).toEqual(typesTruthy)
+    const falsyDiff = diffDB(typesFalsy, typesTruthy)
+    expect(falsyDiff).toEqual(expectedFalsy)
+  })
+})

+ 23 - 6
frontend/src/training/components/BlockInputs.tsx

@@ -1,9 +1,15 @@
 import FormatSelector from './FormatSelector'
 import { TextInput } from '../../form'
 import BlockInstanceInputs from './BlockInstanceInputs'
-import { emptyBlockInstance } from '../utils'
+import {
+  emptyBlockInstance,
+  emptyExercise,
+  emptyExerciseInstance
+} from '../utils'
 import ExerciseInstanceInputs from './ExerciseInstanceInputs'
 import { TBlock } from '../types'
+import { useState, ChangeEvent } from 'react'
+import BlockSelector from './BlockSelector'
 
 interface IBlockInputs {
   onChange: GenericEventHandler
@@ -14,6 +20,15 @@ interface IBlockInputs {
 const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
   return (
     <>
+      <p>
+        {value.id} {value.title}
+      </p>
+      <BlockSelector
+        name={name}
+        value={value}
+        label='Existing block'
+        onChange={onChange}
+      />
       <TextInput
         name={`${name}.title`}
         label='Title'
@@ -74,7 +89,7 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
       <label>Exercises</label>
       {value.exercises && value.exercises.length > 0 && (
         <ExerciseInstanceInputs
-          name={`${name}.blocks`}
+          name={`${name}.exercises`}
           value={value.exercises}
           onChange={onChange}
         />
@@ -82,14 +97,16 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
       <button
         onClick={event => {
           event.preventDefault()
-          const newExercise = empty({
-            order: value.blocks ? value.blocks.length : 0
+          const newExercise = emptyExerciseInstance({
+            order: value.exercises ? value.exercises.length : 0
           })
           onChange({
             target: {
               type: 'custom',
-              name: `${name}.blocks`,
-              value: value.blocks ? [...value.blocks, newBlock] : [newBlock]
+              name: `${name}.exercises`,
+              value: value.exercises
+                ? [...value.exercises, newExercise]
+                : [newExercise]
             }
           })
         }}

+ 64 - 0
frontend/src/training/components/BlockSelector.tsx

@@ -0,0 +1,64 @@
+import { useBlocksQuery } from '../../gql'
+import { useState, useEffect } from 'react'
+import { TBlock } from '../types'
+
+interface IBlockSelector {
+  value?: TBlock
+  onChange: GenericEventHandler
+  name?: string
+  label?: string
+}
+
+const BlockSelector = ({
+  value,
+  onChange,
+  name = 'block',
+  label = 'Block'
+}: IBlockSelector) => {
+  const [state, setState] = useState(value?.id ?? '')
+  const blocks = useBlocksQuery()
+
+  useEffect(() => {
+    setState(value?.id || '')
+  }, [value])
+
+  return (
+    <>
+      <label>{label}</label>
+      <select
+        id={name}
+        name={name}
+        value={state}
+        onChange={ev => setState(ev.target.value)}
+      >
+        {blocks.loading && 'loading blocks...'}
+        {blocks.error && 'error loading blocks'}
+        {blocks.data &&
+          blocks.data.blocks.map(block => (
+            <option key={block.id} value={block.id}>
+              {[block.title, block.description?.slice(0, 60)]
+                .filter(block => !!block)
+                .join(' - ')}
+            </option>
+          ))}
+      </select>
+      <button
+        type='button'
+        onClick={event => {
+          const changeEvent: CustomChangeEvent = {
+            target: {
+              type: 'custom',
+              value: { id: state },
+              name
+            }
+          }
+          onChange(changeEvent)
+        }}
+      >
+        Use
+      </button>
+    </>
+  )
+}
+
+export default BlockSelector

+ 6 - 2
frontend/src/training/components/EditTraining.tsx

@@ -4,7 +4,7 @@ import {
   emptyTraining,
   emptyBlockInstance,
   transformArrayToDB,
-  diffDB
+  KdiffDB
 } from '../utils'
 import TrainingTypeSelector from './TrainingTypeSelector'
 import BlockInstanceInputs from './BlockInstanceInputs'
@@ -29,7 +29,11 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
           console.log({ newValues })
           createTraining({ variables: newValues })
         } else {
-          const { id, ...changes } = diffDB(values, training || emptyTraining())
+          console.log(values, training, typeof KdiffDB)
+          const { id, ...changes } = KdiffDB(
+            values,
+            training || emptyTraining()
+          )
           const newValues = transform(changes, transformArrayToDB)
           if (Object.keys(newValues).length > 0) {
             console.log('saving changes', changes, newValues)

+ 7 - 3
frontend/src/training/components/ExerciseComposition.tsx

@@ -1,8 +1,8 @@
 import { formatTime, printExercises } from '../utils'
-import { IExercise } from '../types'
+import { TExerciseInstance } from '../types'
 
 export interface IExerciseComposition {
-  exercises: IExercise[]
+  exercises: TExerciseInstance[]
   duration: number
 }
 
@@ -11,7 +11,7 @@ const ExerciseComposition = ({ exercises, duration }: IExerciseComposition) => {
 
   return (
     <div className='exercise-composition'>
-      <span>{exerciseString}</span>
+      <span className='exercise-name'>{exerciseString}</span>
       <span className='exercise-time'>{formatTime(duration)}</span>
 
       <style jsx>
@@ -23,6 +23,10 @@ const ExerciseComposition = ({ exercises, duration }: IExerciseComposition) => {
           .exercise-composition .exercise-time {
             text-align: right;
           }
+          .exercise-time {
+            color: gray;
+            font-size: 80%;
+          }
         `}
       </style>
     </div>

+ 8 - 0
frontend/src/training/components/ExerciseInputs.tsx

@@ -1,5 +1,6 @@
 import { TextInput } from '../../form'
 import { TExercise } from '../types'
+import ExerciseSelector from './ExerciseSelector'
 
 interface IExerciseInputs {
   onChange: GenericEventHandler
@@ -10,6 +11,13 @@ interface IExerciseInputs {
 const ExerciseInputs = ({ onChange, value, name }: IExerciseInputs) => {
   return (
     <>
+      <p>ex: {value.id}</p>
+      <ExerciseSelector
+        name={name}
+        value={value}
+        label='Existing exercise'
+        onChange={onChange}
+      />
       <TextInput
         name={`${name}.name`}
         label='Name'

+ 2 - 2
frontend/src/training/components/ExerciseInstanceInputs.tsx

@@ -80,7 +80,7 @@ const ExerciseInstanceInputs = ({
           />
           {item.exercise && (
             <ExerciseInputs
-              name={`${name}.${itemIndex}.block`}
+              name={`${name}.${itemIndex}.exercise`}
               value={item.exercise}
               onChange={onChange}
             />
@@ -100,7 +100,7 @@ const ExerciseInstanceInputs = ({
               updateOrderProperty(newValues, newOrder)
             }}
           >
-            Delete block
+            Delete Exercise
           </button>
         </div>
       )

+ 66 - 0
frontend/src/training/components/ExerciseSelector.tsx

@@ -0,0 +1,66 @@
+import { useExercisesQuery } from '../../gql'
+import { useState, useEffect } from 'react'
+import { TExercise } from '../types'
+
+interface IExerciseSelector {
+  value?: TExercise
+  onChange: GenericEventHandler
+  name?: string
+  label?: string
+}
+
+const ExerciseSelector = ({
+  value,
+  onChange,
+  name = 'exercise',
+  label = 'Exercise'
+}: IExerciseSelector) => {
+  const [state, setState] = useState(value?.id ?? '')
+  const exercises = useExercisesQuery()
+
+  useEffect(() => {
+    setState(value?.id || '')
+  }, [value])
+
+  return (
+    <>
+      <label>{label}</label>
+      <select
+        id={name}
+        name={name}
+        value={state}
+        onChange={ev => setState(ev.target.value)}
+      >
+        {exercises.loading && 'loading exercises...'}
+        {exercises.error && 'error loading exercises'}
+        {exercises.data &&
+          exercises.data.exercises.map(exercise => (
+            <option key={exercise.id} value={exercise.id}>
+              {[exercise.name, exercise.description?.slice(0, 60)]
+                .filter(exercise => !!exercise)
+                .join(' - ')}
+            </option>
+          ))}
+      </select>
+      <button
+        type='button'
+        onClick={event => {
+          const changeEvent: CustomChangeEvent = {
+            target: {
+              type: 'custom',
+              value: exercises.data?.exercises.find(
+                exercise => exercise.id === state
+              ),
+              name
+            }
+          }
+          onChange(changeEvent)
+        }}
+      >
+        Use
+      </button>
+    </>
+  )
+}
+
+export default ExerciseSelector

+ 2 - 2
frontend/src/training/components/FormatSelector.tsx

@@ -43,7 +43,7 @@ const FormatSelector = ({
           const changeEvent: CustomChangeEvent = {
             target: {
               type: 'custom',
-              value: { connect: { id: event.target.value } },
+              value: { id: event.target.value },
               name
             }
           }
@@ -74,7 +74,7 @@ const FormatSelector = ({
               const changeEvent: CustomChangeEvent = {
                 target: {
                   type: 'custom',
-                  value: { connect: { id: result.data.createFormat.id } },
+                  value: { id: result.data.createFormat.id },
                   name
                 }
               }

+ 11 - 9
frontend/src/training/components/Training.tsx

@@ -2,12 +2,10 @@ import theme from '../../styles/theme'
 
 import TrainingBlock from './TrainingBlock'
 import Link from 'next/link'
-import { ITraining } from '../types'
+import { TTraining } from '../types'
 import TrainingMeta from './TrainingMeta'
-import { useRouter } from 'next/router'
-import { useTrainingLazyQuery } from '../../gql'
 
-const Training = ({ training }: { training: ITraining }) => {
+const Training = ({ training }: { training: TTraining }) => {
   return (
     <article>
       <h2>{training.title}</h2>
@@ -16,13 +14,16 @@ const Training = ({ training }: { training: ITraining }) => {
 
       <section>
         <h2>Program</h2>
-        <Link href='/timer'>
-          <button>Start Timer</button>
+        <Link href='/timer/[id]' as={`/timer/${training.id}`}>
+          <button type='button'>Start Timer</button>
         </Link>
         {training.blocks &&
           training.blocks
-            .sort(block => block.sequence || 0)
-            .map(block => <TrainingBlock key={block.id} block={block} />)}
+            .slice()
+            .sort((a, b) => a.order - b.order)
+            .map(block => (
+              <TrainingBlock key={block.id} blockInstance={block} />
+            ))}
       </section>
 
       <style jsx>
@@ -34,9 +35,10 @@ const Training = ({ training }: { training: ITraining }) => {
               '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;
+            background-position: center 0;
             margin: 2em 0;
           }
 

+ 30 - 17
frontend/src/training/components/TrainingBlock.tsx

@@ -1,20 +1,23 @@
-import ExerciseComposition from "./ExerciseComposition";
-import { calculateDuration, formatTime } from "../utils";
-import { IBlock } from "../types";
+import ExerciseComposition from './ExerciseComposition'
+import { calculateDuration, formatTime } from '../utils'
+import { TBlockInstance } from '../types'
 
-const TrainingBlock = ({ block }: { block: IBlock }) => {
-  const duration = calculateDuration(block);
+const TrainingBlock = ({
+  blockInstance
+}: {
+  blockInstance: TBlockInstance
+}) => {
+  const duration = calculateDuration(blockInstance)
+  const { title, blocks, exercises } = blockInstance.block
   return (
     <div>
-      {block.title && (
-        <h3>
-          {block.title} ({formatTime(duration)})
-        </h3>
-      )}
-      {block.blocks &&
-        block.blocks.map(block => <TrainingBlock block={block} />)}
-      {block.exercises && (
-        <ExerciseComposition exercises={block.exercises} duration={duration} />
+      <h3>
+        <span className='block-title'>{title}</span>{' '}
+        <span className='block-time'>{formatTime(duration)}</span>
+      </h3>
+      {blocks && blocks.map(block => <TrainingBlock blockInstance={block} />)}
+      {exercises && (
+        <ExerciseComposition exercises={exercises} duration={duration} />
       )}
 
       <style jsx>
@@ -22,9 +25,19 @@ const TrainingBlock = ({ block }: { block: IBlock }) => {
           section {
             display: grid;
           }
+          .block-time {
+            color: gray;
+            font-size: 90%;
+          }
+          .block-time::before {
+            content: '(';
+          }
+          .block-time::after {
+            content: ')';
+          }
         `}
       </style>
     </div>
-  );
-};
-export default TrainingBlock;
+  )
+}
+export default TrainingBlock

+ 38 - 20
frontend/src/training/components/TrainingMeta.tsx

@@ -1,28 +1,46 @@
-import { ITraining } from "../types";
-import { calculateRating } from "../utils";
+import { TTraining } from '../types'
+import { calculateRating } from '../utils'
+import { useContext } from 'react'
+import { UserContext } from '../../user/hooks'
+import { useRegisterMutation } from '../../gql'
+
+const TrainingMeta = ({ training }: { training: TTraining }) => {
+  const { user } = useContext(UserContext)
+  const [register, registerData] = useRegisterMutation({
+    variables: { training: training.id }
+  })
 
-const TrainingMeta = ({ training }: { training: ITraining }) => {
   return (
     <aside>
-      <div className="info">
-        <span className="caption">Type: </span>
-        <span className="data">{training.type.name}</span>
+      <div className='info'>
+        <span className='caption'>Type: </span>
+        <span className='data'>{training.type.name}</span>
       </div>
-      <div className="info">
-        <span className="caption">Date: </span>
-        <span className="data">
+      <div className='info'>
+        <span className='caption'>Date: </span>
+        <span className='data'>
           {new Date(training.trainingDate).toLocaleString()}
         </span>
       </div>
-      <div className="info">
-        <span className="caption">Location: </span>
-        <span className="data">{training.location}</span>
+      <div className='info'>
+        <span className='caption'>Location: </span>
+        <span className='data'>{training.location}</span>
       </div>
-      {/*<div className="info">
-        <span className="caption">Registrations: </span>
-        <span className="data">{training.registrations.length} </span>
+      <div className='info'>
+        <span className='caption'>Registrations: </span>
+        <span className='data'>{training.registrations?.length ?? 0}</span>
+        {training.registrations &&
+        training.registrations.find(
+          registeredUser =>
+            user?.data?.currentUser &&
+            registeredUser.id === user.data.currentUser.id
+        ) ? (
+          <button onClick={() => register()}>Deregister</button>
+        ) : (
+          <button onClick={() => register()}>Register now!</button>
+        )}
       </div>
-      <div className="info">
+      {/*<div className="info">
         <span className="caption">Attendance: </span>
         <span className="data">{training.attendance}</span>
       </div>
@@ -38,7 +56,7 @@ const TrainingMeta = ({ training }: { training: ITraining }) => {
           <a href="">*</a>
         </span>
       </div>
-          <button>Register now!</button>*/}
+          */}
 
       <style jsx>{`
         aside {
@@ -55,7 +73,7 @@ const TrainingMeta = ({ training }: { training: ITraining }) => {
         }
       `}</style>
     </aside>
-  );
-};
+  )
+}
 
-export default TrainingMeta;
+export default TrainingMeta

+ 1 - 1
frontend/src/training/components/TrainingTypeSelector.tsx

@@ -1,7 +1,7 @@
 import { useTrainingTypesQuery, TrainingType } from '../../gql'
-import { Modal } from '../../modal'
 import AddTrainingType from './AddTrainingType'
 import { useState, useEffect } from 'react'
+import { Modal } from '../../modal'
 
 interface ITrainingTypeSelector {
   value?: TrainingType

+ 125 - 15
frontend/src/training/training.graphql

@@ -1,19 +1,13 @@
 # import * from '../../../backend/database/generated/prisma.graphql'
 
-fragment exerciseContent on ExerciseInstance {
+fragment exerciseContent on Exercise {
   id
-  exercise {
-    id
-    name
-    description
-    videos
-    pictures
-    targets
-    baseExercise
-  }
-  order
-  repetitions
-  variation
+  name
+  description
+  videos
+  pictures
+  targets
+  baseExercise
 }
 
 fragment blockContent on Block {
@@ -39,7 +33,13 @@ fragment blockContent on Block {
     variation
   }
   exercises {
-    ...exerciseContent
+    id
+    exercise {
+      ...exerciseContent
+    }
+    order
+    repetitions
+    variation
   }
 }
 
@@ -60,7 +60,13 @@ fragment blockHint on Block {
     id
   }
   exercises {
-    ...exerciseContent
+    id
+    exercise {
+      ...exerciseContent
+    }
+    order
+    repetitions
+    variation
   }
 }
 
@@ -108,6 +114,98 @@ query training($id: ID!) {
   }
 }
 
+fragment displayTraining on Training {
+  id
+  title
+  type {
+    id
+    name
+    description
+  }
+  trainingDate
+  location
+  attendance
+  blocks {
+    ...displayBlockInstance
+    block {
+      ...displayBlock
+      blocks {
+        ...displayBlockInstance
+        block {
+          ...displayBlock
+          blocks {
+            ...displayBlockInstance
+            block {
+              ...displayBlock
+              blocks {
+                id
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  registrations {
+    id
+  }
+}
+
+fragment displayBlockInstance on BlockInstance {
+  id
+  order
+  rounds
+  variation
+}
+
+fragment displayBlock on Block {
+  id
+  title
+  description
+  videos
+  pictures
+  duration
+  format {
+    name
+    description
+  }
+  rest
+  exercises {
+    order
+    repetitions
+    variation
+    exercise {
+      ...displayExercise
+    }
+  }
+}
+
+fragment displayExerciseInstance on ExerciseInstance {
+  id
+  order
+  repetitions
+  variation
+  exercise {
+    ...displayExercise
+  }
+}
+
+fragment displayExercise on Exercise {
+  id
+  name
+  description
+  videos
+  pictures
+  targets
+  baseExercise
+}
+
+query publishedTrainings {
+  publishedTrainings {
+    ...displayTraining
+  }
+}
+
 query trainings {
   trainings {
     id
@@ -147,6 +245,18 @@ query formats {
   }
 }
 
+query blocks {
+  blocks {
+    ...blockContent
+  }
+}
+
+query exercises {
+  exercises {
+    ...exerciseContent
+  }
+}
+
 mutation createTraining(
   $title: String!
   $type: TrainingTypeCreateOneInput!

+ 41 - 63
frontend/src/training/types.ts

@@ -6,66 +6,44 @@ import {
   Exercise
 } from '../gql'
 
-export interface ITraining {
-  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[]
-}
-
-export type TTraining = Pick<Training, 'id'> & Partial<Omit<Training, 'id'>>
-export type TBlockInstance = Pick<BlockInstance, 'id'> &
-  Partial<Omit<BlockInstance, 'id'>>
-export type TBlock = Pick<Block, 'id'> & Partial<Omit<Block, 'id'>>
-export type TExerciseInstance = Pick<ExerciseInstance, 'id'> &
-  Partial<Omit<ExerciseInstance, 'id'>>
-export type TExercise = Pick<Exercise, 'id'> & Partial<Omit<Exercise, 'id'>>
-
-export interface IBlock {
-  id: string
-  sequence?: number
-  title?: string
-  description?: string
-  video?: string
-  duration?: number
-  repetitions?: number
-  rest?: number
-  format?: IFormat
-  blocks?: IBlock[]
-  exercises?: IExercise[]
-}
-
-export interface IFormat {
-  id: string
-  name: string
-  description: string
-}
-
-export interface IExercise {
-  id: string
-  name: string
-  description: string
-  repetitions: number
-  videos: string[]
-  pictures: string[]
-  targets: string[]
-  baseExercise: {
-    id: string
-    name: string
-  }
-}
-
-export interface IRating {
-  value: number
-}
+export type TTraining = Pick<Training, 'id' | 'type'> &
+  Partial<Omit<Training, 'id' | 'type'>>
+export type TBlockInstance = Pick<BlockInstance, 'id' | 'block' | 'order'> &
+  Partial<Omit<BlockInstance, 'id' | 'block' | 'order'>>
+export type TBlock = Pick<
+  Block,
+  | 'id'
+  | 'title'
+  | 'exercises'
+  | 'videos'
+  | 'blocks'
+  | 'tracks'
+  | 'pictures'
+  | 'format'
+> &
+  Partial<
+    Omit<
+      Block,
+      | 'id'
+      | 'title'
+      | 'exercises'
+      | 'videos'
+      | 'blocks'
+      | 'tracks'
+      | 'pictures'
+      | 'format'
+    >
+  >
+export type TExerciseInstance = Pick<ExerciseInstance, 'id' | 'exercise'> &
+  Partial<Omit<ExerciseInstance, 'id' | 'exercise'>>
+export type TExercise = Pick<
+  Exercise,
+  'id' | 'name' | 'pictures' | 'videos' | 'targets' | 'baseExercise'
+> &
+  Partial<
+    Omit<
+      Exercise,
+      'id' | 'name' | 'pictures' | 'videos' | 'targets' | 'baseExercise'
+    >
+  >
+export type TRating = { value: number }

+ 66 - 55
frontend/src/training/utils.ts

@@ -1,20 +1,19 @@
 import { parse } from 'date-fns'
 import {
-  IBlock,
-  IExercise,
-  IRating,
   TTraining,
   TExerciseInstance,
   TBlockInstance,
   TBlock,
-  TExercise
+  TExercise,
+  TRating
 } from './types'
 import {
   TrainingQuery,
   SubBlockFragment,
-  BlockContentFragment,
-  Training,
-  ExerciseContentFragment
+  Exercise,
+  Block,
+  ExerciseInstance,
+  BlockInstance
 } from '../gql'
 import { isArray, transform, isEqual, isObject } from 'lodash'
 
@@ -22,20 +21,21 @@ import { isArray, transform, isEqual, isObject } from 'lodash'
  * 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.blocks) {
-    const subblockDuration = block.blocks.reduce(
-      (accumulator, block) =>
-        accumulator + (block.duration || calculateDuration(block)),
+export function calculateDuration(
+  blocks?: TBlockInstance | TBlockInstance[]
+): number {
+  if (!blocks) return 0
+  const blocksArray = isArray(blocks) ? blocks : [blocks]
+  const duration = blocksArray.map(blockInstance => {
+    const blockRounds = blockInstance.rounds ?? 1
+    const blockDuration =
+      blockInstance.block?.duration ??
+      calculateDuration(blockInstance.block?.blocks) ??
       0
-    )
-    return repetitions * (subblockDuration + rest)
-  } else {
-    return 0
-  }
+    const blockRest = blockInstance.block?.rest ?? 0
+    return blockRounds * (blockDuration + blockRest)
+  })
+  return duration.reduce((a, b) => a + b, 0)
 }
 
 /**
@@ -53,12 +53,12 @@ export function formatTime(seconds: number) {
  * 4x Exercise 1 - Exercise 2 - 2x Exercise 3
  * @param exercises
  */
-export function printExercises(exercises: IExercise[]) {
+export function printExercises(exercises: TExerciseInstance[]) {
   return exercises
-    .map(exercise =>
-      exercise.repetitions > 1
-        ? `${exercise.repetitions}x ${exercise.name}`
-        : exercise.name
+    .map(exerciseInstance =>
+      exerciseInstance.repetitions && exerciseInstance.repetitions > 1
+        ? `${exerciseInstance.repetitions}x ${exerciseInstance.exercise?.name}`
+        : exerciseInstance.exercise?.name
     )
     .join(' - ')
 }
@@ -67,7 +67,7 @@ export function printExercises(exercises: IExercise[]) {
  * Takes an array of rating and calculates the average rating
  * @param ratings
  */
-export function calculateRating(ratings: IRating[]) {
+export function calculateRating(ratings: TRating[]) {
   const numberOfRatings = ratings.length
   const sumOfRatings = ratings.reduce(
     (accumulator, rating) => accumulator + rating.value,
@@ -83,8 +83,6 @@ export function trainingDBToArray(DBTraining: TrainingQuery['training']) {
   return { ...data, blocks: blockDBToArray(blocks) }
 }
 
-export function trainingArrayToDB() {}
-
 export function blockDBToArray(DBSubBlock?: SubBlockFragment[]) {
   console.log({ DBSubBlock })
   if (!DBSubBlock) return undefined
@@ -123,8 +121,8 @@ function randomID() {
     .substr(0, 10)}`
 }
 
-export function emptyExercise(input?: TExercise) {
-  const emptyExercise = {
+export function emptyExercise(input?: Partial<Exercise>) {
+  const emptyExercise: TExercise = {
     id: randomID(),
     name: '',
     description: '',
@@ -136,8 +134,8 @@ export function emptyExercise(input?: TExercise) {
   return { ...emptyExercise, ...input }
 }
 
-export function emptyExerciseInstance(input?: TExerciseInstance) {
-  const emptyExerciseInstance = {
+export function emptyExerciseInstance(input?: Partial<ExerciseInstance>) {
+  const emptyExerciseInstance: TExerciseInstance = {
     id: randomID(),
     order: 0,
     exercise: emptyExercise()
@@ -145,8 +143,8 @@ export function emptyExerciseInstance(input?: TExerciseInstance) {
   return { ...emptyExerciseInstance, ...input }
 }
 
-export function emptyBlock(input?: TBlock) {
-  const emptyBlock = {
+export function emptyBlock(input?: Partial<Block>) {
+  const emptyBlock: TBlock = {
     id: randomID(),
     title: '',
     format: { id: '', name: '', description: '' },
@@ -158,8 +156,8 @@ export function emptyBlock(input?: TBlock) {
   return { ...emptyBlock, ...input }
 }
 
-export function emptyBlockInstance(input?: TBlockInstance) {
-  const emptyBlockInstance = {
+export function emptyBlockInstance(input?: Partial<BlockInstance>) {
+  const emptyBlockInstance: TBlockInstance = {
     id: randomID(),
     block: emptyBlock(),
     order: 0
@@ -168,7 +166,7 @@ export function emptyBlockInstance(input?: TBlockInstance) {
 }
 
 export function emptyTraining(input?: TTraining) {
-  const emptyTraining = {
+  const emptyTraining: TTraining = {
     id: randomID(),
     title: '',
     type: { id: '', name: '', description: '' },
@@ -180,11 +178,12 @@ export function emptyTraining(input?: TTraining) {
   return { ...emptyTraining, ...input }
 }
 
-export function collectMutationCreateConnect(arr: any[]) {
+export function collectMutations(arr: any[]) {
   const create: any[] = []
   const connect: any[] = []
   const del: any[] = []
   const update: any[] = []
+  console.log('collect', arr)
   arr.forEach(val => {
     if (typeof val === 'object' && val['connect']) connect.push(val['connect'])
     if (typeof val === 'object' && val['create']) create.push(val['create'])
@@ -204,17 +203,19 @@ export function collectMutationCreateConnect(arr: any[]) {
   return returnObject
 }
 
+function isNotInDB(key: any, val: any) {
+  return (
+    key === '__typename' ||
+    (key === 'id' && typeof val === 'string' && val.startsWith('++'))
+  )
+}
+
 export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
-  if (key === '__typename') {
-    // remove the typename from the database
-    return
-  } else if (key === 'id' && typeof val === 'string' && val.startsWith('++')) {
-    // remove placeholder IDs
-    return
-  } else if (isArray(val)) {
-    // collect 'create' and 'connect' statements
-    acc[key] = collectMutationCreateConnect(transform(val, transformArrayToDB))
+  if (isNotInDB(key, val)) return
+  if (isArray(val)) {
+    acc[key] = collectMutations(transform(val, transformArrayToDB))
   } else if (typeof val === 'object' && !!val) {
+    console.log('object', key, val)
     // we found an object!
     if (!!val['id'] && val['id'].startsWith('++')) {
       // values with placeholder IDs are preserved
@@ -223,7 +224,6 @@ export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
       // IDs starting with -- are for deletion
       acc[key] = { delete: { id: val['id'].substr(2) } }
     } else {
-      // values with real IDs are just connected
       const { id, ...data } = val
       if (id?.startsWith('@@')) {
         if (typeof key === 'string') {
@@ -236,7 +236,6 @@ export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
             }
           }
         }
-        console.log('update candidate', key, id, data, acc[key].update.data)
       } else {
         acc[key] = { connect: { id } }
       }
@@ -247,19 +246,31 @@ export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
   }
 }
 
-export function diffDB(newObject: any = {}, oldObject: any = {}) {
+export function KdiffDB(newObject: any = {}, oldObject: any = {}) {
   const transformResult = transform(
     newObject,
     (result: any, value: any, key: string) => {
-      if (key === 'id') {
-        if (isEqual(value, oldObject[key])) result[key] = `@@${value}`
-      } else if (!isEqual(value, oldObject[key])) {
+      /*if (key === 'id') {
+        result[key] = value
+      } else*/ if (
+        !isEqual(value, oldObject[key])
+      ) {
         const newValue =
           isObject(value) && isObject(oldObject[key])
-            ? diffDB(value, oldObject[key])
+            ? KdiffDB(value, oldObject[key])
             : value
         if (newValue !== undefined && newValue !== null) {
-          result[key] = newValue
+          if (isEqual(key, 'id')) {
+            if (result[key] !== undefined) return
+          } else {
+            result[key] = newValue
+            if (
+              oldObject['id'] !== undefined &&
+              oldObject['id'] === newObject['id']
+            ) {
+              result['id'] = `@@${oldObject['id']}`
+            }
+          }
         }
       }
     }