Quellcode durchsuchen

adding exercises

Tomi Cvetic vor 4 Jahren
Ursprung
Commit
80e2af0636

+ 2 - 0
backend/schema.graphql

@@ -100,4 +100,6 @@ type Mutation {
   userSignup(name: String!, email: String!, password: String!): User!
   requestReset(email: String!): String!
   resetPassword(token: String!, password: String!): User!
+  register(training: ID!): String!
+  deregister(training: ID!): String!
 }

+ 25 - 1
backend/src/training/resolvers.ts

@@ -2,7 +2,6 @@
 
 import { IResolvers } from 'apollo-server-express'
 import { checkPermission } from '../user/resolvers'
-import { inspect } from 'util'
 //const LoginError = new Error('You must be logged in.')
 //const PermissionError = new Error('Insufficient permissions.')
 
@@ -14,6 +13,7 @@ export const resolvers: IResolvers = {
     },
     trainings: async (parent, args, context, info) => {
       checkPermission(context)
+      console.log(info)
       return context.db.query.trainings({}, info)
     },
     trainingTypes: async (parent, args, context, info) => {
@@ -61,6 +61,30 @@ export const resolvers: IResolvers = {
       checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
       const block = await context.db.mutation.createFormat({ data: args }, info)
       return block
+    },
+    register: async (parent, args, context, info) => {
+      checkPermission(context)
+      const training = await context.db.query.training(
+        { where: { id: args.training } },
+        '{ id }'
+      )
+      if (!training) throw Error(`Training ${args.training} not found`)
+      return context.db.mutation.updateTraining({
+        where: { id: training.id },
+        data: { registrations: { connect: { id: context.req.userId } } }
+      })
+    },
+    deregister: async (parent, args, context, info) => {
+      checkPermission(context)
+      const training = await context.db.query.training(
+        { where: { id: args.training } },
+        '{ id }'
+      )
+      if (!training) throw Error(`Training ${args.training} not found`)
+      return context.db.mutation.updateTraining({
+        where: { id: training.id },
+        data: { registrations: { disconnect: { id: context.req.userId } } }
+      })
     }
   }
 }

+ 7 - 3
frontend/pages/_app.tsx

@@ -4,6 +4,8 @@ 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 { UserProvider } from '../src/user/hooks'
+import { useCurrentUserQuery } from '../src/gql'
 
 class MyApp extends App {
   static async getInitialProps({ Component, ctx }: any) {
@@ -24,9 +26,11 @@ class MyApp extends App {
 
     return (
       <ApolloProvider client={client}>
-        <Page>
-          <Component {...pageProps} />
-        </Page>
+        <UserProvider>
+          <Page>
+            <Component {...pageProps} />
+          </Page>
+        </UserProvider>
       </ApolloProvider>
     )
   }

+ 61 - 3
frontend/src/gql/index.tsx

@@ -1508,6 +1508,8 @@ export type Mutation = {
   userSignup: User,
   requestReset: Scalars['String'],
   resetPassword: User,
+  register: Scalars['String'],
+  deregister: Scalars['String'],
 };
 
 
@@ -1579,6 +1581,16 @@ export type MutationResetPasswordArgs = {
   password: Scalars['String']
 };
 
+
+export type MutationRegisterArgs = {
+  training: Scalars['ID']
+};
+
+
+export type MutationDeregisterArgs = {
+  training: Scalars['ID']
+};
+
 /** An object with an ID */
 export type Node = {
   /** The id of the object. */
@@ -3337,15 +3349,15 @@ export type TrainingQueryVariables = {
 
 export type TrainingQuery = { training: Maybe<(
     Pick<Training, 'id' | 'title' | 'createdAt' | 'trainingDate' | 'location' | 'attendance' | 'published'>
-    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockFragment>> }
+    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockFragment>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
   )> };
 
 export type TrainingsQueryVariables = {};
 
 
 export type TrainingsQuery = { trainings: Array<(
-    Pick<Training, 'id' | 'title' | 'trainingDate' | 'location' | 'published'>
-    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockHintFragment>> }
+    Pick<Training, 'id' | 'title' | 'trainingDate' | 'location' | 'attendance' | 'published'>
+    & { type: Pick<TrainingType, 'id' | 'name' | 'description'>, blocks: Maybe<Array<SubBlockHintFragment>>, registrations: Maybe<Array<Pick<User, 'id' | 'name'>>> }
   )> };
 
 export type TrainingTypesQueryVariables = {};
@@ -3395,6 +3407,13 @@ export type CreateFormatMutationVariables = {
 
 export type CreateFormatMutation = { createFormat: Pick<Format, 'id'> };
 
+export type RegisterMutationVariables = {
+  training: Scalars['ID']
+};
+
+
+export type RegisterMutation = Pick<Mutation, 'register'>;
+
 export type UsersQueryVariables = {};
 
 
@@ -3565,6 +3584,10 @@ export const TrainingDocument = gql`
     blocks {
       ...subBlock
     }
+    registrations {
+      id
+      name
+    }
   }
 }
     ${SubBlockFragmentDoc}`;
@@ -3606,10 +3629,15 @@ export const TrainingsDocument = gql`
     }
     trainingDate
     location
+    attendance
     published
     blocks {
       ...subBlockHint
     }
+    registrations {
+      id
+      name
+    }
   }
 }
     ${SubBlockHintFragmentDoc}`;
@@ -3843,6 +3871,36 @@ export function useCreateFormatMutation(baseOptions?: ApolloReactHooks.MutationH
 export type CreateFormatMutationHookResult = ReturnType<typeof useCreateFormatMutation>;
 export type CreateFormatMutationResult = ApolloReactCommon.MutationResult<CreateFormatMutation>;
 export type CreateFormatMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateFormatMutation, CreateFormatMutationVariables>;
+export const RegisterDocument = gql`
+    mutation register($training: ID!) {
+  register(training: $training)
+}
+    `;
+export type RegisterMutationFn = ApolloReactCommon.MutationFunction<RegisterMutation, RegisterMutationVariables>;
+
+/**
+ * __useRegisterMutation__
+ *
+ * To run a mutation, you first call `useRegisterMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useRegisterMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [registerMutation, { data, loading, error }] = useRegisterMutation({
+ *   variables: {
+ *      training: // value for 'training'
+ *   },
+ * });
+ */
+export function useRegisterMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<RegisterMutation, RegisterMutationVariables>) {
+        return ApolloReactHooks.useMutation<RegisterMutation, RegisterMutationVariables>(RegisterDocument, baseOptions);
+      }
+export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
+export type RegisterMutationResult = ApolloReactCommon.MutationResult<RegisterMutation>;
+export type RegisterMutationOptions = ApolloReactCommon.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
 export const UsersDocument = gql`
     query Users {
   users {

+ 34 - 23
frontend/src/lib/arrays.ts

@@ -12,31 +12,38 @@ export function diff(newObject: any = {}, oldObject: any = {}, ignore?: Dict) {
         [] as string[]
       )
     : []
-  const transformResult = transform(newObject, (result: any, value: any, key: string) => {
-    if (!currentIgnoreList.includes(key) && !isEqual(value, oldObject[key])) {
-      const child = ignore && ignore[key]
-      let childIgnoreList
-      if (result instanceof Array) {
-        childIgnoreList = ignore
-      } else if (typeof child !== 'boolean') {
-        childIgnoreList = child
-      } else {
-        childIgnoreList = undefined
+  const transformResult = transform(
+    newObject,
+    (result: any, value: any, key: string) => {
+      if (!currentIgnoreList.includes(key) && !isEqual(value, oldObject[key])) {
+        const child = ignore && ignore[key]
+        let childIgnoreList
+        if (result instanceof Array) {
+          childIgnoreList = ignore
+        } else if (typeof child !== 'boolean') {
+          childIgnoreList = child
+        } else {
+          childIgnoreList = undefined
+        }
+        const newValue =
+          isObject(value) && isObject(oldObject[key])
+            ? diff(value, oldObject[key], childIgnoreList)
+            : value
+        if (newValue !== undefined && newValue !== null) result[key] = newValue
       }
-      const newValue =
-        isObject(value) && isObject(oldObject[key])
-          ? diff(value, oldObject[key], childIgnoreList)
-          : value
-      if (typeof newValue === 'boolean' || !!newValue) result[key] = newValue
     }
-  })
+  )
   return Object.keys(transformResult).length > 0 ? transformResult : undefined
 }
 
-export function deepDiff(newObject: any = {}, oldObject: any = {}, ignore?: Dict) {
+export function deepDiff(
+  newObject: any = {},
+  oldObject: any = {},
+  ignore?: Dict
+) {
   return {
     added: diff(newObject, oldObject, ignore),
-    removed: diff(oldObject, newObject, ignore),
+    removed: diff(oldObject, newObject, ignore)
   }
 }
 
@@ -49,7 +56,11 @@ interface IcompareOptions<T> {
   }
 }
 
-export function compare<T>(objectsA: T[], objectsB: T[], compareOptions: IcompareOptions<T>) {
+export function compare<T>(
+  objectsA: T[],
+  objectsB: T[],
+  compareOptions: IcompareOptions<T>
+) {
   const { keyNames, diffIgnore, compareKeys } = compareOptions
   const { inA, inB } = keyNames || { inA: 'inFile', inB: 'inDb' }
   let copyB = [...objectsB]
@@ -69,8 +80,8 @@ export function compare<T>(objectsA: T[], objectsB: T[], compareOptions: Icompar
         ...objectA,
         changes: deepDiff(objectA, objectB, diffIgnore),
         [inA]: true,
-        [inB]: !!objectB,
-      },
+        [inB]: !!objectB
+      }
     ]
   }
   for (let objectB of copyB) {
@@ -80,8 +91,8 @@ export function compare<T>(objectsA: T[], objectsB: T[], compareOptions: Icompar
         ...objectB,
         changes: deepDiff(undefined, objectB, diffIgnore),
         [inA]: false,
-        [inB]: true,
-      },
+        [inB]: true
+      }
     ]
   }
   return copyA

+ 34 - 10
frontend/src/training/components/BlockInputs.tsx

@@ -1,12 +1,13 @@
-import { BlockContentFragment } from '../../gql'
 import FormatSelector from './FormatSelector'
 import { TextInput } from '../../form'
 import BlockInstanceInputs from './BlockInstanceInputs'
 import { emptyBlockInstance } from '../utils'
+import ExerciseInstanceInputs from './ExerciseInstanceInputs'
+import { TBlock } from '../types'
 
 interface IBlockInputs {
   onChange: GenericEventHandler
-  value: Partial<BlockContentFragment>
+  value: TBlock
   name: string
 }
 
@@ -19,14 +20,6 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         value={value.title}
         onChange={onChange}
       />
-      <FormatSelector
-        name={`${name}.format`}
-        value={value.format}
-        onChange={data => {
-          console.log({ data })
-          onChange(data)
-        }}
-      />
       <TextInput
         name={`${name}.description`}
         label='Description'
@@ -40,6 +33,11 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
         type='number'
         onChange={onChange}
       />
+      <FormatSelector
+        name={`${name}.format`}
+        value={value.format}
+        onChange={onChange}
+      />
       <TextInput
         name={`${name}.rest`}
         label='Rest'
@@ -73,6 +71,32 @@ const BlockInputs = ({ onChange, value, name }: IBlockInputs) => {
       >
         Add block
       </button>
+      <label>Exercises</label>
+      {value.exercises && value.exercises.length > 0 && (
+        <ExerciseInstanceInputs
+          name={`${name}.blocks`}
+          value={value.exercises}
+          onChange={onChange}
+        />
+      )}
+      <button
+        onClick={event => {
+          event.preventDefault()
+          const newExercise = empty({
+            order: value.blocks ? value.blocks.length : 0
+          })
+          onChange({
+            target: {
+              type: 'custom',
+              name: `${name}.blocks`,
+              value: value.blocks ? [...value.blocks, newBlock] : [newBlock]
+            }
+          })
+        }}
+        type='button'
+      >
+        Add exercise
+      </button>
     </>
   )
 }

+ 34 - 41
frontend/src/training/components/BlockInstanceInputs.tsx

@@ -2,20 +2,31 @@ import { useState, useEffect } from 'react'
 import arrayMove from 'array-move'
 import BlockInputs from './BlockInputs'
 import { SortableList } from '../../sortable'
-import { SubBlockFragment } from '../../gql'
 import { TextInput } from '../../form'
+import { TBlockInstance } from '../types'
 
 const BlockInstanceInputs = ({
   value = [],
   name,
   onChange
 }: {
-  value?: Partial<SubBlockFragment>[]
+  value?: TBlockInstance[]
   name: string
   onChange: GenericEventHandler
 }) => {
   const [state, setState] = useState(value.map(item => item.id))
 
+  function updateOrderProperty<T extends { id: U }, U>(
+    values: T[],
+    orderList: U[]
+  ) {
+    const orderedValues = values.map(value => {
+      const order = orderList.findIndex(orderedId => orderedId === value.id)
+      return { ...value, order }
+    })
+    onChange({ target: { type: 'custom', name, value: orderedValues } })
+  }
+
   function onSortEnd({
     oldIndex,
     newIndex
@@ -25,23 +36,13 @@ const BlockInstanceInputs = ({
   }) {
     const newOrder = arrayMove(state, oldIndex, newIndex)
     setState(newOrder)
-    const updatedValues = value.map(item => {
-      const order = newOrder.findIndex(orderedId => orderedId === item.id)
-      return { ...item, order }
-    })
-    onChange({
-      target: {
-        type: 'custom',
-        name,
-        value: updatedValues
-      }
-    })
+    updateOrderProperty(value, newOrder)
   }
 
   useEffect(() => {
     const missingIds = value
       .filter(item => !state.includes(item.id))
-      .filter(item => item.id && !item.id.startsWith('--'))
+      .filter(item => !item.id.startsWith('--'))
       .map(item => item.id)
     const stateWithoutRemovedItems = state.filter(stateId =>
       value.find(item => stateId === item.id)
@@ -56,18 +57,20 @@ const BlockInstanceInputs = ({
       const item = value[itemIndex]
       return (
         <div key={item.id}>
-          <p>
-            {item.order} {item.id}
-          </p>
+          <p>{item.id}</p>
+          <TextInput
+            name={`${name}.${itemIndex}.order`}
+            label='Order'
+            value={item.order}
+            type='number'
+            onChange={onChange}
+          />
           <TextInput
             name={`${name}.${itemIndex}.rounds`}
             label='Rounds'
             value={item.rounds}
             type='number'
-            onChange={data => {
-              console.log(data)
-              onChange(data)
-            }}
+            onChange={onChange}
           />
           <TextInput
             name={`${name}.${itemIndex}.variation`}
@@ -85,26 +88,16 @@ const BlockInstanceInputs = ({
           <button
             type='button'
             onClick={ev => {
-              const updatedValues: CustomChangeEvent = {
-                target: { type: 'custom', name, value }
-              }
-              if (item.id?.startsWith('++')) {
-                updatedValues.target.value = [
-                  ...value.slice(0, itemIndex),
-                  ...value.slice(itemIndex + 1)
-                ]
-              } else {
-                updatedValues.target.value = [
-                  ...value.slice(0, itemIndex),
-                  {
-                    ...item,
-                    id: `--${item.id}`
-                  },
-                  ...value.slice(itemIndex + 1)
-                ]
-              }
-              console.log(updatedValues)
-              onChange(updatedValues)
+              const newOrder = state.filter(orderedId => item.id !== orderedId)
+              setState(newOrder)
+              const newValues = item.id?.startsWith('++')
+                ? [...value.slice(0, itemIndex), ...value.slice(itemIndex + 1)]
+                : [
+                    ...value.slice(0, itemIndex),
+                    { ...item, id: `--${item.id}` },
+                    ...value.slice(itemIndex + 1)
+                  ]
+              updateOrderProperty(newValues, newOrder)
             }}
           >
             Delete block

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

@@ -1,11 +1,17 @@
 import { useCreateTrainingMutation, useUpdateTrainingMutation } from '../../gql'
 import { useForm, TextInput, DateTimeInput, Checkbox } from '../../form'
-import { emptyTraining, emptyBlockInstance, transformArrayToDB } from '../utils'
+import {
+  emptyTraining,
+  emptyBlockInstance,
+  transformArrayToDB,
+  diffDB
+} from '../utils'
 import TrainingTypeSelector from './TrainingTypeSelector'
 import BlockInstanceInputs from './BlockInstanceInputs'
 import { TTraining } from '../types'
 import { transform } from 'lodash'
-import { diff } from '../../lib/arrays'
+import Registrations from './Registrations'
+import Ratings from './Ratings'
 
 const EditTraining = ({ training }: { training?: TTraining }) => {
   const { values, touched, onChange, loadData } = useForm(
@@ -20,14 +26,13 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
         ev.preventDefault()
         if (values.id.startsWith('++')) {
           const newValues = transform(values, transformArrayToDB)
-          console.log(newValues)
+          console.log({ newValues })
           createTraining({ variables: newValues })
         } else {
-          const changes = diff(values, training || emptyTraining())
+          const { id, ...changes } = diffDB(values, training || emptyTraining())
           const newValues = transform(changes, transformArrayToDB)
-          console.log(newValues)
           if (Object.keys(newValues).length > 0) {
-            console.log('saving changes', newValues)
+            console.log('saving changes', changes, newValues)
             updateTraining({
               variables: { where: { id: values.id }, data: newValues }
             })
@@ -37,6 +42,9 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
         }
       }}
     >
+      <p>
+        {values.createdAt} {values.id}
+      </p>
       <TextInput
         name='title'
         label='Title'
@@ -60,6 +68,7 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
         value={values.location}
         onChange={onChange}
       />
+      <Registrations registrations={values.registrations} />
       <TextInput
         name='attendance'
         label='Attendance'
@@ -67,6 +76,7 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
         value={values.attendance}
         onChange={onChange}
       />
+      <Ratings ratings={values.ratings} />
       <Checkbox
         name='published'
         label='Published'

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

@@ -0,0 +1,29 @@
+import { TextInput } from '../../form'
+import { TExercise } from '../types'
+
+interface IExerciseInputs {
+  onChange: GenericEventHandler
+  value: TExercise
+  name: string
+}
+
+const ExerciseInputs = ({ onChange, value, name }: IExerciseInputs) => {
+  return (
+    <>
+      <TextInput
+        name={`${name}.name`}
+        label='Name'
+        value={value.name}
+        onChange={onChange}
+      />
+      <TextInput
+        name={`${name}.description`}
+        label='Description'
+        value={value.description}
+        onChange={onChange}
+      />
+    </>
+  )
+}
+
+export default ExerciseInputs

+ 120 - 0
frontend/src/training/components/ExerciseInstanceInputs.tsx

@@ -0,0 +1,120 @@
+import { useState, useEffect } from 'react'
+import arrayMove from 'array-move'
+import ExerciseInputs from './ExerciseInputs'
+import { SortableList } from '../../sortable'
+import { TextInput } from '../../form'
+import { TExerciseInstance } from '../types'
+
+const ExerciseInstanceInputs = ({
+  value = [],
+  name,
+  onChange
+}: {
+  value?: TExerciseInstance[]
+  name: string
+  onChange: GenericEventHandler
+}) => {
+  const [state, setState] = useState(value.map(item => item.id))
+
+  function updateOrderProperty<T extends { id: U }, U>(
+    values: T[],
+    orderList: U[]
+  ) {
+    const orderedValues = values.map(value => {
+      const order = orderList.findIndex(orderedId => orderedId === value.id)
+      return { ...value, order }
+    })
+    onChange({ target: { type: 'custom', name, value: orderedValues } })
+  }
+
+  function onSortEnd({
+    oldIndex,
+    newIndex
+  }: {
+    oldIndex: number
+    newIndex: number
+  }) {
+    const newOrder = arrayMove(state, oldIndex, newIndex)
+    setState(newOrder)
+    updateOrderProperty(value, newOrder)
+  }
+
+  useEffect(() => {
+    const missingIds = value
+      .filter(item => !state.includes(item.id))
+      .filter(item => !item.id.startsWith('--'))
+      .map(item => item.id)
+    const stateWithoutRemovedItems = state.filter(stateId =>
+      value.find(item => stateId === item.id)
+    )
+    setState([...stateWithoutRemovedItems, ...missingIds])
+  }, [value])
+
+  const items = state
+    .map(stateId => {
+      const itemIndex = value.findIndex(item => item.id === stateId)
+      if (itemIndex < 0) return null
+      const item = value[itemIndex]
+      return (
+        <div key={item.id}>
+          <p>{item.id}</p>
+          <TextInput
+            name={`${name}.${itemIndex}.order`}
+            label='Order'
+            value={item.order}
+            type='number'
+            onChange={onChange}
+          />
+          <TextInput
+            name={`${name}.${itemIndex}.repetitions`}
+            label='Repetitions'
+            value={item.repetitions}
+            type='number'
+            onChange={onChange}
+          />
+          <TextInput
+            name={`${name}.${itemIndex}.variation`}
+            label='Variation'
+            value={item.variation}
+            onChange={onChange}
+          />
+          {item.exercise && (
+            <ExerciseInputs
+              name={`${name}.${itemIndex}.block`}
+              value={item.exercise}
+              onChange={onChange}
+            />
+          )}
+          <button
+            type='button'
+            onClick={ev => {
+              const newOrder = state.filter(orderedId => item.id !== orderedId)
+              setState(newOrder)
+              const newValues = item.id?.startsWith('++')
+                ? [...value.slice(0, itemIndex), ...value.slice(itemIndex + 1)]
+                : [
+                    ...value.slice(0, itemIndex),
+                    { ...item, id: `--${item.id}` },
+                    ...value.slice(itemIndex + 1)
+                  ]
+              updateOrderProperty(newValues, newOrder)
+            }}
+          >
+            Delete block
+          </button>
+        </div>
+      )
+    })
+    .filter(block => block !== null)
+
+  return (
+    <SortableList
+      items={items}
+      onSortEnd={onSortEnd}
+      useDragHandle
+      lockAxis={'y'}
+    />
+  )
+}
+
+export default ExerciseInstanceInputs

+ 22 - 0
frontend/src/training/components/Ratings.tsx

@@ -0,0 +1,22 @@
+import { Rating } from '../../gql'
+
+const Ratings = ({ ratings }: { ratings?: Rating[] }) => {
+  return (
+    <>
+      <h2>Ratings</h2>
+      {ratings ? (
+        <ul>
+          {ratings.map(rating => (
+            <li key={rating.id}>
+              {rating.comment} {rating.value} {rating.user.name}
+            </li>
+          ))}
+        </ul>
+      ) : (
+        <p>No ratings found</p>
+      )}
+    </>
+  )
+}
+
+export default Ratings

+ 25 - 0
frontend/src/training/components/Registrations.tsx

@@ -0,0 +1,25 @@
+import { User } from '../../gql'
+
+const Registrations = ({ registrations }: { registrations?: User[] }) => {
+  return (
+    <>
+      <h2>Registrations</h2>
+      {registrations && registrations.length > 0 ? (
+        <ul>
+          {registrations.map(registration => (
+            <li key={registration.id}>
+              <button type='button' onClick={() => alert('not implemented.')}>
+                delete
+              </button>
+              Registration: {registration.name}
+            </li>
+          ))}
+        </ul>
+      ) : (
+        <p>No registrations found.</p>
+      )}
+    </>
+  )
+}
+
+export default Registrations

+ 13 - 0
frontend/src/training/training.graphql

@@ -101,6 +101,10 @@ query training($id: ID!) {
     blocks {
       ...subBlock
     }
+    registrations {
+      id
+      name
+    }
   }
 }
 
@@ -115,10 +119,15 @@ query trainings {
     }
     trainingDate
     location
+    attendance
     published
     blocks {
       ...subBlockHint
     }
+    registrations {
+      id
+      name
+    }
   }
 }
 
@@ -180,3 +189,7 @@ mutation createFormat($name: String!, $description: String!) {
     id
   }
 }
+
+mutation register($training: ID!) {
+  register(training: $training)
+}

+ 13 - 1
frontend/src/training/types.ts

@@ -1,4 +1,10 @@
-import { Training } from '../gql'
+import {
+  Training,
+  BlockInstance,
+  Block,
+  ExerciseInstance,
+  Exercise
+} from '../gql'
 
 export interface ITraining {
   id: string
@@ -19,6 +25,12 @@ export interface ITraining {
 }
 
 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

+ 95 - 34
frontend/src/training/utils.ts

@@ -1,12 +1,22 @@
 import { parse } from 'date-fns'
-import { IBlock, IExercise, IRating, TTraining } from './types'
+import {
+  IBlock,
+  IExercise,
+  IRating,
+  TTraining,
+  TExerciseInstance,
+  TBlockInstance,
+  TBlock,
+  TExercise
+} from './types'
 import {
   TrainingQuery,
   SubBlockFragment,
   BlockContentFragment,
-  Training
+  Training,
+  ExerciseContentFragment
 } from '../gql'
-import { isArray, transform } from 'lodash'
+import { isArray, transform, isEqual, isObject } from 'lodash'
 
 /**
  * Takes a block of exercises and calculates the duration in seconds.
@@ -113,8 +123,30 @@ function randomID() {
     .substr(0, 10)}`
 }
 
-export function emptyBlock(input?: Partial<BlockContentFragment>) {
-  const emptyBlock: BlockContentFragment = {
+export function emptyExercise(input?: TExercise) {
+  const emptyExercise = {
+    id: randomID(),
+    name: '',
+    description: '',
+    videos: [],
+    pictures: [],
+    targets: [],
+    baseExercise: []
+  }
+  return { ...emptyExercise, ...input }
+}
+
+export function emptyExerciseInstance(input?: TExerciseInstance) {
+  const emptyExerciseInstance = {
+    id: randomID(),
+    order: 0,
+    exercise: emptyExercise()
+  }
+  return { ...emptyExerciseInstance, ...input }
+}
+
+export function emptyBlock(input?: TBlock) {
+  const emptyBlock = {
     id: randomID(),
     title: '',
     format: { id: '', name: '', description: '' },
@@ -126,8 +158,8 @@ export function emptyBlock(input?: Partial<BlockContentFragment>) {
   return { ...emptyBlock, ...input }
 }
 
-export function emptyBlockInstance(input?: Partial<SubBlockFragment>) {
-  const emptyBlockInstance: SubBlockFragment = {
+export function emptyBlockInstance(input?: TBlockInstance) {
+  const emptyBlockInstance = {
     id: randomID(),
     block: emptyBlock(),
     order: 0
@@ -136,7 +168,7 @@ export function emptyBlockInstance(input?: Partial<SubBlockFragment>) {
 }
 
 export function emptyTraining(input?: TTraining) {
-  const emptyTraining: TTraining = {
+  const emptyTraining = {
     id: randomID(),
     title: '',
     type: { id: '', name: '', description: '' },
@@ -152,22 +184,24 @@ export function collectMutationCreateConnect(arr: any[]) {
   const create: any[] = []
   const connect: any[] = []
   const del: any[] = []
+  const update: any[] = []
   arr.forEach(val => {
-    if (typeof val === 'object' && val['connect']) {
-      connect.push(val['connect'])
-    }
-    if (typeof val === 'object' && val['create']) {
-      create.push(val['create'])
-    }
-    if (typeof val === 'object' && val['delete']) {
-      del.push(val['delete'])
-    }
+    if (typeof val === 'object' && val['connect']) connect.push(val['connect'])
+    if (typeof val === 'object' && val['create']) create.push(val['create'])
+    if (typeof val === 'object' && val['delete']) del.push(val['delete'])
+    if (typeof val === 'object' && val['update']) update.push(val['update'])
   })
-  if (create.length > 0 || connect.length > 0) {
-    return { create, connect, delete: del }
-  } else {
-    return arr
-  }
+  const returnObject: {
+    connect?: any
+    delete?: any
+    create?: any
+    update?: any
+  } = {}
+  if (connect.length > 0) returnObject.connect = connect
+  if (del.length > 0) returnObject.delete = del
+  if (update.length > 0) returnObject.update = update
+  if (create.length > 0) returnObject.create = create
+  return returnObject
 }
 
 export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
@@ -182,26 +216,53 @@ export function transformArrayToDB(acc: any, val: any, key: any, object: any) {
     acc[key] = collectMutationCreateConnect(transform(val, transformArrayToDB))
   } else if (typeof val === 'object' && !!val) {
     // we found an object!
-    if (
-      !!val['id'] &&
-      typeof val['id'] === 'string' &&
-      val['id'].startsWith('++')
-    ) {
+    if (!!val['id'] && val['id'].startsWith('++')) {
       // values with placeholder IDs are preserved
       acc[key] = { create: transform(val, transformArrayToDB) }
-    } else if (
-      !!val['id'] &&
-      typeof val['id'] === 'string' &&
-      val['id'].startsWith('--')
-    ) {
-      // values with placeholder IDs are preserved
+    } else if (!!val['id'] && val['id'].startsWith('--')) {
+      // IDs starting with -- are for deletion
       acc[key] = { delete: { id: val['id'].substr(2) } }
     } else {
       // values with real IDs are just connected
-      acc[key] = { connect: { id: val['id'] } }
+      const { id, ...data } = val
+      if (id?.startsWith('@@')) {
+        if (typeof key === 'string') {
+          acc[key] = { update: transform(data, transformArrayToDB) }
+        } else {
+          acc[key] = {
+            update: {
+              where: { id: id.substr(2) },
+              data: transform(data, transformArrayToDB)
+            }
+          }
+        }
+        console.log('update candidate', key, id, data, acc[key].update.data)
+      } else {
+        acc[key] = { connect: { id } }
+      }
     }
   } else {
     // copy the value
     acc[key] = val
   }
 }
+
+export function diffDB(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])) {
+        const newValue =
+          isObject(value) && isObject(oldObject[key])
+            ? diffDB(value, oldObject[key])
+            : value
+        if (newValue !== undefined && newValue !== null) {
+          result[key] = newValue
+        }
+      }
+    }
+  )
+  return Object.keys(transformResult).length > 0 ? transformResult : undefined
+}

+ 15 - 0
frontend/src/user/hooks.tsx

@@ -0,0 +1,15 @@
+import { createContext, FunctionComponent } from 'react'
+import { CurrentUserQuery, useCurrentUserQuery } from '../gql'
+
+export const UserContext = createContext<
+  CurrentUserQuery['currentUser'] | undefined
+>(undefined)
+
+export const UserProvider: FunctionComponent = ({ children }) => {
+  const user = useCurrentUserQuery()
+  return (
+    <UserContext.Provider value={user.data?.currentUser}>
+      {children}
+    </UserContext.Provider>
+  )
+}