Prechádzať zdrojové kódy

working on the backend.

Tomi Cvetic 5 rokov pred
rodič
commit
ac05af304c

+ 4 - 2
backend/index.ts

@@ -50,8 +50,10 @@ const server = new ApolloServer({
 } as ApolloServerExpressConfig)
 server.applyMiddleware({ app, cors: corsOptions })
 
-app.listen({ port: 4000 }, () => {
-  console.log(`Server ready at http://localhost:4000${server.graphqlPath}.`)
+app.listen({ port: process.env.PORT }, () => {
+  console.log(
+    `Server ready at http://localhost:${process.env.PORT}/${server.graphqlPath}.`
+  )
 })
 
 export default app

+ 2 - 2
backend/package.json

@@ -19,12 +19,12 @@
     "@types/jsonwebtoken": "^8.3.8",
     "@types/lodash": "^4.14.149",
     "@types/randombytes": "^2.0.0",
-    "apollo-server-express": "^2.11.0",
+    "apollo-server-express": "2.11.0",
     "bcryptjs": "^2.4.3",
     "body-parser": "1.19.0",
     "cookie-parser": "1.4.4",
     "cors": "2.8.5",
-    "date-fns": "^2.11.1",
+    "date-fns": "2.11.1",
     "dotenv": "8.2.0",
     "graphql": "^14.6.0",
     "graphql-yoga": "1.18.3",

+ 24 - 5
backend/schema.graphql

@@ -9,7 +9,7 @@ type Query {
     before: String
     first: Int
     last: Int
-  ): [User]!
+  ): [User!]!
   training(where: TrainingWhereUniqueInput!): Training
   trainings(
     where: TrainingWhereInput
@@ -19,7 +19,8 @@ type Query {
     before: String
     first: Int
     last: Int
-  ): [Training]!
+  ): [Training!]!
+  trainingType(where: TrainingTypeWhereUniqueInput!): TrainingType
   trainingTypes(
     where: TrainingTypeWhereInput
     orderBy: TrainingTypeOrderByInput
@@ -28,7 +29,7 @@ type Query {
     before: String
     first: Int
     last: Int
-  ): [TrainingType]!
+  ): [TrainingType!]!
   blocks(
     where: BlockWhereInput
     orderBy: BlockOrderByInput
@@ -37,7 +38,16 @@ type Query {
     before: String
     first: Int
     last: Int
-  ): [Block]!
+  ): [Block!]!
+  formats(
+    where: FormatWhereInput
+    orderBy: FormatOrderByInput
+    skip: Int
+    after: String
+    before: String
+    first: Int
+    last: Int
+  ): [Format!]!
   currentUser: User!
 }
 
@@ -45,7 +55,15 @@ type Mutation {
   createUser(data: UserCreateInput!): User!
   updateUser(email: String!, data: UserUpdateInput!): User
   deleteUser(email: String!): User
-  createTraining(title: String!): Training!
+  createTraining(
+    title: String!
+    type: TrainingTypeCreateOneInput!
+    trainingDate: DateTime!
+    location: String!
+    attendance: Int!
+    published: Boolean!
+    blocks: [BlockCreateInput!]!
+  ): Training!
   createTrainingType(name: String!, description: String!): TrainingType!
   createBlock(
     sequence: Int!
@@ -57,6 +75,7 @@ type Mutation {
     exercises: [ID]!
     description: String!
   ): Block!
+  createFormat(name: String!, description: String!): Format!
   userLogin(email: String!, password: String!): User!
   userLogout: String!
   userSignup(name: String!, email: String!, password: String!): User!

+ 18 - 6
backend/src/training/resolvers.ts

@@ -14,29 +14,36 @@ export const resolvers: IResolvers = {
     },
     trainings: async (parent, args, context, info) => {
       checkPermission(context)
-      return context.db.query.trainings()
+      return context.db.query.trainings({}, info)
     },
     trainingTypes: async (parent, args, context, info) => {
       checkPermission(context)
-      return context.db.query.trainingTypes()
+      return context.db.query.trainingTypes({}, info)
     },
     blocks: async (parent, args, context, info) => {
       checkPermission(context)
-      return context.db.query.trainingTypes()
+      return context.db.query.blocks({}, info)
+    },
+    formats: async (parent, args, context, info) => {
+      checkPermission(context)
+      return context.db.query.formats({}, info)
     }
   },
 
   Mutation: {
     createTraining: async (parent, args, context, info) => {
-      checkPermission(context)
+      console.log('pre permission')
+      checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
+      console.log('post permission')
       const training = await context.db.mutation.createTraining(
         { data: args },
         info
       )
+      console.log('post saving.')
       return training
     },
     createTrainingType: async (parent, args, context, info) => {
-      checkPermission(context)
+      checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
       const trainingType = await context.db.mutation.createTrainingType(
         { data: args },
         info
@@ -44,9 +51,14 @@ export const resolvers: IResolvers = {
       return trainingType
     },
     createBlock: async (parent, args, context, info) => {
-      checkPermission(context)
+      checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
       const block = await context.db.mutation.createBlock({ data: args }, info)
       return block
+    },
+    createFormat: async (parent, args, context, info) => {
+      checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
+      const block = await context.db.mutation.createFormat({ data: args }, info)
+      return block
     }
   }
 }

+ 2 - 1
frontend/global.d.ts

@@ -1 +1,2 @@
-type Dict = { [name: string]: any }
+type Dict = { [key: string]: any }
+type NestedEvent = { target: { type: 'custom'; name: string; value: any } }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1525 - 102
frontend/package-lock.json


+ 17 - 16
frontend/package.json

@@ -11,20 +11,21 @@
     "type-check": "tsc"
   },
   "dependencies": {
-    "@apollo/client": "^3.0.0-beta.16",
+    "@apollo/client": "3.0.0-beta.41",
     "@apollo/react-ssr": "^3.1.3",
     "@types/howler": "^2.1.2",
-    "@types/jest": "^24.0.25",
+    "@types/jest": "24.9.1",
     "@types/lodash": "^4.14.149",
     "@types/styled-jsx": "^2.2.8",
-    "@types/video.js": "^7.3.4",
+    "@types/video.js": "7.3.6",
     "apollo-boost": "0.4.7",
     "apollo-link": "^1.2.13",
     "apollo-link-error": "^1.1.12",
+    "date-fns": "^2.11.1",
     "dotenv": "^8.2.0",
-    "formik": "^2.1.1",
+    "formik": "2.1.4",
     "fuse.js": "3.4.5",
-    "graphql": "^14.5.8",
+    "graphql": "14.6.0",
     "howler": "^2.1.3",
     "isomorphic-unfetch": "^3.0.0",
     "lodash": "^4.17.15",
@@ -32,31 +33,31 @@
     "next-link": "^2.0.0",
     "normalize.css": "^8.0.1",
     "nprogress": "^0.2.0",
-    "react": "^16.11.0",
+    "react": "16.13.1",
     "react-dom": "16.11.0",
-    "standard": "^14.3.1",
+    "standard": "14.3.3",
     "video.js": "^7.7.5",
     "yup": "^0.27.0"
   },
   "devDependencies": {
     "@apollo/react-testing": "^3.1.3",
-    "@babel/core": "^7.7.5",
-    "@babel/preset-env": "^7.7.6",
-    "@babel/preset-react": "^7.7.4",
-    "@testing-library/react": "^9.4.0",
+    "@babel/core": "7.9.0",
+    "@babel/preset-env": "7.9.0",
+    "@babel/preset-react": "7.9.4",
+    "@testing-library/react": "9.5.0",
     "@testing-library/react-hooks": "^3.2.1",
     "@types/enzyme": "^3.10.5",
-    "@types/react": "^16.9.17",
-    "@types/yup": "^0.26.27",
+    "@types/react": "16.9.31",
+    "@types/yup": "0.26.34",
     "@zeit/next-typescript": "^1.1.1",
-    "babel-eslint": "^10.0.3",
+    "babel-eslint": "10.1.0",
     "babel-jest": "^24.9.0",
     "enzyme": "^3.11.0",
     "enzyme-adapter-react-16": "^1.15.2",
     "jest": "^24.9.0",
     "jest-transform-graphql": "^2.1.0",
-    "react-test-renderer": "^16.12.0",
-    "typescript": "^3.7.3"
+    "react-test-renderer": "16.13.1",
+    "typescript": "3.8.3"
   },
   "jest": {
     "setupFilesAfterEnv": [

+ 1 - 1
frontend/pages/training.tsx

@@ -1,4 +1,4 @@
-import NewTraining from '../src/training/components/NewTraining'
+import NewTraining from '../src/training/components/EditTraining'
 
 const TrainingPage = () => {
   return <NewTraining />

+ 93 - 0
frontend/src/form/hooks/useForm.tsx

@@ -0,0 +1,93 @@
+import { useState, ChangeEvent } from 'react'
+import { format, parse, parseISO } from 'date-fns'
+import { set, cloneDeep } from 'lodash'
+
+export function useForm<T extends Dict>(initialValues: T) {
+  const [values, setValues] = useState(initialValues)
+
+  const touched = Object.entries(initialValues)
+    .filter(([key, value]) => value !== values[key])
+    .map(([key]) => key)
+
+  function handleChange(
+    event:
+      | ChangeEvent<HTMLInputElement>
+      | ChangeEvent<HTMLSelectElement>
+      | NestedEvent
+  ) {
+    const { type, name, value } = event.target
+    const newValue =
+      type === 'checkbox'
+        ? (event.target as EventTarget & HTMLInputElement).checked
+        : value
+
+    if (name.includes('.')) {
+      const copy = cloneDeep(values)
+      set(copy, name, newValue)
+      setValues(copy)
+    } else {
+      setValues({ ...values, [name]: newValue })
+    }
+  }
+
+  return { touched, values, handleChange }
+}
+
+export const DateTimeInput = ({ name, id, value, onChange, ...props }: any) => {
+  const timeFormat = 'HH:mm'
+  const dateFormat = 'yyyy-MM-dd'
+  const date = parseISO(value)
+  console.log('render', date)
+  const [state, setState] = useState({
+    date: format(parseISO(value), dateFormat),
+    time: format(parseISO(value), timeFormat)
+  })
+
+  function handleChange(event: ChangeEvent<HTMLInputElement>) {
+    setState({ ...state, [event.target.name]: event.target.value })
+  }
+
+  function handleBlur(event: ChangeEvent<HTMLInputElement>) {
+    const timeISO = parse(
+      `${state.date} ${state.time}`,
+      `${dateFormat} ${timeFormat}`,
+      new Date(0)
+    )
+    const returnEvent = {
+      ...event,
+      target: {
+        ...event.target,
+        name,
+        type: 'text',
+        value: timeISO.toISOString()
+      }
+    }
+    console.log('blur', state, timeISO, returnEvent)
+    onChange(returnEvent)
+  }
+
+  return (
+    <>
+      <input
+        type='date'
+        id='date'
+        name='date'
+        {...props}
+        step={60}
+        value={state.date}
+        onChange={handleChange}
+        onBlur={handleBlur}
+      />
+      <input
+        type='time'
+        id='time'
+        name='time'
+        {...props}
+        step={60}
+        value={state.time}
+        onChange={handleChange}
+        onBlur={handleBlur}
+      />
+    </>
+  )
+}

+ 398 - 10
frontend/src/gql/index.tsx

@@ -63,6 +63,35 @@ export type BlockExercisesArgs = {
   last?: Maybe<Scalars['Int']>
 };
 
+export type BlockCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  sequence: Scalars['Int'],
+  title: Scalars['String'],
+  description?: Maybe<Scalars['String']>,
+  duration?: Maybe<Scalars['Int']>,
+  rounds?: Maybe<Scalars['Int']>,
+  rest?: Maybe<Scalars['Int']>,
+  videos?: Maybe<BlockCreatevideosInput>,
+  pictures?: Maybe<BlockCreatepicturesInput>,
+  format: FormatCreateOneInput,
+  tracks?: Maybe<TrackCreateManyInput>,
+  blocks?: Maybe<BlockCreateManyInput>,
+  exercises?: Maybe<ExerciseInstanceCreateManyInput>,
+};
+
+export type BlockCreateManyInput = {
+  create?: Maybe<Array<BlockCreateInput>>,
+  connect?: Maybe<Array<BlockWhereUniqueInput>>,
+};
+
+export type BlockCreatepicturesInput = {
+  set?: Maybe<Array<Scalars['String']>>,
+};
+
+export type BlockCreatevideosInput = {
+  set?: Maybe<Array<Scalars['String']>>,
+};
+
 export enum BlockOrderByInput {
   IdAsc = 'id_ASC',
   IdDesc = 'id_DESC',
@@ -240,6 +269,10 @@ export type BlockWhereInput = {
   exercises_none?: Maybe<ExerciseInstanceWhereInput>,
 };
 
+export type BlockWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+};
+
 export type Comment = Node & {
   id: Scalars['ID'],
   text: Scalars['String'],
@@ -473,12 +506,45 @@ export type Exercise = Node & {
   baseExercise: Array<Scalars['String']>,
 };
 
+export type ExerciseCreatebaseExerciseInput = {
+  set?: Maybe<Array<Scalars['String']>>,
+};
+
+export type ExerciseCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  name: Scalars['String'],
+  description: Scalars['String'],
+  video: Scalars['String'],
+  targets?: Maybe<ExerciseCreatetargetsInput>,
+  baseExercise?: Maybe<ExerciseCreatebaseExerciseInput>,
+};
+
+export type ExerciseCreateOneInput = {
+  create?: Maybe<ExerciseCreateInput>,
+  connect?: Maybe<ExerciseWhereUniqueInput>,
+};
+
+export type ExerciseCreatetargetsInput = {
+  set?: Maybe<Array<Scalars['String']>>,
+};
+
 export type ExerciseInstance = Node & {
   id: Scalars['ID'],
   exercise: Exercise,
   repetitions?: Maybe<Scalars['Int']>,
 };
 
+export type ExerciseInstanceCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  repetitions?: Maybe<Scalars['Int']>,
+  exercise: ExerciseCreateOneInput,
+};
+
+export type ExerciseInstanceCreateManyInput = {
+  create?: Maybe<Array<ExerciseInstanceCreateInput>>,
+  connect?: Maybe<Array<ExerciseInstanceWhereUniqueInput>>,
+};
+
 export enum ExerciseInstanceOrderByInput {
   IdAsc = 'id_ASC',
   IdDesc = 'id_DESC',
@@ -538,6 +604,10 @@ export type ExerciseInstanceWhereInput = {
   exercise?: Maybe<ExerciseWhereInput>,
 };
 
+export type ExerciseInstanceWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+};
+
 export type ExerciseWhereInput = {
   /** Logical AND on all given filters. */
   AND?: Maybe<Array<ExerciseWhereInput>>,
@@ -655,12 +725,36 @@ export type ExerciseWhereInput = {
   video_not_ends_with?: Maybe<Scalars['String']>,
 };
 
+export type ExerciseWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+};
+
 export type Format = Node & {
   id: Scalars['ID'],
   name: Scalars['String'],
   description: Scalars['String'],
 };
 
+export type FormatCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  name: Scalars['String'],
+  description: Scalars['String'],
+};
+
+export type FormatCreateOneInput = {
+  create?: Maybe<FormatCreateInput>,
+  connect?: Maybe<FormatWhereUniqueInput>,
+};
+
+export enum FormatOrderByInput {
+  IdAsc = 'id_ASC',
+  IdDesc = 'id_DESC',
+  NameAsc = 'name_ASC',
+  NameDesc = 'name_DESC',
+  DescriptionAsc = 'description_ASC',
+  DescriptionDesc = 'description_DESC'
+}
+
 export type FormatWhereInput = {
   /** Logical AND on all given filters. */
   AND?: Maybe<Array<FormatWhereInput>>,
@@ -751,6 +845,10 @@ export type FormatWhereInput = {
   description_not_ends_with?: Maybe<Scalars['String']>,
 };
 
+export type FormatWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+};
+
 export type Mutation = {
   createUser: User,
   updateUser?: Maybe<User>,
@@ -758,6 +856,7 @@ export type Mutation = {
   createTraining: Training,
   createTrainingType: TrainingType,
   createBlock: Block,
+  createFormat: Format,
   userLogin: User,
   userLogout: Scalars['String'],
   userSignup: User,
@@ -783,7 +882,13 @@ export type MutationDeleteUserArgs = {
 
 
 export type MutationCreateTrainingArgs = {
-  title: Scalars['String']
+  title: Scalars['String'],
+  type: TrainingTypeCreateOneInput,
+  trainingDate: Scalars['DateTime'],
+  location: Scalars['String'],
+  attendance: Scalars['Int'],
+  published: Scalars['Boolean'],
+  blocks: Array<BlockCreateInput>
 };
 
 
@@ -805,6 +910,12 @@ export type MutationCreateBlockArgs = {
 };
 
 
+export type MutationCreateFormatArgs = {
+  name: Scalars['String'],
+  description: Scalars['String']
+};
+
+
 export type MutationUserLoginArgs = {
   email: Scalars['String'],
   password: Scalars['String']
@@ -840,11 +951,13 @@ export enum Permission {
 }
 
 export type Query = {
-  users: Array<Maybe<User>>,
+  users: Array<User>,
   training?: Maybe<Training>,
-  trainings: Array<Maybe<Training>>,
-  trainingTypes: Array<Maybe<TrainingType>>,
-  blocks: Array<Maybe<Block>>,
+  trainings: Array<Training>,
+  trainingType?: Maybe<TrainingType>,
+  trainingTypes: Array<TrainingType>,
+  blocks: Array<Block>,
+  formats: Array<Format>,
   currentUser: User,
 };
 
@@ -876,6 +989,11 @@ export type QueryTrainingsArgs = {
 };
 
 
+export type QueryTrainingTypeArgs = {
+  where: TrainingTypeWhereUniqueInput
+};
+
+
 export type QueryTrainingTypesArgs = {
   where?: Maybe<TrainingTypeWhereInput>,
   orderBy?: Maybe<TrainingTypeOrderByInput>,
@@ -897,6 +1015,17 @@ export type QueryBlocksArgs = {
   last?: Maybe<Scalars['Int']>
 };
 
+
+export type QueryFormatsArgs = {
+  where?: Maybe<FormatWhereInput>,
+  orderBy?: Maybe<FormatOrderByInput>,
+  skip?: Maybe<Scalars['Int']>,
+  after?: Maybe<Scalars['String']>,
+  before?: Maybe<Scalars['String']>,
+  first?: Maybe<Scalars['Int']>,
+  last?: Maybe<Scalars['Int']>
+};
+
 export type Rating = Node & {
   id: Scalars['ID'],
   user: User,
@@ -1164,6 +1293,19 @@ export type Track = Node & {
   link: Scalars['String'],
 };
 
+export type TrackCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  title: Scalars['String'],
+  artist: Scalars['String'],
+  duration: Scalars['Int'],
+  link: Scalars['String'],
+};
+
+export type TrackCreateManyInput = {
+  create?: Maybe<Array<TrackCreateInput>>,
+  connect?: Maybe<Array<TrackWhereUniqueInput>>,
+};
+
 export enum TrackOrderByInput {
   IdAsc = 'id_ASC',
   IdDesc = 'id_DESC',
@@ -1309,6 +1451,10 @@ export type TrackWhereInput = {
   link_not_ends_with?: Maybe<Scalars['String']>,
 };
 
+export type TrackWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+};
+
 export type Training = Node & {
   id: Scalars['ID'],
   title: Scalars['String'],
@@ -1379,6 +1525,17 @@ export type TrainingType = Node & {
   description: Scalars['String'],
 };
 
+export type TrainingTypeCreateInput = {
+  id?: Maybe<Scalars['ID']>,
+  name: Scalars['String'],
+  description: Scalars['String'],
+};
+
+export type TrainingTypeCreateOneInput = {
+  create?: Maybe<TrainingTypeCreateInput>,
+  connect?: Maybe<TrainingTypeWhereUniqueInput>,
+};
+
 export enum TrainingTypeOrderByInput {
   IdAsc = 'id_ASC',
   IdDesc = 'id_DESC',
@@ -1478,6 +1635,11 @@ export type TrainingTypeWhereInput = {
   description_not_ends_with?: Maybe<Scalars['String']>,
 };
 
+export type TrainingTypeWhereUniqueInput = {
+  id?: Maybe<Scalars['ID']>,
+  name?: Maybe<Scalars['String']>,
+};
+
 export type TrainingWhereInput = {
   /** Logical AND on all given filters. */
   AND?: Maybe<Array<TrainingWhereInput>>,
@@ -1907,19 +2069,61 @@ export type UserWhereInput = {
 export type TrainingsQueryVariables = {};
 
 
-export type TrainingsQuery = { trainings: Array<Maybe<Pick<Training, 'id' | 'published'>>> };
+export type TrainingsQuery = { trainings: Array<(
+    Pick<Training, 'id' | 'title' | 'location' | 'trainingDate' | 'attendance' | 'published'>
+    & { type: Pick<TrainingType, 'id' | 'name' | 'description'> }
+  )> };
+
+export type TrainingTypesQueryVariables = {};
+
+
+export type TrainingTypesQuery = { trainingTypes: Array<Pick<TrainingType, 'id' | 'name' | 'description'>> };
+
+export type TrainingTypeQueryVariables = {
+  where: TrainingTypeWhereUniqueInput
+};
+
+
+export type TrainingTypeQuery = { trainingType: Maybe<Pick<TrainingType, 'id' | 'name' | 'description'>> };
+
+export type FormatsQueryVariables = {};
+
+
+export type FormatsQuery = { formats: Array<Pick<Format, 'id' | 'name' | 'description'>> };
 
 export type CreateTrainingMutationVariables = {
-  title: Scalars['String']
+  title: Scalars['String'],
+  type: TrainingTypeCreateOneInput,
+  trainingDate: Scalars['DateTime'],
+  location: Scalars['String'],
+  attendance: Scalars['Int'],
+  published: Scalars['Boolean'],
+  blocks: Array<BlockCreateInput>
 };
 
 
 export type CreateTrainingMutation = { createTraining: Pick<Training, 'id'> };
 
+export type CreateTrainingTypeMutationVariables = {
+  name: Scalars['String'],
+  description: Scalars['String']
+};
+
+
+export type CreateTrainingTypeMutation = { createTrainingType: Pick<TrainingType, 'id'> };
+
+export type CreateFormatMutationVariables = {
+  name: Scalars['String'],
+  description: Scalars['String']
+};
+
+
+export type CreateFormatMutation = { createFormat: Pick<Format, 'id'> };
+
 export type UsersQueryVariables = {};
 
 
-export type UsersQuery = { users: Array<Maybe<Pick<User, 'id' | 'email' | 'name' | 'permissions' | 'interests'>>> };
+export type UsersQuery = { users: Array<Pick<User, 'id' | 'email' | 'name' | 'permissions' | 'interests'>> };
 
 export type UserSignupMutationVariables = {
   email: Scalars['String'],
@@ -1983,6 +2187,15 @@ export const TrainingsDocument = gql`
     query trainings {
   trainings {
     id
+    title
+    type {
+      id
+      name
+      description
+    }
+    location
+    trainingDate
+    attendance
     published
   }
 }
@@ -2012,9 +2225,112 @@ 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 TrainingTypesDocument = gql`
+    query trainingTypes {
+  trainingTypes {
+    id
+    name
+    description
+  }
+}
+    `;
+
+/**
+ * __useTrainingTypesQuery__
+ *
+ * To run a query within a React component, call `useTrainingTypesQuery` and pass it any options that fit your needs.
+ * When your component renders, `useTrainingTypesQuery` returns an object from Apollo Client that contains loading, error, and data properties 
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useTrainingTypesQuery({
+ *   variables: {
+ *   },
+ * });
+ */
+export function useTrainingTypesQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<TrainingTypesQuery, TrainingTypesQueryVariables>) {
+        return ApolloReactHooks.useQuery<TrainingTypesQuery, TrainingTypesQueryVariables>(TrainingTypesDocument, baseOptions);
+      }
+export function useTrainingTypesLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<TrainingTypesQuery, TrainingTypesQueryVariables>) {
+          return ApolloReactHooks.useLazyQuery<TrainingTypesQuery, TrainingTypesQueryVariables>(TrainingTypesDocument, baseOptions);
+        }
+export type TrainingTypesQueryHookResult = ReturnType<typeof useTrainingTypesQuery>;
+export type TrainingTypesLazyQueryHookResult = ReturnType<typeof useTrainingTypesLazyQuery>;
+export type TrainingTypesQueryResult = ApolloReactCommon.QueryResult<TrainingTypesQuery, TrainingTypesQueryVariables>;
+export const TrainingTypeDocument = gql`
+    query trainingType($where: TrainingTypeWhereUniqueInput!) {
+  trainingType(where: $where) {
+    id
+    name
+    description
+  }
+}
+    `;
+
+/**
+ * __useTrainingTypeQuery__
+ *
+ * To run a query within a React component, call `useTrainingTypeQuery` and pass it any options that fit your needs.
+ * When your component renders, `useTrainingTypeQuery` returns an object from Apollo Client that contains loading, error, and data properties 
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useTrainingTypeQuery({
+ *   variables: {
+ *      where: // value for 'where'
+ *   },
+ * });
+ */
+export function useTrainingTypeQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<TrainingTypeQuery, TrainingTypeQueryVariables>) {
+        return ApolloReactHooks.useQuery<TrainingTypeQuery, TrainingTypeQueryVariables>(TrainingTypeDocument, baseOptions);
+      }
+export function useTrainingTypeLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<TrainingTypeQuery, TrainingTypeQueryVariables>) {
+          return ApolloReactHooks.useLazyQuery<TrainingTypeQuery, TrainingTypeQueryVariables>(TrainingTypeDocument, baseOptions);
+        }
+export type TrainingTypeQueryHookResult = ReturnType<typeof useTrainingTypeQuery>;
+export type TrainingTypeLazyQueryHookResult = ReturnType<typeof useTrainingTypeLazyQuery>;
+export type TrainingTypeQueryResult = ApolloReactCommon.QueryResult<TrainingTypeQuery, TrainingTypeQueryVariables>;
+export const FormatsDocument = gql`
+    query formats {
+  formats {
+    id
+    name
+    description
+  }
+}
+    `;
+
+/**
+ * __useFormatsQuery__
+ *
+ * To run a query within a React component, call `useFormatsQuery` and pass it any options that fit your needs.
+ * When your component renders, `useFormatsQuery` returns an object from Apollo Client that contains loading, error, and data properties 
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useFormatsQuery({
+ *   variables: {
+ *   },
+ * });
+ */
+export function useFormatsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<FormatsQuery, FormatsQueryVariables>) {
+        return ApolloReactHooks.useQuery<FormatsQuery, FormatsQueryVariables>(FormatsDocument, baseOptions);
+      }
+export function useFormatsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<FormatsQuery, FormatsQueryVariables>) {
+          return ApolloReactHooks.useLazyQuery<FormatsQuery, FormatsQueryVariables>(FormatsDocument, baseOptions);
+        }
+export type FormatsQueryHookResult = ReturnType<typeof useFormatsQuery>;
+export type FormatsLazyQueryHookResult = ReturnType<typeof useFormatsLazyQuery>;
+export type FormatsQueryResult = ApolloReactCommon.QueryResult<FormatsQuery, FormatsQueryVariables>;
 export const CreateTrainingDocument = gql`
-    mutation createTraining($title: String!) {
-  createTraining(title: $title) {
+    mutation createTraining($title: String!, $type: TrainingTypeCreateOneInput!, $trainingDate: DateTime!, $location: String!, $attendance: Int!, $published: Boolean!, $blocks: [BlockCreateInput!]!) {
+  createTraining(title: $title, type: $type, trainingDate: $trainingDate, location: $location, attendance: $attendance, published: $published, blocks: $blocks) {
     id
   }
 }
@@ -2035,6 +2351,12 @@ export type CreateTrainingMutationFn = ApolloReactCommon.MutationFunction<Create
  * const [createTrainingMutation, { data, loading, error }] = useCreateTrainingMutation({
  *   variables: {
  *      title: // value for 'title'
+ *      type: // value for 'type'
+ *      trainingDate: // value for 'trainingDate'
+ *      location: // value for 'location'
+ *      attendance: // value for 'attendance'
+ *      published: // value for 'published'
+ *      blocks: // value for 'blocks'
  *   },
  * });
  */
@@ -2044,6 +2366,72 @@ export function useCreateTrainingMutation(baseOptions?: ApolloReactHooks.Mutatio
 export type CreateTrainingMutationHookResult = ReturnType<typeof useCreateTrainingMutation>;
 export type CreateTrainingMutationResult = ApolloReactCommon.MutationResult<CreateTrainingMutation>;
 export type CreateTrainingMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTrainingMutation, CreateTrainingMutationVariables>;
+export const CreateTrainingTypeDocument = gql`
+    mutation createTrainingType($name: String!, $description: String!) {
+  createTrainingType(name: $name, description: $description) {
+    id
+  }
+}
+    `;
+export type CreateTrainingTypeMutationFn = ApolloReactCommon.MutationFunction<CreateTrainingTypeMutation, CreateTrainingTypeMutationVariables>;
+
+/**
+ * __useCreateTrainingTypeMutation__
+ *
+ * To run a mutation, you first call `useCreateTrainingTypeMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useCreateTrainingTypeMutation` 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 [createTrainingTypeMutation, { data, loading, error }] = useCreateTrainingTypeMutation({
+ *   variables: {
+ *      name: // value for 'name'
+ *      description: // value for 'description'
+ *   },
+ * });
+ */
+export function useCreateTrainingTypeMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateTrainingTypeMutation, CreateTrainingTypeMutationVariables>) {
+        return ApolloReactHooks.useMutation<CreateTrainingTypeMutation, CreateTrainingTypeMutationVariables>(CreateTrainingTypeDocument, baseOptions);
+      }
+export type CreateTrainingTypeMutationHookResult = ReturnType<typeof useCreateTrainingTypeMutation>;
+export type CreateTrainingTypeMutationResult = ApolloReactCommon.MutationResult<CreateTrainingTypeMutation>;
+export type CreateTrainingTypeMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateTrainingTypeMutation, CreateTrainingTypeMutationVariables>;
+export const CreateFormatDocument = gql`
+    mutation createFormat($name: String!, $description: String!) {
+  createFormat(name: $name, description: $description) {
+    id
+  }
+}
+    `;
+export type CreateFormatMutationFn = ApolloReactCommon.MutationFunction<CreateFormatMutation, CreateFormatMutationVariables>;
+
+/**
+ * __useCreateFormatMutation__
+ *
+ * To run a mutation, you first call `useCreateFormatMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useCreateFormatMutation` 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 [createFormatMutation, { data, loading, error }] = useCreateFormatMutation({
+ *   variables: {
+ *      name: // value for 'name'
+ *      description: // value for 'description'
+ *   },
+ * });
+ */
+export function useCreateFormatMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<CreateFormatMutation, CreateFormatMutationVariables>) {
+        return ApolloReactHooks.useMutation<CreateFormatMutation, CreateFormatMutationVariables>(CreateFormatDocument, baseOptions);
+      }
+export type CreateFormatMutationHookResult = ReturnType<typeof useCreateFormatMutation>;
+export type CreateFormatMutationResult = ApolloReactCommon.MutationResult<CreateFormatMutation>;
+export type CreateFormatMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateFormatMutation, CreateFormatMutationVariables>;
 export const UsersDocument = gql`
     query Users {
   users {

+ 1 - 1
frontend/src/lib/apollo.js

@@ -24,7 +24,7 @@ const client = new ApolloClient({
       }
     }),
     new HttpLink({
-      uri: 'http://localhost:8801/',
+      uri: 'http://localhost:8801/graphql',
       credentials: 'include',
       fetch
     })

+ 81 - 0
frontend/src/training/components/EditBlock.tsx

@@ -0,0 +1,81 @@
+import { get } from 'lodash'
+import { useFormatsQuery } from '../../gql'
+import FormatSelector from './FormatSelector'
+
+const EditBlock = ({
+  onChange,
+  value,
+  name
+}: {
+  onChange: any
+  value: any
+  name: string
+}) => {
+  const sequence = `${name}.sequence`
+  const title = `${name}.title`
+  const description = `${name}.description`
+  const duration = `${name}.duration`
+  const rounds = `${name}.rounds`
+  const format = `${name}.format`
+  const rest = `${name}.rest`
+
+  return (
+    <>
+      <label>Sequence</label>
+      <input
+        type='number'
+        name={sequence}
+        id={sequence}
+        value={get(value, sequence)}
+        onChange={onChange}
+      />
+      <label>Title</label>
+      <input
+        type='text'
+        name={title}
+        id={title}
+        value={get(value, title)}
+        onChange={onChange}
+      />
+      <label>Description</label>
+      <input
+        type='text'
+        name={description}
+        id={description}
+        value={get(value, description)}
+        onChange={onChange}
+      />
+      <label>duration</label>
+      <input
+        type='number'
+        name={duration}
+        id={duration}
+        value={get(value, duration)}
+        onChange={onChange}
+      />
+      <label>Rounds</label>
+      <FormatSelector
+        name={format}
+        value={get(value, format)}
+        onChange={onChange}
+      />
+      <input
+        type='number'
+        name={rounds}
+        id={rounds}
+        value={get(value, rounds)}
+        onChange={onChange}
+      />
+      <label>Rest</label>
+      <input
+        type='number'
+        name={rest}
+        id={rest}
+        value={get(value, rest)}
+        onChange={onChange}
+      />
+    </>
+  )
+}
+
+export default EditBlock

+ 122 - 0
frontend/src/training/components/EditTraining.tsx

@@ -0,0 +1,122 @@
+import {
+  useTrainingsQuery,
+  useCreateTrainingMutation,
+  BlockCreateInput
+} from '../../gql'
+import { useForm } from '../../form/hooks/useForm'
+import EditBlock from './EditBlock'
+import TrainingTypeSelector from './TrainingTypeSelector'
+
+const TrainingList = () => {
+  const { data, error, loading } = useTrainingsQuery()
+
+  return (
+    <ul>{data && data.trainings.map(training => <li>{training.id}</li>)}</ul>
+  )
+}
+
+const NewTraining = ({ id }: { id?: string }) => {
+  const { values, touched, handleChange } = useForm({
+    title: '',
+    type: undefined,
+    trainingDate: '',
+    location: '',
+    attendance: 0,
+    published: false,
+    blocks: [] as BlockCreateInput[]
+  })
+  const [createTraining, createData] = useCreateTrainingMutation()
+
+  return (
+    <>
+      <TrainingList />
+
+      <form
+        onSubmit={ev => {
+          ev.preventDefault()
+          createTraining({
+            variables: {
+              ...values,
+              type: { connect: { id: values.type } }
+            }
+          })
+        }}
+      >
+        <label>Title</label>
+        <input
+          type='text'
+          name='title'
+          id='title'
+          value={values.title}
+          onChange={handleChange}
+        />
+        <TrainingTypeSelector value={values.type} onChange={handleChange} />
+        <label>Training date</label>
+        <input
+          type='date'
+          name='trainingDate'
+          id='trainingDate'
+          value={values.trainingDate}
+          onChange={handleChange}
+        />
+        <label>Location</label>
+        <input
+          type='text'
+          name='location'
+          id='location'
+          value={values.location}
+          onChange={handleChange}
+        />
+        <label>Attendance</label>
+        <input
+          type='number'
+          name='attendance'
+          id='attendance'
+          value={values.attendance}
+          onChange={handleChange}
+        />
+        <label>Published</label>
+        <input
+          type='checkbox'
+          name='published'
+          id='published'
+          checked={values.published}
+          onChange={handleChange}
+        />
+        <label>Blocks</label>
+        {values.blocks.map((block, index) => (
+          <EditBlock
+            key={index}
+            name={`blocks.${index}`}
+            value={block}
+            onChange={handleChange}
+          />
+        ))}
+        <button
+          onClick={event => {
+            event.preventDefault()
+            handleChange({
+              target: {
+                type: 'custom',
+                name: 'blocks',
+                value: [...values.blocks, { sequence: 0, title: '' }]
+              }
+            })
+          }}
+          type='button'
+        >
+          Add block
+        </button>
+        <button type='submit' disabled={createData.loading}>
+          Save
+        </button>
+        {createData.data && <span color='green'>Saved.</span>}
+        {createData.error && (
+          <span color='red'>Error saving: {createData.error.message}</span>
+        )}
+      </form>
+    </>
+  )
+}
+
+export default NewTraining

+ 76 - 0
frontend/src/training/components/FormatSelector.tsx

@@ -0,0 +1,76 @@
+import { useFormatsQuery } from '../../gql'
+import { ChangeEvent } from 'react'
+
+interface IFormatSelector {
+  value: { connect: { id: string } } | undefined
+  name?: string
+  label?: string
+  onChange: (event: ChangeEvent<HTMLSelectElement> | NestedEvent) => void
+}
+
+const FormatSelector = ({
+  onChange,
+  value,
+  name = 'format',
+  label = 'Format',
+  ...props
+}: IFormatSelector) => {
+  const formats = useFormatsQuery()
+  const id = value && value.connect.id
+
+  if (formats.data && !value) {
+    const id = formats.data.formats[0].id
+    onChange({
+      target: {
+        type: 'custom',
+        value: { connect: { id } },
+        name
+      }
+    })
+  }
+
+  return (
+    <>
+      <label>{label}</label>
+      <select
+        id={name}
+        name={name}
+        value={id}
+        onChange={event => {
+          const copy: NestedEvent = {
+            target: {
+              type: 'custom',
+              value: { connect: { id: event.target.value } },
+              name
+            }
+          }
+          onChange(copy)
+        }}
+        {...props}
+      >
+        {formats.loading && 'loading formats...'}
+        {formats.error && 'error loading formats'}
+        {formats.data &&
+          formats.data.formats.map(format => (
+            <option
+              key={format.id}
+              value={format.id}
+              selected={id === format.id}
+            >
+              {format.name}
+            </option>
+          ))}
+      </select>
+      <button
+        type='button'
+        onClick={event => {
+          event.preventDefault()
+        }}
+      >
+        Add format
+      </button>
+    </>
+  )
+}
+
+export default FormatSelector

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

@@ -1,54 +0,0 @@
-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

+ 75 - 0
frontend/src/training/components/TrainingTypeSelector.tsx

@@ -0,0 +1,75 @@
+import { ChangeEvent } from 'react'
+import { useTrainingTypesQuery } from '../../gql'
+
+interface ITrainingTypeSelector {
+  value: { connect: { id: string } } | undefined
+  label?: string
+  name?: string
+  onChange: (event: ChangeEvent<HTMLSelectElement> | NestedEvent) => void
+}
+
+const TrainingTypeSelector = ({
+  onChange,
+  value,
+  name = 'type',
+  label = 'Training type',
+  ...props
+}: ITrainingTypeSelector) => {
+  const trainingTypes = useTrainingTypesQuery()
+  const id = value && value.connect.id
+
+  if (trainingTypes.data && !value) {
+    const id = trainingTypes.data.trainingTypes[0].id
+    onChange({
+      target: {
+        type: 'custom',
+        value: { connect: { id } },
+        name
+      }
+    })
+  }
+
+  return (
+    <>
+      <label>{label}</label>
+      <select
+        id={name}
+        name={name}
+        value={id}
+        onChange={event => {
+          const copy: NestedEvent = {
+            target: {
+              type: 'custom',
+              value: { connect: { id: event.target.value } },
+              name
+            }
+          }
+          onChange(copy)
+        }}
+        {...props}
+      >
+        {trainingTypes.loading && 'loading training types...'}
+        {trainingTypes.error && 'error loading training types'}
+        {trainingTypes.data &&
+          trainingTypes.data.trainingTypes.map(trainingType => (
+            <option
+              key={trainingType.id}
+              value={trainingType.id}
+              selected={id === trainingType.id}
+            >
+              {trainingType.name}
+            </option>
+          ))}
+      </select>
+      <button
+        type='button'
+        onClick={event => {
+          event.preventDefault()
+        }}
+      >
+        Add type
+      </button>
+    </>
+  )
+}
+export default TrainingTypeSelector

+ 65 - 2
frontend/src/training/training.graphql

@@ -1,12 +1,75 @@
+# import * from '../../../backend/database/generated/prisma.graphql'
+
 query trainings {
   trainings {
     id
+    title
+    type {
+      id
+      name
+      description
+    }
+    location
+    trainingDate
+    attendance
     published
   }
 }
 
-mutation createTraining($title: String!) {
-  createTraining(title: $title) {
+query trainingTypes {
+  trainingTypes {
+    id
+    name
+    description
+  }
+}
+
+query trainingType($where: TrainingTypeWhereUniqueInput!) {
+  trainingType(where: $where) {
+    id
+    name
+    description
+  }
+}
+
+query formats {
+  formats {
+    id
+    name
+    description
+  }
+}
+
+mutation createTraining(
+  $title: String!
+  $type: TrainingTypeCreateOneInput!
+  $trainingDate: DateTime!
+  $location: String!
+  $attendance: Int!
+  $published: Boolean!
+  $blocks: [BlockCreateInput!]!
+) {
+  createTraining(
+    title: $title
+    type: $type
+    trainingDate: $trainingDate
+    location: $location
+    attendance: $attendance
+    published: $published
+    blocks: $blocks
+  ) {
+    id
+  }
+}
+
+mutation createTrainingType($name: String!, $description: String!) {
+  createTrainingType(name: $name, description: $description) {
+    id
+  }
+}
+
+mutation createFormat($name: String!, $description: String!) {
+  createFormat(name: $name, description: $description) {
     id
   }
 }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov