Tomi Cvetic 5 роки тому
батько
коміт
c1ba44ab67

+ 4 - 1
.vscode/settings.json

@@ -10,5 +10,8 @@
     "editor.fontLigatures": true,
     "files.trimTrailingWhitespace": true,
     "workbench.colorTheme": "Cobalt2",
-    "editor.tabSize": 2
+    "editor.tabSize": 2,
+    "prettier.jsxSingleQuote": true,
+    "prettier.semi": false,
+    "prettier.singleQuote": true
 }

Різницю між файлами не показано, бо вона завелика
+ 498 - 408
backend/database/generated/prisma-client/index.d.ts


+ 2 - 2
backend/database/generated/prisma-client/index.js

@@ -33,11 +33,11 @@ var models = [
     embedded: false
   },
   {
-    name: "Exercise",
+    name: "ExerciseInstance",
     embedded: false
   },
   {
-    name: "BaseExercise",
+    name: "Exercise",
     embedded: false
   },
   {

Різницю між файлами не показано, бо вона завелика
+ 341 - 376
backend/database/generated/prisma-client/prisma-schema.js


Різницю між файлами не показано, бо вона завелика
+ 390 - 446
backend/database/generated/prisma.graphql


+ 28 - 24
backend/datamodel.prisma

@@ -6,10 +6,10 @@ type User {
     resetToken: String
     resetTokenExpiry: Float
     createdAt: DateTime! @createdAt
-    comments: [Comment]!
-    ratings: [Rating]!
-    permissions: [Permission]!  @scalarList(strategy: RELATION)
-    interests: [String]!  @scalarList(strategy: RELATION)
+    comments: [Comment!]!
+    ratings: [Rating!]!
+    permissions: [Permission!]!  @scalarList(strategy: RELATION)
+    interests: [String!]!  @scalarList(strategy: RELATION)
 }
 
 enum Permission {
@@ -21,14 +21,14 @@ type Training {
     id: ID! @id
     title: String!
     type: TrainingType!
-    content: [Block]!
     createdAt: DateTime! @createdAt
-    trainingDate: DateTime!
-    location: String!
-    registration: [User]!
-    attendance: Int!
-    ratings: [Rating]!
+    trainingDate: DateTime
+    location: String
+    registrations: [User!]!
+    attendance: Int
+    ratings: [Rating!]!
     published: Boolean!
+    blocks: [Block!]!
 }
 
 type TrainingType {
@@ -41,12 +41,16 @@ type Block {
     id: ID! @id
     sequence: Int!
     title: String!
-    duration: Int!
-    variation: String
-    format: Format
-    tracks: [Track]!
-    exercises: [Exercise]!
-    description: String!
+    description: String
+    videos: [String!]! @scalarList(strategy: RELATION)
+    pictures: [String!]! @scalarList(strategy: RELATION)
+    duration: Int
+    rounds: Int
+    format: Format!
+    rest: Int
+    tracks: [Track!]!
+    blocks: [Block!]!
+    exercises: [ExerciseInstance!]!
 }
 
 type Format {
@@ -63,19 +67,19 @@ type Track {
     link: String!
 }
 
-type Exercise {
+type ExerciseInstance {
     id: ID! @id
-    name: String!
-    description: String!
-    video: String!
-    targets: [String]! @scalarList(strategy: RELATION)
-    baseExercise: BaseExercise!
+    exercise: Exercise!
+    repetitions: Int
 }
 
-type BaseExercise {
+type Exercise {
     id: ID! @id
     name: String!
-    variations: [Exercise]!
+    description: String!
+    video: String!
+    targets: [String!]! @scalarList(strategy: RELATION)
+    baseExercise: [String!]! @scalarList(strategy: RELATION)
 }
 
 type Rating {

+ 17 - 20
backend/index.js

@@ -5,22 +5,19 @@
  * Configure CORS for use with localhost.
  */
 
-require("dotenv").config();
-const { GraphQLServer } = require("graphql-yoga");
-const cookieParser = require("cookie-parser");
-const bodyParser = require("body-parser");
-const { merge } = require("lodash");
-const { db, populateUser } = require("./src/db");
-//const user = require("./src/user");
+require('dotenv').config()
+const { GraphQLServer } = require('graphql-yoga')
+const cookieParser = require('cookie-parser')
+const bodyParser = require('body-parser')
+const { merge } = require('lodash')
+const { db, populateUser } = require('./src/db')
+const user = require('./src/user')
 
-const prismaResolvers = require("./src/resolvers");
+const prismaResolvers = require('./src/resolvers')
 
-const resolvers = merge(
-  prismaResolvers.resolvers
-  //user.resolvers
-);
+const resolvers = merge(prismaResolvers.resolvers, user.resolvers)
 
-const typeDefs = ["./schema.graphql"];
+const typeDefs = ['./schema.graphql']
 
 const server = new GraphQLServer({
   typeDefs,
@@ -30,14 +27,14 @@ const server = new GraphQLServer({
     db,
     debug: true
   })
-});
+})
 
-server.express.use(cookieParser());
-server.express.use(bodyParser.json());
+server.express.use(cookieParser())
+server.express.use(bodyParser.json())
 // server.express.use(quickMiddleware)
-// server.express.use(user.authenticate);
-server.express.use(populateUser);
-// server.express.use("/static", express.static("static"));
+server.express.use(user.authenticate)
+server.express.use(populateUser)
+server.express.use('/static', express.static('static'))
 
 server.start(
   {
@@ -47,4 +44,4 @@ server.start(
     }
   },
   server => console.log(`Server is running on http://localhost:${server.port}`)
-);
+)

+ 16 - 0
backend/src/user/authenticate.ts

@@ -0,0 +1,16 @@
+import jwt from 'jsonwebtoken'
+import { Response, NextFunction } from 'express'
+
+if (!process.env.APP_SECRET) {
+  throw Error('App secret (APP_SECRET) must be set in the .env file')
+}
+
+export const authenticate = (req: any, res: Response, next: NextFunction) => {
+  const { token } = req.cookies
+  if (token) {
+    const verifiedToken: any = jwt.verify(token, process.env.APP_SECRET || '')
+    const { userId } = verifiedToken
+    req.userId = userId
+  }
+  next()
+}

+ 8 - 0
backend/src/user/index.ts

@@ -0,0 +1,8 @@
+import { authenticate } from './authenticate'
+import { resolvers, checkPermission } from './resolvers'
+
+export default {
+  authenticate,
+  resolvers,
+  checkPermission
+}

+ 192 - 0
backend/src/user/resolvers.ts

@@ -0,0 +1,192 @@
+import { IResolvers } from 'apollo-server'
+
+import bcrypt from 'bcryptjs'
+import jwt from 'jsonwebtoken'
+import { promisify } from 'util'
+import randombytes from 'randombytes'
+
+if (!process.env.APP_SECRET) {
+  throw Error('App secret (APP_SECRET) must be set in the .env file')
+}
+
+export const resolvers: IResolvers = {
+  Query: {
+    /* Public functions without login/permission check */
+
+    /* Functions that require a login */
+    currentUser: (parent, args, context, info) => {
+      checkPermission(context)
+      return context.db.query.user(
+        {
+          where: { id: context.req.userId }
+        },
+        info
+      )
+    },
+
+    /* Functions that require higher permissions */
+    users: async (parent, args, context, info) => {
+      checkPermission(context, 'ADMIN')
+      return context.db.query.users(null, info)
+    }
+  },
+
+  Mutation: {
+    /* Public functions without login/permission check */
+    userSignup: async (parent, { email, password, ...args }, ctx, info) => {
+      const lowercaseEmail = email.toLowerCase()
+      const encryptedPassword = await bcrypt.hash(password, 10)
+      const user = await ctx.db.mutation.createUser(
+        {
+          data: {
+            ...args,
+            email: lowercaseEmail,
+            password: encryptedPassword
+          }
+        },
+        info
+      )
+      const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET || '')
+      ctx.res.cookie('token', token, {
+        httpOnly: true,
+        maxAge: 24 * 60 * 60 * 1000
+      })
+      return user
+    },
+    userLogin: async (parent, { email, password }, context, info) => {
+      const user = await context.db.query.user({ where: { email } })
+      if (!user) throw new Error('Email address not found')
+      const valid = await bcrypt.compare(password, user.password)
+      if (!valid) throw new Error('Invalid password')
+      const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET || '')
+      context.res.cookie(
+        'token',
+        token,
+        {
+          httpOnly: true,
+          maxAge: 7 * 24 * 3600 * 1000
+        },
+        info
+      )
+      return user
+    },
+    userLogout: async (parent, args, context, info) => {
+      context.res.clearCookie('token')
+      return 'Logged out.'
+    },
+    requestReset: async (parent, { email }, context, info) => {
+      const user = await context.db.query.user({ where: { email } })
+      if (!user) throw Error('Email address not found')
+      const randombytesPromisified = promisify(randombytes)
+      const resetToken = (await randombytesPromisified(20)).toString('hex')
+      const resetTokenExpiry = Date.now() + 3600000 // 1 hour from now
+      await context.db.mutation.updateUser({
+        where: { email },
+        data: { resetToken, resetTokenExpiry }
+      })
+      /* await transport.sendMail({
+        from: 'wes@wesbos.com',
+        to: user.email,
+        subject: 'Your Password Reset Token',
+        html: emailTemplate(`Your Password Reset Token is here!
+        \n\n
+        <a href="${process.env
+            .FRONTEND_URL}/reset?resetToken=${resetToken}">Click Here to Reset</a>`)
+      }) */
+      return 'Success.'
+    },
+    resetPassword: async (parent, { token, password }, context, info) => {
+      const [user] = await context.db.query.users({
+        where: {
+          resetToken: token,
+          resetTokenExpiry_gte: Date.now() - 3600000
+        }
+      })
+      if (!user) {
+        throw Error('Token invalid or expired.')
+      }
+      const encryptedPassword = await bcrypt.hash(password, 10)
+      const updatedUser = await context.db.mutation.updateUser({
+        where: { email: user.email },
+        data: {
+          password: encryptedPassword,
+          resetToken: null,
+          resetTokenExpiry: null
+        }
+      })
+      const cookieToken = jwt.sign(
+        { userId: updatedUser.id },
+        process.env.APP_SECRET || ''
+      )
+      context.res.cookie('token', cookieToken, {
+        httpOnly: true,
+        maxAge: 1000 * 60 * 60 * 24 * 365
+      })
+      return updatedUser
+    },
+
+    /* Functions that require higher permissions */
+    createUser: async (parent, { email, password, ...args }, context, info) => {
+      checkPermission(context, 'ADMIN')
+      const lowercaseEmail = email.toLowerCase()
+      const encryptedPassword = await bcrypt.hash(password, 10)
+      return context.db.mutation.createUser(
+        {
+          data: {
+            ...args,
+            email: lowercaseEmail,
+            password: encryptedPassword
+          }
+        },
+        info
+      )
+    },
+    updateUser: async (parent, { email, password, ...args }, context, info) => {
+      checkPermission(context, 'ADMIN')
+      const updatedData = { ...args }
+      if (!!email) updatedData.email = email.toLowerCase()
+      if (!!password) updatedData.password = await bcrypt.hash(password, 10)
+      return context.db.mutation.updateUser(
+        {
+          data: {
+            ...updatedData
+          },
+          where: { email }
+        },
+        info
+      )
+    },
+    deleteUser: (parent, { email }, context, info) => {
+      checkPermission(context, 'ADMIN')
+      return context.db.mutation.deleteUser({ where: { email } })
+    }
+  }
+}
+
+/**
+ * Checks whether the current user has the required permissions. Raises an error if he doesn't.
+ * @param context - Resolver context. Needs to have 'userId' and 'user' populated in req
+ * @param permission - String or list of strings with required permissions
+ */
+export function checkPermission(context: any, permission?: string | string[]) {
+  if (!context.req.userId) throw new Error('Login required.')
+  if (typeof permission === 'string') {
+    if (!context.req.user.permissions.includes(permission))
+      throw Error(
+        `No permission. This operation requires ${permission} privilege.`
+      )
+  } else if (Array.isArray(permission)) {
+    if (
+      !context.req.user.permissions.reduce(
+        (state: boolean, userPermission: string) =>
+          state || permission.includes(userPermission),
+        false
+      )
+    )
+      throw Error(
+        `No permission. This operation requires ${permission.join(
+          ' or '
+        )} privilege.`
+      )
+  }
+}

+ 890 - 0
frontend/corona1.ts

@@ -0,0 +1,890 @@
+import { ITraining } from './src/training/types'
+
+const data: { trainings: ITraining[]; polls: any } = {
+  trainings: [
+    {
+      id: 'training0',
+      title: 'Corona 1',
+      type: {
+        id: 'type0',
+        name: 'HIIT',
+        description: 'High Intensity Interval Training'
+      },
+      createdAt: '2020-03-25T21:13:43.284Z',
+      trainingDate: '2020-03-31T09:45:00.000Z',
+      location: 'At home',
+      registrations: [],
+      attendance: 0,
+      ratings: [],
+      published: true,
+      blocks: [
+        {
+          id: 'block0',
+          sequence: 0,
+          title: 'Drop Sets',
+          repetitions: 1,
+          rest: 25,
+          format: {
+            id: 'format0',
+            name: 'Sequence',
+            description: 'Sequence of exercises'
+          },
+          blocks: [
+            {
+              id: 'block1',
+              sequence: 0,
+              title: 'Lateral move with jumps',
+              repetitions: 1,
+              format: {
+                id: 'format0',
+                name: 'Drop Sets',
+                description:
+                  'During the exercise, the moves get easier. Keep the heart rate up by increasing the intensity!'
+              },
+              rest: 25,
+              blocks: [
+                {
+                  id: 'block2',
+                  duration: 40,
+                  video: '/media/block1-1.mp4',
+                  description:
+                    '4 lateral high knee steps, drop squat, a jump and a butt kick',
+                  exercises: [
+                    {
+                      id: 'exercise0',
+                      name: 'Lateral high knee run',
+                      repetitions: 4,
+                      description:
+                        'Jog pulling your knees up high while moving sideways',
+                      videos: ['https://www.youtube.com/watch?v=s5GanRixp6I'],
+                      pictures: [
+                        'https://media1.popsugar-assets.com/files/thumbor/xfgCQbEWOZpPDA_HTMSfgcOnYYE/fit-in/1024x1024/filters:format_auto-!!-:strip_icc-!!-/2015/06/26/981/n/1922729/a7719ba19ea7a1ae_lateral-run-and-hold/i/Tabata-One-Lateral-High-Knee-Run-Hold.jpg'
+                      ],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jog on the spot'
+                      }
+                    },
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description:
+                        'Jump up. Feet are at least hip-wide, land in a squat. Use your arms to add momentum.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    },
+                    {
+                      id: 'exercise3',
+                      name: 'Butt kick',
+                      repetitions: 1,
+                      description:
+                        'Jump up. While jumping, try to kick your butt with your heels.',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block3',
+                  duration: 30,
+                  video: '/media/block1-2.mp4',
+                  description: 'Drop squat, a jump and a butt kick.',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description:
+                        'Jump up. Feet are at least hip-wide, land in a squat. Use your arms to add momentum.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    },
+                    {
+                      id: 'exercise3',
+                      name: 'Butt kick',
+                      repetitions: 1,
+                      description:
+                        'Jump up. While jumping, try to kick your butt with your heels.',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block4',
+                  duration: 20,
+                  video: '/media/block1-3.mp4',
+                  description: 'Drop squat and a jump.',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description:
+                        'Jump up. Feet are at least hip-wide, land in a squat. Use your arms to add momentum.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                }
+              ]
+            },
+            {
+              id: 'block5',
+              sequence: 0,
+              title: 'Pushup with frog-jump and block jump',
+              repetitions: 1,
+              format: {
+                id: 'format0',
+                name: 'Drop Sets',
+                description:
+                  'During the exercise, the moves get easier. Keep the heart rate up by increasing the intensity!'
+              },
+              rest: 25,
+              blocks: [
+                {
+                  id: 'block6',
+                  duration: 40,
+                  video: '/media/block2-1.mp4',
+                  description: 'Pushup, frog jump and a jump.',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise4',
+                      name: 'Pushup',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Chest', 'Triceps'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Pushup'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Frog jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Core', 'Quads'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Mountain climber'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block7',
+                  duration: 30,
+                  video: '/media/block2-2.mp4',
+                  description: 'Pushup and a jump',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise4',
+                      name: 'Pushup',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Chest', 'Triceps'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Pushup'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block8',
+                  duration: 20,
+                  video: '/media/burpee.mp4',
+                  description: 'Just a burpee.',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Squat',
+                      repetitions: 1,
+                      description:
+                        'Sit down backwards, hip about knee-high. Keep your knees behind the toes.',
+                      videos: [],
+                      pictures: [],
+                      targets: ['Thighs', 'Glutes'],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Squat'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+
+        {
+          id: 'block9',
+          sequence: 0,
+          title: 'Power Sets with Kicker',
+          repetitions: 2,
+          format: {
+            id: 'format0',
+            name: 'Sequence',
+            description: 'Sequence of exercises'
+          },
+          rest: 25,
+          blocks: [
+            {
+              id: 'block10',
+              sequence: 0,
+              title: 'Block A',
+              repetitions: 1,
+              format: {
+                id: 'format0',
+                name: 'Power Set',
+                description:
+                  'Sequence of exercises targeting different regions without rest in between.'
+              },
+              rest: 25,
+              blocks: [
+                {
+                  id: 'block11',
+                  duration: 25,
+                  video: '/media/block3-1.mp4',
+                  description: '',
+                  exercises: [
+                    {
+                      id: 'exercise6',
+                      name: 'Donkey kick',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Mountain climber'
+                      }
+                    },
+                    {
+                      id: 'exercise1',
+                      name: 'Plank walk',
+                      repetitions: 2,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Plank'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block12',
+                  duration: 25,
+                  video: '/media/block3-2.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Long jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    },
+                    {
+                      id: 'exercise2',
+                      name: '180°',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block13',
+                  duration: 10,
+                  video: '/media/burpee.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Burpee',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Burpee'
+                      }
+                    }
+                  ]
+                }
+              ]
+            },
+
+            {
+              id: 'block14',
+              sequence: 0,
+              title: 'Block B',
+              repetitions: 1,
+              format: {
+                id: 'format0',
+                name: 'Power Set',
+                description:
+                  'Sequence of exercises targeting different regions without rest in between.'
+              },
+              rest: 25,
+              blocks: [
+                {
+                  id: 'block15',
+                  duration: 25,
+                  video: '/media/block4-1.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise6',
+                      name: 'Side climber',
+                      repetitions: 2,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Mountain climber'
+                      }
+                    },
+                    {
+                      id: 'exercise1',
+                      name: 'Pushup',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Plank'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block16',
+                  duration: 25,
+                  video: '/media/block4-2.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Power lunge',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Lunge'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block17',
+                  duration: 20,
+                  video: '/media/burpee.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Burpee',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Burpee'
+                      }
+                    }
+                  ]
+                }
+              ]
+            },
+
+            {
+              id: 'block18',
+              sequence: 0,
+              title: 'Block C',
+              repetitions: 1,
+              format: {
+                id: 'format0',
+                name: 'Power Set',
+                description:
+                  'Sequence of exercises targeting different regions without rest in between.'
+              },
+              rest: 25,
+              blocks: [
+                {
+                  id: 'block19',
+                  duration: 25,
+                  video: '/media/block5-1.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise6',
+                      name: 'Inch worm',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Plank'
+                      }
+                    },
+                    {
+                      id: 'exercise1',
+                      name: 'Block jump',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jump'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block20',
+                  duration: 25,
+                  video: '/media/fastfeet.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Fast feet',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Jog on the spot'
+                      }
+                    }
+                  ]
+                },
+                {
+                  id: 'block1',
+                  duration: 30,
+                  video: '/media/burpee.mp4',
+                  exercises: [
+                    {
+                      id: 'exercise1',
+                      name: 'Burpee',
+                      repetitions: 1,
+                      description: '',
+                      videos: [],
+                      pictures: [],
+                      targets: [],
+                      baseExercise: {
+                        id: 'baseExercise1',
+                        name: 'Burpee'
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        },
+
+        {
+          id: 'block25',
+          sequence: 0,
+          title: 'Leg AMRAP',
+          repetitions: 3,
+          format: {
+            id: 'format0',
+            name: 'AMRAP',
+            description: 'As many rounds as possible'
+          },
+          rest: 25,
+          blocks: [
+            {
+              id: 'block0',
+              duration: 90,
+              video: '/media/block6-1.mp4',
+              description:
+                'Try to finish as many rounds as possible in 90 seconds! In the second round, finish the same number of rounds, but add the challenge (bear crawl) after each round. In the third round, finish the same number of rounds again, but add the challenge after every exercise. The target is 3 rounds.',
+              exercises: [
+                {
+                  id: 'exercise6',
+                  name: 'Leap/Jump',
+                  repetitions: 5,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Mountain climber'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Low 180',
+                  repetitions: 10,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Ice skater',
+                  repetitions: 15,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Bear crawl',
+                  repetitions: 1,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                }
+              ]
+            }
+          ]
+        },
+
+        {
+          id: 'block3',
+          sequence: 0,
+          title: '1 Minute Challenges',
+          repetitions: 1,
+          format: {
+            id: 'format0',
+            name: 'Sequence',
+            description: 'Sequence of exercises'
+          },
+          rest: 25,
+          blocks: [
+            {
+              id: 'block0',
+              duration: 60,
+              rest: 25,
+              video: '/media/block7-1.mp4',
+              description:
+                'Go fast on the dynamic exercise, and hold still on the static part, moving as little as possible!',
+              exercises: [
+                {
+                  id: 'exercise6',
+                  name: 'Single arm burpee',
+                  repetitions: 8,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Mountain climber'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Single arm plank + Foot lift L/R',
+                  repetitions: 8,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                }
+              ]
+            },
+
+            {
+              id: 'block0',
+              duration: 60,
+              rest: 25,
+              video: '/media/block8-1.mp4',
+              description:
+                'Go fast on the dynamic exercise, and hold still on the static part, moving as little as possible!',
+              exercises: [
+                {
+                  id: 'exercise6',
+                  name: 'Rotation drop squat + Jack',
+                  repetitions: 8,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Mountain climber'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Hold squat + Step out L/R',
+                  repetitions: 8,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                }
+              ]
+            }
+          ]
+        },
+
+        {
+          id: 'block2',
+          sequence: 0,
+          title: 'Core AMRAP',
+          repetitions: 1,
+          format: {
+            id: 'format0',
+            name: 'AMRAP',
+            description: 'As many rounds as possible'
+          },
+          rest: 25,
+          blocks: [
+            {
+              id: 'block0',
+              duration: 90,
+              video: '/media/block9-1.mp4',
+              description:
+                'Try to finish as many rounds as possible in 90 seconds! Keep the transitions short. The target is 3 rounds.',
+              exercises: [
+                {
+                  id: 'exercise6',
+                  name: 'C-Crunch',
+                  repetitions: 10,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Mountain climber'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Bicycle crunch',
+                  repetitions: 10,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                },
+                {
+                  id: 'exercise1',
+                  name: 'Mountain climber',
+                  repetitions: 10,
+                  description: '',
+                  videos: [],
+                  pictures: [],
+                  targets: [],
+                  baseExercise: {
+                    id: 'baseExercise1',
+                    name: 'Plank'
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ],
+  polls: []
+}
+
+export default data

+ 7 - 0
frontend/pages/training.tsx

@@ -0,0 +1,7 @@
+import NewTraining from '../src/training/components/NewTraining'
+
+const TrainingPage = () => {
+  return <NewTraining />
+}
+
+export default TrainingPage

+ 207 - 253
frontend/src/gql/index.tsx

@@ -14,101 +14,21 @@ export type Scalars = {
   DateTime: any,
 };
 
-export type BaseExercise = Node & {
-   __typename?: 'BaseExercise',
-  id: Scalars['ID'],
-  name: Scalars['String'],
-  variations?: Maybe<Array<Exercise>>,
-};
-
-
-export type BaseExerciseVariationsArgs = {
-  where?: Maybe<ExerciseWhereInput>,
-  orderBy?: Maybe<ExerciseOrderByInput>,
-  skip?: Maybe<Scalars['Int']>,
-  after?: Maybe<Scalars['String']>,
-  before?: Maybe<Scalars['String']>,
-  first?: Maybe<Scalars['Int']>,
-  last?: Maybe<Scalars['Int']>
-};
-
-export type BaseExerciseWhereInput = {
-  /** Logical AND on all given filters. */
-  AND?: Maybe<Array<BaseExerciseWhereInput>>,
-  /** Logical OR on all given filters. */
-  OR?: Maybe<Array<BaseExerciseWhereInput>>,
-  /** Logical NOT on all given filters combined by AND. */
-  NOT?: Maybe<Array<BaseExerciseWhereInput>>,
-  id?: Maybe<Scalars['ID']>,
-  /** All values that are not equal to given value. */
-  id_not?: Maybe<Scalars['ID']>,
-  /** All values that are contained in given list. */
-  id_in?: Maybe<Array<Scalars['ID']>>,
-  /** All values that are not contained in given list. */
-  id_not_in?: Maybe<Array<Scalars['ID']>>,
-  /** All values less than the given value. */
-  id_lt?: Maybe<Scalars['ID']>,
-  /** All values less than or equal the given value. */
-  id_lte?: Maybe<Scalars['ID']>,
-  /** All values greater than the given value. */
-  id_gt?: Maybe<Scalars['ID']>,
-  /** All values greater than or equal the given value. */
-  id_gte?: Maybe<Scalars['ID']>,
-  /** All values containing the given string. */
-  id_contains?: Maybe<Scalars['ID']>,
-  /** All values not containing the given string. */
-  id_not_contains?: Maybe<Scalars['ID']>,
-  /** All values starting with the given string. */
-  id_starts_with?: Maybe<Scalars['ID']>,
-  /** All values not starting with the given string. */
-  id_not_starts_with?: Maybe<Scalars['ID']>,
-  /** All values ending with the given string. */
-  id_ends_with?: Maybe<Scalars['ID']>,
-  /** All values not ending with the given string. */
-  id_not_ends_with?: Maybe<Scalars['ID']>,
-  name?: Maybe<Scalars['String']>,
-  /** All values that are not equal to given value. */
-  name_not?: Maybe<Scalars['String']>,
-  /** All values that are contained in given list. */
-  name_in?: Maybe<Array<Scalars['String']>>,
-  /** All values that are not contained in given list. */
-  name_not_in?: Maybe<Array<Scalars['String']>>,
-  /** All values less than the given value. */
-  name_lt?: Maybe<Scalars['String']>,
-  /** All values less than or equal the given value. */
-  name_lte?: Maybe<Scalars['String']>,
-  /** All values greater than the given value. */
-  name_gt?: Maybe<Scalars['String']>,
-  /** All values greater than or equal the given value. */
-  name_gte?: Maybe<Scalars['String']>,
-  /** All values containing the given string. */
-  name_contains?: Maybe<Scalars['String']>,
-  /** All values not containing the given string. */
-  name_not_contains?: Maybe<Scalars['String']>,
-  /** All values starting with the given string. */
-  name_starts_with?: Maybe<Scalars['String']>,
-  /** All values not starting with the given string. */
-  name_not_starts_with?: Maybe<Scalars['String']>,
-  /** All values ending with the given string. */
-  name_ends_with?: Maybe<Scalars['String']>,
-  /** All values not ending with the given string. */
-  name_not_ends_with?: Maybe<Scalars['String']>,
-  variations_every?: Maybe<ExerciseWhereInput>,
-  variations_some?: Maybe<ExerciseWhereInput>,
-  variations_none?: Maybe<ExerciseWhereInput>,
-};
-
 export type Block = Node & {
    __typename?: 'Block',
   id: Scalars['ID'],
   sequence: Scalars['Int'],
   title: Scalars['String'],
-  duration: Scalars['Int'],
-  variation?: Maybe<Scalars['String']>,
-  format?: Maybe<Format>,
+  description?: Maybe<Scalars['String']>,
+  videos: Array<Scalars['String']>,
+  pictures: Array<Scalars['String']>,
+  duration?: Maybe<Scalars['Int']>,
+  rounds?: Maybe<Scalars['Int']>,
+  format: Format,
+  rest?: Maybe<Scalars['Int']>,
   tracks?: Maybe<Array<Track>>,
-  exercises?: Maybe<Array<Exercise>>,
-  description: Scalars['String'],
+  blocks?: Maybe<Array<Block>>,
+  exercises?: Maybe<Array<ExerciseInstance>>,
 };
 
 
@@ -123,9 +43,20 @@ export type BlockTracksArgs = {
 };
 
 
+export type BlockBlocksArgs = {
+  where?: Maybe<BlockWhereInput>,
+  orderBy?: Maybe<BlockOrderByInput>,
+  skip?: Maybe<Scalars['Int']>,
+  after?: Maybe<Scalars['String']>,
+  before?: Maybe<Scalars['String']>,
+  first?: Maybe<Scalars['Int']>,
+  last?: Maybe<Scalars['Int']>
+};
+
+
 export type BlockExercisesArgs = {
-  where?: Maybe<ExerciseWhereInput>,
-  orderBy?: Maybe<ExerciseOrderByInput>,
+  where?: Maybe<ExerciseInstanceWhereInput>,
+  orderBy?: Maybe<ExerciseInstanceOrderByInput>,
   skip?: Maybe<Scalars['Int']>,
   after?: Maybe<Scalars['String']>,
   before?: Maybe<Scalars['String']>,
@@ -140,12 +71,14 @@ export enum BlockOrderByInput {
   SequenceDesc = 'sequence_DESC',
   TitleAsc = 'title_ASC',
   TitleDesc = 'title_DESC',
+  DescriptionAsc = 'description_ASC',
+  DescriptionDesc = 'description_DESC',
   DurationAsc = 'duration_ASC',
   DurationDesc = 'duration_DESC',
-  VariationAsc = 'variation_ASC',
-  VariationDesc = 'variation_DESC',
-  DescriptionAsc = 'description_ASC',
-  DescriptionDesc = 'description_DESC'
+  RoundsAsc = 'rounds_ASC',
+  RoundsDesc = 'rounds_DESC',
+  RestAsc = 'rest_ASC',
+  RestDesc = 'rest_DESC'
 }
 
 export type BlockWhereInput = {
@@ -224,6 +157,33 @@ export type BlockWhereInput = {
   title_ends_with?: Maybe<Scalars['String']>,
   /** All values not ending with the given string. */
   title_not_ends_with?: Maybe<Scalars['String']>,
+  description?: Maybe<Scalars['String']>,
+  /** All values that are not equal to given value. */
+  description_not?: Maybe<Scalars['String']>,
+  /** All values that are contained in given list. */
+  description_in?: Maybe<Array<Scalars['String']>>,
+  /** All values that are not contained in given list. */
+  description_not_in?: Maybe<Array<Scalars['String']>>,
+  /** All values less than the given value. */
+  description_lt?: Maybe<Scalars['String']>,
+  /** All values less than or equal the given value. */
+  description_lte?: Maybe<Scalars['String']>,
+  /** All values greater than the given value. */
+  description_gt?: Maybe<Scalars['String']>,
+  /** All values greater than or equal the given value. */
+  description_gte?: Maybe<Scalars['String']>,
+  /** All values containing the given string. */
+  description_contains?: Maybe<Scalars['String']>,
+  /** All values not containing the given string. */
+  description_not_contains?: Maybe<Scalars['String']>,
+  /** All values starting with the given string. */
+  description_starts_with?: Maybe<Scalars['String']>,
+  /** All values not starting with the given string. */
+  description_not_starts_with?: Maybe<Scalars['String']>,
+  /** All values ending with the given string. */
+  description_ends_with?: Maybe<Scalars['String']>,
+  /** All values not ending with the given string. */
+  description_not_ends_with?: Maybe<Scalars['String']>,
   duration?: Maybe<Scalars['Int']>,
   /** All values that are not equal to given value. */
   duration_not?: Maybe<Scalars['Int']>,
@@ -239,67 +199,46 @@ export type BlockWhereInput = {
   duration_gt?: Maybe<Scalars['Int']>,
   /** All values greater than or equal the given value. */
   duration_gte?: Maybe<Scalars['Int']>,
-  variation?: Maybe<Scalars['String']>,
+  rounds?: Maybe<Scalars['Int']>,
   /** All values that are not equal to given value. */
-  variation_not?: Maybe<Scalars['String']>,
+  rounds_not?: Maybe<Scalars['Int']>,
   /** All values that are contained in given list. */
-  variation_in?: Maybe<Array<Scalars['String']>>,
+  rounds_in?: Maybe<Array<Scalars['Int']>>,
   /** All values that are not contained in given list. */
-  variation_not_in?: Maybe<Array<Scalars['String']>>,
+  rounds_not_in?: Maybe<Array<Scalars['Int']>>,
   /** All values less than the given value. */
-  variation_lt?: Maybe<Scalars['String']>,
+  rounds_lt?: Maybe<Scalars['Int']>,
   /** All values less than or equal the given value. */
-  variation_lte?: Maybe<Scalars['String']>,
+  rounds_lte?: Maybe<Scalars['Int']>,
   /** All values greater than the given value. */
-  variation_gt?: Maybe<Scalars['String']>,
+  rounds_gt?: Maybe<Scalars['Int']>,
   /** All values greater than or equal the given value. */
-  variation_gte?: Maybe<Scalars['String']>,
-  /** All values containing the given string. */
-  variation_contains?: Maybe<Scalars['String']>,
-  /** All values not containing the given string. */
-  variation_not_contains?: Maybe<Scalars['String']>,
-  /** All values starting with the given string. */
-  variation_starts_with?: Maybe<Scalars['String']>,
-  /** All values not starting with the given string. */
-  variation_not_starts_with?: Maybe<Scalars['String']>,
-  /** All values ending with the given string. */
-  variation_ends_with?: Maybe<Scalars['String']>,
-  /** All values not ending with the given string. */
-  variation_not_ends_with?: Maybe<Scalars['String']>,
-  description?: Maybe<Scalars['String']>,
+  rounds_gte?: Maybe<Scalars['Int']>,
+  rest?: Maybe<Scalars['Int']>,
   /** All values that are not equal to given value. */
-  description_not?: Maybe<Scalars['String']>,
+  rest_not?: Maybe<Scalars['Int']>,
   /** All values that are contained in given list. */
-  description_in?: Maybe<Array<Scalars['String']>>,
+  rest_in?: Maybe<Array<Scalars['Int']>>,
   /** All values that are not contained in given list. */
-  description_not_in?: Maybe<Array<Scalars['String']>>,
+  rest_not_in?: Maybe<Array<Scalars['Int']>>,
   /** All values less than the given value. */
-  description_lt?: Maybe<Scalars['String']>,
+  rest_lt?: Maybe<Scalars['Int']>,
   /** All values less than or equal the given value. */
-  description_lte?: Maybe<Scalars['String']>,
+  rest_lte?: Maybe<Scalars['Int']>,
   /** All values greater than the given value. */
-  description_gt?: Maybe<Scalars['String']>,
+  rest_gt?: Maybe<Scalars['Int']>,
   /** All values greater than or equal the given value. */
-  description_gte?: Maybe<Scalars['String']>,
-  /** All values containing the given string. */
-  description_contains?: Maybe<Scalars['String']>,
-  /** All values not containing the given string. */
-  description_not_contains?: Maybe<Scalars['String']>,
-  /** All values starting with the given string. */
-  description_starts_with?: Maybe<Scalars['String']>,
-  /** All values not starting with the given string. */
-  description_not_starts_with?: Maybe<Scalars['String']>,
-  /** All values ending with the given string. */
-  description_ends_with?: Maybe<Scalars['String']>,
-  /** All values not ending with the given string. */
-  description_not_ends_with?: Maybe<Scalars['String']>,
+  rest_gte?: Maybe<Scalars['Int']>,
   format?: Maybe<FormatWhereInput>,
   tracks_every?: Maybe<TrackWhereInput>,
   tracks_some?: Maybe<TrackWhereInput>,
   tracks_none?: Maybe<TrackWhereInput>,
-  exercises_every?: Maybe<ExerciseWhereInput>,
-  exercises_some?: Maybe<ExerciseWhereInput>,
-  exercises_none?: Maybe<ExerciseWhereInput>,
+  blocks_every?: Maybe<BlockWhereInput>,
+  blocks_some?: Maybe<BlockWhereInput>,
+  blocks_none?: Maybe<BlockWhereInput>,
+  exercises_every?: Maybe<ExerciseInstanceWhereInput>,
+  exercises_some?: Maybe<ExerciseInstanceWhereInput>,
+  exercises_none?: Maybe<ExerciseInstanceWhereInput>,
 };
 
 export type Comment = Node & {
@@ -534,20 +473,75 @@ export type Exercise = Node & {
   description: Scalars['String'],
   video: Scalars['String'],
   targets: Array<Scalars['String']>,
-  baseExercise: BaseExercise,
+  baseExercise: Array<Scalars['String']>,
+};
+
+export type ExerciseInstance = Node & {
+   __typename?: 'ExerciseInstance',
+  id: Scalars['ID'],
+  exercise: Exercise,
+  repetitions?: Maybe<Scalars['Int']>,
 };
 
-export enum ExerciseOrderByInput {
+export enum ExerciseInstanceOrderByInput {
   IdAsc = 'id_ASC',
   IdDesc = 'id_DESC',
-  NameAsc = 'name_ASC',
-  NameDesc = 'name_DESC',
-  DescriptionAsc = 'description_ASC',
-  DescriptionDesc = 'description_DESC',
-  VideoAsc = 'video_ASC',
-  VideoDesc = 'video_DESC'
+  RepetitionsAsc = 'repetitions_ASC',
+  RepetitionsDesc = 'repetitions_DESC'
 }
 
+export type ExerciseInstanceWhereInput = {
+  /** Logical AND on all given filters. */
+  AND?: Maybe<Array<ExerciseInstanceWhereInput>>,
+  /** Logical OR on all given filters. */
+  OR?: Maybe<Array<ExerciseInstanceWhereInput>>,
+  /** Logical NOT on all given filters combined by AND. */
+  NOT?: Maybe<Array<ExerciseInstanceWhereInput>>,
+  id?: Maybe<Scalars['ID']>,
+  /** All values that are not equal to given value. */
+  id_not?: Maybe<Scalars['ID']>,
+  /** All values that are contained in given list. */
+  id_in?: Maybe<Array<Scalars['ID']>>,
+  /** All values that are not contained in given list. */
+  id_not_in?: Maybe<Array<Scalars['ID']>>,
+  /** All values less than the given value. */
+  id_lt?: Maybe<Scalars['ID']>,
+  /** All values less than or equal the given value. */
+  id_lte?: Maybe<Scalars['ID']>,
+  /** All values greater than the given value. */
+  id_gt?: Maybe<Scalars['ID']>,
+  /** All values greater than or equal the given value. */
+  id_gte?: Maybe<Scalars['ID']>,
+  /** All values containing the given string. */
+  id_contains?: Maybe<Scalars['ID']>,
+  /** All values not containing the given string. */
+  id_not_contains?: Maybe<Scalars['ID']>,
+  /** All values starting with the given string. */
+  id_starts_with?: Maybe<Scalars['ID']>,
+  /** All values not starting with the given string. */
+  id_not_starts_with?: Maybe<Scalars['ID']>,
+  /** All values ending with the given string. */
+  id_ends_with?: Maybe<Scalars['ID']>,
+  /** All values not ending with the given string. */
+  id_not_ends_with?: Maybe<Scalars['ID']>,
+  repetitions?: Maybe<Scalars['Int']>,
+  /** All values that are not equal to given value. */
+  repetitions_not?: Maybe<Scalars['Int']>,
+  /** All values that are contained in given list. */
+  repetitions_in?: Maybe<Array<Scalars['Int']>>,
+  /** All values that are not contained in given list. */
+  repetitions_not_in?: Maybe<Array<Scalars['Int']>>,
+  /** All values less than the given value. */
+  repetitions_lt?: Maybe<Scalars['Int']>,
+  /** All values less than or equal the given value. */
+  repetitions_lte?: Maybe<Scalars['Int']>,
+  /** All values greater than the given value. */
+  repetitions_gt?: Maybe<Scalars['Int']>,
+  /** All values greater than or equal the given value. */
+  repetitions_gte?: Maybe<Scalars['Int']>,
+  exercise?: Maybe<ExerciseWhereInput>,
+};
+
 export type ExerciseWhereInput = {
   /** Logical AND on all given filters. */
   AND?: Maybe<Array<ExerciseWhereInput>>,
@@ -663,7 +657,6 @@ export type ExerciseWhereInput = {
   video_ends_with?: Maybe<Scalars['String']>,
   /** All values not ending with the given string. */
   video_not_ends_with?: Maybe<Scalars['String']>,
-  baseExercise?: Maybe<BaseExerciseWhereInput>,
 };
 
 export type Format = Node & {
@@ -1330,20 +1323,20 @@ export type Training = Node & {
   id: Scalars['ID'],
   title: Scalars['String'],
   type: TrainingType,
-  content?: Maybe<Array<Block>>,
   createdAt: Scalars['DateTime'],
-  trainingDate: Scalars['DateTime'],
-  location: Scalars['String'],
-  registration?: Maybe<Array<User>>,
-  attendance: Scalars['Int'],
+  trainingDate?: Maybe<Scalars['DateTime']>,
+  location?: Maybe<Scalars['String']>,
+  registrations?: Maybe<Array<User>>,
+  attendance?: Maybe<Scalars['Int']>,
   ratings?: Maybe<Array<Rating>>,
   published: Scalars['Boolean'],
+  blocks?: Maybe<Array<Block>>,
 };
 
 
-export type TrainingContentArgs = {
-  where?: Maybe<BlockWhereInput>,
-  orderBy?: Maybe<BlockOrderByInput>,
+export type TrainingRegistrationsArgs = {
+  where?: Maybe<UserWhereInput>,
+  orderBy?: Maybe<UserOrderByInput>,
   skip?: Maybe<Scalars['Int']>,
   after?: Maybe<Scalars['String']>,
   before?: Maybe<Scalars['String']>,
@@ -1352,9 +1345,9 @@ export type TrainingContentArgs = {
 };
 
 
-export type TrainingRegistrationArgs = {
-  where?: Maybe<UserWhereInput>,
-  orderBy?: Maybe<UserOrderByInput>,
+export type TrainingRatingsArgs = {
+  where?: Maybe<RatingWhereInput>,
+  orderBy?: Maybe<RatingOrderByInput>,
   skip?: Maybe<Scalars['Int']>,
   after?: Maybe<Scalars['String']>,
   before?: Maybe<Scalars['String']>,
@@ -1363,9 +1356,9 @@ export type TrainingRegistrationArgs = {
 };
 
 
-export type TrainingRatingsArgs = {
-  where?: Maybe<RatingWhereInput>,
-  orderBy?: Maybe<RatingOrderByInput>,
+export type TrainingBlocksArgs = {
+  where?: Maybe<BlockWhereInput>,
+  orderBy?: Maybe<BlockOrderByInput>,
   skip?: Maybe<Scalars['Int']>,
   after?: Maybe<Scalars['String']>,
   before?: Maybe<Scalars['String']>,
@@ -1633,15 +1626,15 @@ export type TrainingWhereInput = {
   /** All values that are not equal to given value. */
   published_not?: Maybe<Scalars['Boolean']>,
   type?: Maybe<TrainingTypeWhereInput>,
-  content_every?: Maybe<BlockWhereInput>,
-  content_some?: Maybe<BlockWhereInput>,
-  content_none?: Maybe<BlockWhereInput>,
-  registration_every?: Maybe<UserWhereInput>,
-  registration_some?: Maybe<UserWhereInput>,
-  registration_none?: Maybe<UserWhereInput>,
+  registrations_every?: Maybe<UserWhereInput>,
+  registrations_some?: Maybe<UserWhereInput>,
+  registrations_none?: Maybe<UserWhereInput>,
   ratings_every?: Maybe<RatingWhereInput>,
   ratings_some?: Maybe<RatingWhereInput>,
   ratings_none?: Maybe<RatingWhereInput>,
+  blocks_every?: Maybe<BlockWhereInput>,
+  blocks_some?: Maybe<BlockWhereInput>,
+  blocks_none?: Maybe<BlockWhereInput>,
 };
 
 export type TrainingWhereUniqueInput = {
@@ -1930,41 +1923,23 @@ export type TrainingsQuery = (
   { __typename?: 'Query' }
   & { trainings: Array<Maybe<(
     { __typename?: 'Training' }
-    & Pick<Training, 'id' | 'published' | 'title' | 'createdAt' | 'trainingDate' | 'location' | 'attendance'>
-    & { type: (
-      { __typename?: 'TrainingType' }
-      & Pick<TrainingType, 'id' | 'name' | 'description'>
-    ), content: Maybe<Array<(
-      { __typename?: 'Block' }
-      & Pick<Block, 'id' | 'sequence' | 'title' | 'duration' | 'variation' | 'description'>
-      & { format: Maybe<(
-        { __typename?: 'Format' }
-        & Pick<Format, 'id' | 'name' | 'description'>
-      )>, tracks: Maybe<Array<(
-        { __typename?: 'Track' }
-        & Pick<Track, 'id' | 'title' | 'artist' | 'duration' | 'link'>
-      )>>, exercises: Maybe<Array<(
-        { __typename?: 'Exercise' }
-        & Pick<Exercise, 'id' | 'name' | 'description' | 'video' | 'targets'>
-        & { baseExercise: (
-          { __typename?: 'BaseExercise' }
-          & Pick<BaseExercise, 'id' | 'name'>
-        ) }
-      )>> }
-    )>>, registration: Maybe<Array<(
-      { __typename?: 'User' }
-      & Pick<User, 'id' | 'name'>
-    )>>, ratings: Maybe<Array<(
-      { __typename?: 'Rating' }
-      & Pick<Rating, 'value' | 'comment'>
-      & { user: (
-        { __typename?: 'User' }
-        & Pick<User, 'id' | 'name'>
-      ) }
-    )>> }
+    & Pick<Training, 'id' | 'published'>
   )>> }
 );
 
+export type CreateTrainingMutationVariables = {
+  title: Scalars['String']
+};
+
+
+export type CreateTrainingMutation = (
+  { __typename?: 'Mutation' }
+  & { createTraining: (
+    { __typename?: 'Training' }
+    & Pick<Training, 'id'>
+  ) }
+);
+
 export type UsersQueryVariables = {};
 
 
@@ -2077,63 +2052,10 @@ export type UserUpdateMutation = (
 
 
 export const TrainingsDocument = gql`
-    query Trainings {
+    query trainings {
   trainings {
     id
     published
-    title
-    type {
-      id
-      name
-      description
-    }
-    content {
-      id
-      sequence
-      title
-      duration
-      variation
-      format {
-        id
-        name
-        description
-      }
-      tracks {
-        id
-        title
-        artist
-        duration
-        link
-      }
-      exercises {
-        id
-        name
-        description
-        video
-        targets
-        baseExercise {
-          id
-          name
-        }
-      }
-      description
-    }
-    createdAt
-    trainingDate
-    location
-    registration {
-      id
-      name
-    }
-    attendance
-    ratings {
-      user {
-        id
-        name
-      }
-      value
-      comment
-    }
   }
 }
     `;
@@ -2162,6 +2084,38 @@ export function useTrainingsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHo
 export type TrainingsQueryHookResult = ReturnType<typeof useTrainingsQuery>;
 export type TrainingsLazyQueryHookResult = ReturnType<typeof useTrainingsLazyQuery>;
 export type TrainingsQueryResult = ApolloReactCommon.QueryResult<TrainingsQuery, TrainingsQueryVariables>;
+export const CreateTrainingDocument = gql`
+    mutation createTraining($title: String!) {
+  createTraining(title: $title) {
+    id
+  }
+}
+    `;
+export type CreateTrainingMutationFn = ApolloReactCommon.MutationFunction<CreateTrainingMutation, CreateTrainingMutationVariables>;
+
+/**
+ * __useCreateTrainingMutation__
+ *
+ * To run a mutation, you first call `useCreateTrainingMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useCreateTrainingMutation` 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 [createTrainingMutation, { data, loading, error }] = useCreateTrainingMutation({
+ *   variables: {
+ *      title: // value for 'title'
+ *   },
+ * });
+ */
+export function useCreateTrainingMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTrainingMutation, CreateTrainingMutationVariables>) {
+        return ApolloReactHooks.useMutation<CreateTrainingMutation, CreateTrainingMutationVariables>(CreateTrainingDocument, baseOptions);
+      }
+export type CreateTrainingMutationHookResult = ReturnType<typeof useCreateTrainingMutation>;
+export type CreateTrainingMutationResult = ApolloReactCommon.MutationResult<CreateTrainingMutation>;
+export type CreateTrainingMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTrainingMutation, CreateTrainingMutationVariables>;
 export const UsersDocument = gql`
     query Users {
   users {

+ 29 - 62
frontend/src/timer/components/Timer.tsx

@@ -5,96 +5,72 @@ 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 { useTimer } from '../hooks/useTimer'
+import { useVoice } from '../hooks/useVoice'
+import { useVideo } from '../hooks/useVideo'
 import theme from '../../styles/theme'
 
 const Timer = ({ training }: { training: ITraining }) => {
   const [time, timer] = useTimer({ tickPeriod: 100 })
+  const voice = useVoice('rosie')
 
   const [state, setState] = useState({
     exerciseList: [] as IExerciseItem[],
     totalTime: 0
   })
 
-  const rosie = useRef(
-    new Howl({
-      src: ['/media/ROSIE.mp3'],
-      sprite: {
-        ttt: [114, 2956 - 114],
-        five: [10211, 10883 - 10211],
-        four: [11292, 11873 - 11292],
-        three: [12251, 12809 - 12251],
-        two: [13208, 13930 - 13208],
-        one: [14376, 14948 - 14376],
-        go: [18219, 18735 - 18219],
-        rest: [18988, 19615 - 18988],
-        ninety: [24418, 25540 - 24418],
-        sixty: [25791, 26831 - 25791],
-        thirty: [27027, 28250 - 27027]
-      }
-    })
-  )
-
   useEffect(() => {
-    //console.log("effect 1");
-    rosie.current.play('ttt')
+    voice.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);
-
   const {
     currentExercise,
     previousExercise,
     nextExercise,
     exerciseTime
   } = getPosition(state.exerciseList, timer.time)
-  //console.log("aaa", time, currentExercise);
+
+  const videoSrc =
+    (currentExercise && currentExercise.video) || '/media/block0.mp4'
+  const [videoRef, videoPlayer] = useVideo(videoSrc)
 
   useEffect(() => {
     if (time > state.totalTime) stopTimer()
     const countdown = currentExercise
       ? currentExercise.duration + currentExercise.offset - timer.intTime + 1
       : 0
-    if (timer.running && rosie.current && !rosie.current.playing()) {
+    if (timer.running && voice && !voice.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')
+          voice.play('go')
+        else voice.play('rest')
+      } else if (countdown === 90) voice.play('ninety')
+      else if (countdown === 60) voice.play('sixty')
+      else if (countdown === 30) voice.play('thirty')
+      else if (countdown === 5) voice.play('five')
+      else if (countdown === 4) voice.play('four')
+      else if (countdown === 3) voice.play('three')
+      else if (countdown === 2) voice.play('two')
+      else if (countdown === 1) voice.play('one')
     }
   }, [time, timer.running])
 
-  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 (videoPlayer) videoPlayer.play()
     //if (audio.current) audio.current.play();
-    //console.log("Timer started.");
   }
 
   function stopTimer() {
     timer.stop()
-    if (video.current) video.current.pause()
+    if (videoPlayer) videoPlayer.pause()
     //if (audio.current) audio.current.pause();
-    //console.log("stopped");
   }
 
   function forward() {
@@ -109,14 +85,8 @@ const Timer = ({ training }: { training: ITraining }) => {
     }
   }
 
-  const videoSrc =
-    currentExercise && currentExercise.video
-      ? currentExercise.video
-      : '/media/block0.mp4'
-  //console.log("current state:", currentExercise, state);
-
   return (
-    <div id='timer'>
+    <p id='timer'>
       <h1>{(currentExercise && currentExercise.toplevelBlock) || 'Torture'}</h1>
       <div id='flow'>
         <Countdown
@@ -151,17 +121,14 @@ const Timer = ({ training }: { training: ITraining }) => {
           {nextExercise ? nextExercise.exercise : '😎'}
         </p>
       </div>
-      <div id='description'>
-        <VideoPlayer
-          src={videoSrc}
-          getVideoHandle={(videoHandle: VideoJsPlayer) =>
-            (video.current = videoHandle)
-          }
-        />
+      <p id='description'>
+        <div data-vjs-player>
+          <video ref={videoRef} className='video-js vjs-16-9' />
+        </div>
         <p className='description'>
           {currentExercise && currentExercise.description}
         </p>
-      </div>
+      </p>
 
       <style jsx>{`
         #timer {
@@ -233,7 +200,7 @@ const Timer = ({ training }: { training: ITraining }) => {
           }
         }
       `}</style>
-    </div>
+    </p>
   )
 }
 

+ 0 - 5
frontend/src/timer/components/VideoPlayer.tsx

@@ -39,11 +39,6 @@ const VideoPlayer = ({ getVideoHandle, src }: IVideoPlayer) => {
   return (
     <div data-vjs-player>
       <video ref={videoPlayerRef as any} className='video-js vjs-16-9' />
-      <style jsx>{`
-        div {
-          max-width: 100vw;
-        }
-      `}</style>
     </div>
   )
 }

+ 74 - 0
frontend/src/timer/hooks/useTimer.tsx

@@ -0,0 +1,74 @@
+import { useState, useEffect, useRef } from 'react'
+
+interface ITimer {
+  tickPeriod?: number
+}
+
+interface ITimerHandler {
+  running: boolean
+  time: number
+  intTime: number
+  start: () => void
+  stop: () => void
+  reset: () => void
+  setTime: (value: number) => void
+}
+
+interface ITimeoutRef {
+  current: ReturnType<typeof setTimeout> | undefined
+}
+
+export const useTimer = (args?: ITimer): [number, ITimerHandler] => {
+  const { tickPeriod } = { tickPeriod: 100, ...args }
+
+  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)
+
+  useEffect(() => {
+    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))
+  }
+
+  function start() {
+    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)
+  }
+
+  function reset() {
+    setTime(0)
+  }
+
+  const handler = {
+    running,
+    time,
+    intTime,
+    start,
+    stop,
+    reset,
+    setTime
+  }
+
+  return [intTime, handler]
+}

+ 29 - 0
frontend/src/timer/hooks/useVideo.tsx

@@ -0,0 +1,29 @@
+import { useRef, useEffect } from 'react'
+import videojs, { VideoJsPlayer } from 'video.js'
+
+export const useVideo = (src: string): [any, VideoJsPlayer | undefined] => {
+  const videoPlayerRef: any = useRef()
+  const videoPlayer: { current: VideoJsPlayer | undefined } = useRef()
+
+  useEffect(() => {
+    videoPlayer.current = videojs(videoPlayerRef.current, {
+      width: 512,
+      loop: true,
+      controls: true,
+      muted: true
+    })
+    return () => {
+      if (videoPlayer.current) videoPlayer.current.dispose()
+    }
+  }, [])
+
+  useEffect(() => {
+    if (videoPlayer.current) {
+      const paused = videoPlayer.current.paused()
+      videoPlayer.current.src(src)
+      if (!paused) videoPlayer.current.play()
+    }
+  }, [src])
+
+  return [videoPlayerRef, videoPlayer.current]
+}

+ 45 - 0
frontend/src/timer/hooks/useVoice.tsx

@@ -0,0 +1,45 @@
+import { useRef } from 'react'
+import { Howl } from 'howler'
+
+export const useVoice = (name: 'rosie' | 'britney' | 'siri' = 'rosie') => {
+  let data: IHowlProperties
+  if (name === 'rosie') data = rosie
+  else data = rosie
+
+  const voice = useRef(new Howl(data))
+
+  return voice.current
+}
+
+const rosie: IHowlProperties = {
+  src: ['/media/ROSIE.mp3'],
+  sprite: {
+    ttt: [114, 2956 - 114],
+    ten: [4797, 5354 - 4797],
+    nine: [5872, 6502 - 5872],
+    eight: [6846, 7273 - 6846],
+    seven: [7970, 8625 - 7970],
+    six: [9051, 9783 - 9051],
+    five: [10211, 10883 - 10211],
+    four: [11292, 11873 - 11292],
+    three: [12251, 12809 - 12251],
+    two: [13208, 13930 - 13208],
+    one: [14376, 14948 - 14376],
+    zero: [15519, 16176 - 15519],
+    ready: [16707, 17307 - 16707],
+    set: [17443, 17860 - 17443],
+    go: [18219, 18735 - 18219],
+    rest: [18988, 19615 - 18988],
+    ninety: [24418, 25540 - 24418],
+    sixty: [25791, 26831 - 25791],
+    thirty: [27027, 28250 - 27027],
+    herewegoin: [3468, 4003 - 3468],
+    stop: [19885, 20515 - 19885],
+    round: [20921, 21287 - 20921],
+    roundone: [20923, 21718 - 20923],
+    halfway: [21961, 22724 - 21961],
+    last: [22981, 23526 - 22981],
+    lastround: [22989, 24110 - 22989],
+    coretime: [28478, 29299 - 28478]
+  }
+}

+ 54 - 0
frontend/src/training/components/NewTraining.tsx

@@ -0,0 +1,54 @@
+import { useTrainingsQuery, useCreateTrainingMutation } from '../../gql'
+
+const NewBlock = () => {
+  return <p>nothing</p>
+}
+
+const TrainingList = () => {
+  const { data, error, loading } = useTrainingsQuery()
+
+  return (
+    <ul>{data && data.trainings.map(training => console.log(training))}</ul>
+  )
+}
+
+const NewTraining = () => {
+  const [a, b] = useCreateTrainingMutation()
+
+  return (
+    <>
+      <TrainingList />
+
+      <form>
+        <label>
+          Title
+          <input type='text' />
+        </label>
+        <label>
+          Type
+          <select>
+            <option>Type 1</option>
+          </select>
+        </label>
+        <label>
+          Training date{' '}
+          <input type='date' name='trainingDate' id='trainingDate' />
+        </label>
+        <label>
+          Location <input type='text' name='location' id='location' />
+        </label>
+        <label>
+          Attendance <input type='number' name='attendance' id='attendance' />
+        </label>
+        <label>
+          Published <input type='checkbox' name='published' id='published' />
+        </label>
+        <label>Blocks</label>
+        <NewBlock />
+        <button type='submit'>Save</button>
+      </form>
+    </>
+  )
+}
+
+export default NewTraining

+ 7 - 54
frontend/src/training/training.graphql

@@ -1,59 +1,12 @@
-query Trainings {
+query trainings {
   trainings {
     id
     published
-    title
-    type {
-      id
-      name
-      description
-    }
-    content {
-      id
-      sequence
-      title
-      duration
-      variation
-      format {
-        id
-        name
-        description
-      }
-      tracks {
-        id
-        title
-        artist
-        duration
-        link
-      }
-      exercises {
-        id
-        name
-        description
-        video
-        targets
-        baseExercise {
-          id
-          name
-        }
-      }
-      description
-    }
-    createdAt
-    trainingDate
-    location
-    registration {
-      id
-      name
-    }
-    attendance
-    ratings {
-      user {
-        id
-        name
-      }
-      value
-      comment
-    }
+  }
+}
+
+mutation createTraining($title: String!) {
+  createTraining(title: $title) {
+    id
   }
 }

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

@@ -20,15 +20,14 @@ export interface IBlock {
   id: string
   sequence?: number
   title?: string
-  comment?: string
   description?: string
+  video?: string
   duration?: number
   repetitions?: number
   rest?: number
   format?: IFormat
   blocks?: IBlock[]
   exercises?: IExercise[]
-  video?: string
 }
 
 export interface IFormat {}

+ 2 - 1
frontend/tsconfig.json

@@ -21,6 +21,7 @@
     "**/*.ts",
     "**/*.tsx",
     "lib/apollo.js",
-    "pages/_app.tsx"
+    "pages/_app.tsx",
+    "src/lib/apollo.js"
   ]
 }

Деякі файли не було показано, через те що забагато файлів було змінено