Tomi Cvetic 4 anni fa
parent
commit
6023292e45

+ 3 - 3
backend/schema.graphql

@@ -122,7 +122,7 @@ type Mutation {
   userSignup(name: String!, email: String!, password: String!): User!
   requestReset(email: String!): String!
   resetPassword(token: String!, password: String!): User!
-  register(training: ID!): String!
-  deregister(training: ID!): String!
-  publish(training: ID!, status: Boolean): String!
+  register(training: ID!): Training!
+  deregister(training: ID!): Training!
+  publish(training: ID!, status: Boolean): Training!
 }

+ 8 - 12
backend/src/training/resolvers.ts

@@ -84,34 +84,30 @@ export const resolvers: IResolvers = {
     //   const block = await context.db.mutation.createBlock({ data: args }, info)
     //   return block
     // },
-    createFormat: async (parent, args, context, info) => {
+    createFormat: (parent, args, context, info) => {
       checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
-      const block = await context.db.mutation.createFormat({ data: args }, info)
-      return block
+      return context.db.mutation.createFormat({ data: args }, info)
     },
-    register: async (parent, args, context, info) => {
+    register: (parent, args, context, info) => {
       checkPermission(context)
-      await context.db.mutation.updateTraining({
+      return context.db.mutation.updateTraining({
         where: { id: args.training },
         data: { registrations: { connect: { id: context.req.userId } } },
       })
-      return 'Success!'
     },
-    deregister: async (parent, args, context, info) => {
+    deregister: (parent, args, context, info) => {
       checkPermission(context)
-      await context.db.mutation.updateTraining({
+      return context.db.mutation.updateTraining({
         where: { id: args.training },
         data: { registrations: { disconnect: { id: context.req.userId } } },
       })
-      return 'Success!'
     },
-    publish: async (parent, args, context, info) => {
+    publish: (parent, args, context, info) => {
       checkPermission(context, ['INSTRUCTOR'])
-      await context.db.mutation.updateTraining({
+      return context.db.mutation.updateTraining({
         where: { id: args.training },
         data: { published: args.status },
       })
-      return 'Success!'
     },
   },
 }

+ 4 - 1
frontend/pages/_app.tsx

@@ -3,12 +3,15 @@ import { ApolloProvider } from '@apollo/client'
 
 import client from '../src/lib/apollo'
 import { UserProvider } from '../src/user/hooks'
+import { Page } from '../src/app'
 
 const MyApp = ({ Component, pageProps }: AppProps) => {
   return (
     <ApolloProvider client={client}>
       <UserProvider>
-        <Component {...pageProps} />
+        <Page>
+          <Component {...pageProps} />
+        </Page>
       </UserProvider>
     </ApolloProvider>
   )

+ 32 - 29
frontend/pages/admin/training/index.tsx

@@ -1,40 +1,43 @@
 import Link from 'next/link'
 import { useTrainingsQuery } from '../../../src/gql'
+import { AdminPage } from '../../../src/app'
 
 const TrainingsList = () => {
   const { data, error, loading } = useTrainingsQuery()
 
   return (
-    <section>
-      {error && <p>Error loading trainings...</p>}
-      {loading && <p>Loading data...</p>}
-      {data && data.trainings.length > 0 ? (
-        <ul>
-          {data?.trainings.map(training => (
-            <li key={training.id}>
-              <button type='button'>Delete</button>
-              <Link href='training/[id]' as={`training/${training.id}`}>
-                <a>{training.title}</a>
-              </Link>
-            </li>
-          ))}
-        </ul>
-      ) : (
-        <p>No trainings found.</p>
-      )}
-      <Link href='training/create'>
-        <a>Create training</a>
-      </Link>
+    <AdminPage>
+      <section>
+        {error && <p>Error loading trainings...</p>}
+        {loading && <p>Loading data...</p>}
+        {data && data.trainings.length > 0 ? (
+          <ul>
+            {data?.trainings.map((training) => (
+              <li key={training.id}>
+                <button type='button'>Delete</button>
+                <Link href='training/[id]' as={`training/${training.id}`}>
+                  <a>{training.title}</a>
+                </Link>
+              </li>
+            ))}
+          </ul>
+        ) : (
+          <p>No trainings found.</p>
+        )}
+        <Link href='training/create'>
+          <a>Create training</a>
+        </Link>
 
-      <style jsx>{`
-        li {
-          list-style: none;
-        }
-        ul {
-          padding: 0;
-        }
-      `}</style>
-    </section>
+        <style jsx>{`
+          li {
+            list-style: none;
+          }
+          ul {
+            padding: 0;
+          }
+        `}</style>
+      </section>
+    </AdminPage>
   )
 }
 

+ 81 - 85
frontend/pages/index.tsx

@@ -1,7 +1,5 @@
-//import initialData from '../initial-data'
 import { useTrainingQuery } from '../src/gql'
 import { TrainingArchive } from '../src/training'
-import { Page } from '../src/app'
 import { useState, useEffect } from 'react'
 import { TTraining } from '../src/training/types'
 import TrainingProgram from '../src/training/components/TrainingProgram'
@@ -20,101 +18,99 @@ const Home = () => {
   }, [data])
 
   return (
-    <Page>
-      <section className='next-training'>
-        {training ? (
-          <>
-            <h1 className='training-title'>{training.title}</h1>
-            <TrainingMeta training={training} />
-            <TrainingProgram training={training} />
-          </>
-        ) : null}
+    <section className='next-training'>
+      {training ? (
+        <>
+          <h1 className='training-title'>{training.title}</h1>
+          <TrainingMeta training={training} />
+          <TrainingProgram training={training} />
+        </>
+      ) : null}
 
-        <TrainingArchive />
+      <TrainingArchive />
 
-        <style jsx>
-          {`
+      <style jsx>
+        {`
+          .training-title {
+            grid-area: title;
+            margin: 0.3em 0;
+            padding: 0;
+            font-size: 3.4rem;
+            font-weight: 300;
+            text-transform: uppercase;
+            color: ${theme.colors.blue};
+            padding: 0;
+            margin: 0.3em 0;
+            position: relative;
+          }
+          .training-title::before {
+            content: 'your next training:';
+            text-transform: none;
+            font-weight: 900;
+            font-size: 1.1rem;
+            display: block;
+            position: absolute;
+            top: -1rem;
+            left: 01rem;
+            color: ${theme.colors.darkerblue};
+          }
+
+          :global(.training-program) {
+            grid-area: program;
+          }
+          :global(.training-archive) {
+            grid-area: archive;
+          }
+          :global(.training-meta) {
+            grid-area: info;
+          }
+          @media (min-width: 600px) {
+            .next-training {
+              display: grid;
+              grid-template-columns: 1fr auto;
+              grid-template-areas:
+                'title    info'
+                'program  program'
+                'archive  archive';
+              column-gap: 1em;
+            }
+          }
+          @media (min-width: 1024px) {
+            .next-training {
+              display: grid;
+              grid-template-columns: 2fr 1fr;
+              grid-template-rows: auto 1fr auto;
+              grid-template-areas:
+                'title    program'
+                'info     program'
+                'archive  program';
+              column-gap: 2em;
+            }
             .training-title {
-              grid-area: title;
-              margin: 0.3em 0;
-              padding: 0;
-              font-size: 3.4rem;
-              font-weight: 300;
-              text-transform: uppercase;
-              color: ${theme.colors.blue};
-              padding: 0;
-              margin: 0.3em 0;
-              position: relative;
+              font-size: 8rem;
+              margin: 0;
             }
             .training-title::before {
-              content: 'your next training:';
-              text-transform: none;
-              font-weight: 900;
-              font-size: 1.1rem;
+              font-size: 1.4rem;
               display: block;
               position: absolute;
-              top: -1rem;
-              left: 01rem;
+              top: -0.4rem;
+              left: 2.4rem;
               color: ${theme.colors.darkerblue};
             }
-
-            :global(.training-program) {
-              grid-area: program;
-            }
-            :global(.training-archive) {
-              grid-area: archive;
-            }
             :global(.training-meta) {
               grid-area: info;
+              height: 350px;
+              background-image: url('/media/man_working_out.jpg');
+              background-size: auto 100%;
+              background-repeat: no-repeat;
+              background-position: right center;
+              clip-path: polygon(0% 0%, 95% 0%, 100% 50%, 95% 100%, 0% 100%, 0% 0%);
             }
-            @media (min-width: 600px) {
-              .next-training {
-                display: grid;
-                grid-template-columns: 1fr auto;
-                grid-template-areas:
-                  'title    info'
-                  'program  program'
-                  'archive  archive';
-                column-gap: 1em;
-              }
-            }
-            @media (min-width: 1024px) {
-              .next-training {
-                display: grid;
-                grid-template-columns: 2fr 1fr;
-                grid-template-rows: auto 1fr auto;
-                grid-template-areas:
-                  'title    program'
-                  'info     program'
-                  'archive  program';
-                column-gap: 2em;
-              }
-              .training-title {
-                font-size: 8rem;
-                margin: 0;
-              }
-              .training-title::before {
-                font-size: 1.4rem;
-                display: block;
-                position: absolute;
-                top: -0.4rem;
-                left: 2.4rem;
-                color: ${theme.colors.darkerblue};
-              }
-              :global(.training-meta) {
-                grid-area: info;
-                height: 350px;
-                background-image: url('/media/man_working_out.jpg');
-                background-size: auto 100%;
-                background-repeat: no-repeat;
-                background-position: right center;
-                clip-path: polygon(0% 0%, 95% 0%, 100% 50%, 95% 100%, 0% 100%, 0% 0%);
-              }
-            }
-          `}
-        </style>
-      </section>
-    </Page>
+          }
+        `}
+      </style>
+    </section>
   )
 }
 

+ 5 - 7
frontend/pages/timer/[id].tsx

@@ -12,13 +12,11 @@ const TimerPage = () => {
     variables: { id: typeof id === 'string' ? id : id && id[0] },
   })
 
-  return (
-    <Page>
-      {loading && <p>Loading data...</p>}
-      {error && <p>Error loading training.</p>}
-      {data?.training && <Timer training={data.training} />}
-    </Page>
-  )
+  if (loading) return <p>Loading data...</p>
+  if (error) return <p>Error loading training.</p>
+
+  if (!data?.training) return <p>Training not found.</p>
+  else return <Timer training={data.training} />
 }
 
 export default TimerPage

+ 24 - 4
frontend/src/app/components/AdminPage.tsx

@@ -1,13 +1,33 @@
-import { FunctionComponent } from 'react'
+import { FunctionComponent, useContext } from 'react'
 import AdminSideBar from './AdminSideBar'
+import { UserContext } from '../../user/hooks'
+import { LoginPage } from '../../user'
+import { Meta, Header, Footer } from '..'
+import GlobalStyle from '../../styles/global'
 
 const AdminPage: FunctionComponent = ({ children }) => {
+  const { user } = useContext(UserContext)
+  console.log(user)
+
+  let content
+  if (!user || user.loading) content = <p>Please wait...</p>
+  else if (user.error) content = <p>Error logging in.</p>
+  else if (user.data?.currentUser) content = children
+  else content = <LoginPage />
+
   return (
-    <section>
+    <>
+      <Meta />
+
+      <Header />
       <AdminSideBar />
+      <main>{content}</main>
+      <Footer />
 
-      <article>{children}</article>
-    </section>
+      <style jsx global>
+        {GlobalStyle}
+      </style>
+    </>
   )
 }
 

+ 8 - 2
frontend/src/app/components/AdminSideBar.tsx

@@ -2,7 +2,7 @@ import Link from 'next/link'
 
 const AdminSideBar = () => {
   return (
-    <aside className='admin-sidebar'>
+    <nav className='admin-sidebar'>
       <ul className='admin-menu'>
         <li className='admin-item'>
           <Link href='training'>
@@ -15,7 +15,13 @@ const AdminSideBar = () => {
           </Link>
         </li>
       </ul>
-    </aside>
+
+      <style jsx>{`
+        nav * {
+          all: unset;
+        }
+      `}</style>
+    </nav>
   )
 }
 

+ 2 - 2
frontend/src/app/components/Header.tsx

@@ -24,11 +24,11 @@ const Header = () => (
           grid-template-columns: 1fr auto minmax(670px, 1fr) 1fr;
         }
 
-        :global(.logo) {
+        header :global(.logo) {
           grid-column: 2;
         }
 
-        :global(nav) {
+        header :global(nav) {
           grid-column: 3;
         }
       }

+ 9 - 18
frontend/src/app/components/Nav.tsx

@@ -22,14 +22,6 @@ const Nav = () => {
               </a>
             </Link>
           </li>
-          <li>
-            <Link href='/timer'>
-              <a>
-                <FontAwesomeIcon icon={faStopwatch20} />
-                Timer
-              </a>
-            </Link>
-          </li>
           <UserNav />
         </ul>
       </nav>
@@ -58,18 +50,18 @@ const Nav = () => {
           list-style: none;
         }
 
-        :global(nav li) {
+        nav :global(li) {
           margin: 0 0.2em;
           padding: 0.6em 1.5em;
           border-bottom: 1px solid #696969e7;
         }
 
-        :global(nav svg) {
+        nav :global(svg) {
           width: 0.8em;
           margin-right: 0.7em;
         }
 
-        :global(nav a) {
+        nav :global(a) {
           color: ${theme.colors.offWhite};
           text-decoration: none;
           font-size: 1.2rem;
@@ -143,21 +135,20 @@ const Nav = () => {
           nav {
             all: unset;
             display: flex;
+            position: relative;
             align-items: center;
             justify-content: flex-end;
           }
 
-          :global(nav li) {
+          nav :global(li) {
             display: inline-block;
             border-bottom: none;
-            position: relative;
             text-align: center;
           }
 
-          :global(nav li::after) {
+          nav :global(li::after) {
             content: '';
             display: block;
-            position: absolute;
             height: 1px;
             left: 10%;
             right: 10%;
@@ -167,16 +158,16 @@ const Nav = () => {
             background-color: ${theme.colors.darkerblue};
           }
 
-          :global(nav li:hover::after) {
+          nav :global(li:hover::after) {
             background-color: ${theme.colors.blue};
             transform: scale(1, 1);
           }
 
-          :global(nav a) {
+          nav :global(a) {
             color: ${theme.colors.darkerblue};
           }
 
-          :global(nav a:hover) {
+          nav :global(a:hover) {
             color: ${theme.colors.blue};
           }
         }

+ 5 - 5
frontend/src/app/components/Tile.tsx

@@ -13,23 +13,23 @@ const Tile: FunctionComponent = ({ children }) => {
           min-width: 100%;
         }
 
-        :global(.tile p),
-        :global(.tile h2) {
+        .tile :global(p),
+        .tile :global(h2) {
           padding: 0;
           margin: 0 0 0.7em;
         }
 
-        :global(.tile h2 .icon) {
+        .tile :global(h2 .icon) {
           position: absolute;
           right: 1em;
         }
 
-        :global(.tile .count) {
+        .tile :global(.count) {
           font-size: 120%;
           font-weight: 900;
         }
 
-        :global(.tile img) {
+        .tile :global(img) {
           max-width: 100%;
         }
       `}</style>

+ 15 - 9
frontend/src/gql/index.tsx

@@ -1536,9 +1536,9 @@ export type Mutation = {
   userSignup: User,
   requestReset: Scalars['String'],
   resetPassword: User,
-  register: Scalars['String'],
-  deregister: Scalars['String'],
-  publish: Scalars['String'],
+  register: Training,
+  deregister: Training,
+  publish: Training,
 };
 
 
@@ -3514,14 +3514,14 @@ export type RegisterMutationVariables = {
 };
 
 
-export type RegisterMutation = Pick<Mutation, 'register'>;
+export type RegisterMutation = { register: Pick<Training, 'id'> };
 
 export type DeregisterMutationVariables = {
   training: Scalars['ID']
 };
 
 
-export type DeregisterMutation = Pick<Mutation, 'deregister'>;
+export type DeregisterMutation = { deregister: Pick<Training, 'id'> };
 
 export type PublishMutationVariables = {
   training: Scalars['ID'],
@@ -3529,7 +3529,7 @@ export type PublishMutationVariables = {
 };
 
 
-export type PublishMutation = Pick<Mutation, 'publish'>;
+export type PublishMutation = { publish: Pick<Training, 'id'> };
 
 export type ExerciseContentFragment = Pick<Exercise, 'id' | 'name' | 'description' | 'videos' | 'pictures' | 'targets' | 'baseExercise'>;
 
@@ -4138,7 +4138,9 @@ export type CreateFormatMutationResult = ApolloReactCommon.MutationResult<Create
 export type CreateFormatMutationOptions = ApolloReactCommon.BaseMutationOptions<CreateFormatMutation, CreateFormatMutationVariables>;
 export const RegisterDocument = gql`
     mutation register($training: ID!) {
-  register(training: $training)
+  register(training: $training) {
+    id
+  }
 }
     `;
 export type RegisterMutationFn = ApolloReactCommon.MutationFunction<RegisterMutation, RegisterMutationVariables>;
@@ -4168,7 +4170,9 @@ export type RegisterMutationResult = ApolloReactCommon.MutationResult<RegisterMu
 export type RegisterMutationOptions = ApolloReactCommon.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
 export const DeregisterDocument = gql`
     mutation deregister($training: ID!) {
-  deregister(training: $training)
+  deregister(training: $training) {
+    id
+  }
 }
     `;
 export type DeregisterMutationFn = ApolloReactCommon.MutationFunction<DeregisterMutation, DeregisterMutationVariables>;
@@ -4198,7 +4202,9 @@ export type DeregisterMutationResult = ApolloReactCommon.MutationResult<Deregist
 export type DeregisterMutationOptions = ApolloReactCommon.BaseMutationOptions<DeregisterMutation, DeregisterMutationVariables>;
 export const PublishDocument = gql`
     mutation publish($training: ID!, $status: Boolean) {
-  publish(training: $training, status: $status)
+  publish(training: $training, status: $status) {
+    id
+  }
 }
     `;
 export type PublishMutationFn = ApolloReactCommon.MutationFunction<PublishMutation, PublishMutationVariables>;

+ 31 - 10
frontend/src/styles/global.ts

@@ -18,8 +18,8 @@ const GlobalStyle = css.global`
   }
 
   *,
-  *:before,
-  *:after {
+  *::before,
+  *::after {
     box-sizing: inherit;
   }
 
@@ -38,6 +38,17 @@ const GlobalStyle = css.global`
     box-shadow: ${theme.bs};
   }
 
+  #__next.admin {
+    grid-template-areas:
+      'header header'
+      'sidebar main'
+      'footer footer';
+  }
+
+  #__next.admin :global(nav.admin-sidebar) {
+    grid-area: sidebar;
+  }
+
   header {
     grid-area: header;
   }
@@ -68,35 +79,45 @@ const GlobalStyle = css.global`
 
   @media (min-width: 480px) {
     main {
-      padding: 2em 3em;
+      padding: 2em 1em;
     }
   }
 
-  form * {
-    font-size: 120%;
-    padding: 0.3em 0.8em;
-    margin: 0.8em auto;
+  @media (min-width: 1024px) {
+    main {
+      padding: 2em 3em;
+    }
   }
 
   form label {
     display: none;
   }
-  form input {
+
+  form input,
+  form select,
+  form textbox {
     display: block;
     width: 100%;
+    margin: 0.8em auto;
+    padding: 0.3em;
     border: 2px solid ${theme.colors.darkerblue}00;
     color: ${theme.colors.darkerblue};
     background-color: ${theme.colors.darkerblue}27;
     transition: all 250ms ease-in-out;
   }
-  form input:focus {
+
+  form input:focus,
+  form select:focus,
+  form textbox:focus {
     color: ${theme.colors.blue};
     background-color: ${theme.colors.blue}15;
     border-bottom: 2px solid ${theme.colors.blue}44;
   }
 
-  form button {
+  button {
     display: block;
+    margin: 0.8em auto;
+    padding: 0.5em 1em;
     border: none;
     background-color: ${theme.colors.darkerblue};
     color: ${theme.colors.offWhite};

+ 2 - 2
frontend/src/timer/components/Countdown.tsx

@@ -29,7 +29,7 @@ const Countdown = ({ seconds, totalPercent, exercisePercent, onClick }: ICountdo
   }
 
   return (
-    <div id='timer' onClick={onClick}>
+    <div className='timer' onClick={onClick}>
       <svg>
         <text textAnchor='middle' alignmentBaseline='central' y='185' x='150' fill={color}>
           {formatTime(intSeconds)}
@@ -54,7 +54,7 @@ const Countdown = ({ seconds, totalPercent, exercisePercent, onClick }: ICountdo
 
       <style jsx>
         {`
-          :global(#timer svg) {
+          .timer :global(svg) {
             width: 300px;
             height: 300px;
             font-size: 100px;

+ 61 - 87
frontend/src/training/components/EditTraining.tsx

@@ -1,29 +1,20 @@
 import { useCreateTrainingMutation, useUpdateTrainingMutation } from '../../gql'
 import { useForm, TextInput, DateTimeInput, Checkbox } from '../../form'
-import {
-  emptyTraining,
-  emptyBlockInstance,
-  transformArrayToDB,
-  diffDB,
-  prepareDataForDB
-} from '../utils'
+import { emptyTraining, emptyBlockInstance, prepareDataForDB } from '../utils'
 import TrainingTypeSelector from './TrainingTypeSelector'
 import BlockInstanceInputs from './BlockInstanceInputs'
 import { TTraining } from '../types'
-import { transform, isEqual, isObject } from 'lodash'
 import Registrations from './Registrations'
 import Ratings from './Ratings'
 
 const EditTraining = ({ training }: { training?: TTraining }) => {
-  const { values, touched, onChange, loadData } = useForm(
-    training || emptyTraining()
-  )
+  const { values, touched, onChange, loadData } = useForm(training || emptyTraining())
   const [createTraining, createData] = useCreateTrainingMutation()
   const [updateTraining, updateDate] = useUpdateTrainingMutation()
 
   return (
     <form
-      onSubmit={async event => {
+      onSubmit={async (event) => {
         event.preventDefault()
         const newValues = prepareDataForDB(values, training || emptyTraining())
         if (!newValues || Object.keys(newValues).length === 0) {
@@ -37,88 +28,71 @@ const EditTraining = ({ training }: { training?: TTraining }) => {
           console.log('created training', createData)
         } else if (id.startsWith('@@')) {
           const updateData = await updateTraining({
-            variables: { where: { id: id.substr(2) }, data }
+            variables: { where: { id: id.substr(2) }, data },
           })
           console.log('updated training', updateData)
         }
       }}
     >
-      <p>
-        {values.createdAt} {values.id}
-      </p>
-      <TextInput
-        name='title'
-        label='Title'
-        value={values.title}
-        onChange={onChange}
-      />
-      <TrainingTypeSelector
-        name='type'
-        value={values.type}
-        onChange={onChange}
-      />
-      <DateTimeInput
-        name='trainingDate'
-        label='Training date'
-        value={values.trainingDate}
-        onChange={onChange}
-      />
-      <TextInput
-        name='location'
-        label='Location'
-        value={values.location}
-        onChange={onChange}
-      />
-      <Registrations registrations={values.registrations} />
-      <TextInput
-        name='attendance'
-        label='Attendance'
-        type='number'
-        value={values.attendance}
-        onChange={onChange}
-      />
-      <Ratings ratings={values.ratings} />
-      <Checkbox
-        name='published'
-        label='Published'
-        value={values.published}
-        onChange={onChange}
-      />
-      <label>Blocks</label>
-      {values.blocks && (
-        <BlockInstanceInputs
-          name='blocks'
-          value={values.blocks}
+      <fieldset className='fields-training'>
+        <p>
+          {values.createdAt} {values.id}
+        </p>
+        <TextInput name='title' label='Title' value={values.title} onChange={onChange} />
+        <TrainingTypeSelector name='type' value={values.type} onChange={onChange} />
+        <DateTimeInput
+          name='trainingDate'
+          label='Training date'
+          value={values.trainingDate}
           onChange={onChange}
         />
-      )}
-      <button
-        onClick={event => {
-          event.preventDefault()
-          const newBlock = emptyBlockInstance({
-            order: values.blocks
-              ? values.blocks.filter(block => !block.id.startsWith('--')).length
-              : 0
-          })
-          onChange({
-            target: {
-              type: 'custom',
-              name: 'blocks',
-              value: values.blocks ? [...values.blocks, newBlock] : [newBlock]
-            }
-          })
-        }}
-        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>
-      )}
+        <TextInput name='location' label='Location' value={values.location} onChange={onChange} />
+        <Registrations registrations={values.registrations} />
+        <TextInput
+          name='attendance'
+          label='Attendance'
+          type='number'
+          value={values.attendance}
+          onChange={onChange}
+        />
+        <Ratings ratings={values.ratings} />
+        <Checkbox name='published' label='Published' value={values.published} onChange={onChange} />
+        <label>Blocks</label>
+        {values.blocks && (
+          <BlockInstanceInputs name='blocks' value={values.blocks} onChange={onChange} />
+        )}
+        <button
+          onClick={(event) => {
+            event.preventDefault()
+            const newBlock = emptyBlockInstance({
+              order: values.blocks
+                ? values.blocks.filter((block) => !block.id.startsWith('--')).length
+                : 0,
+            })
+            onChange({
+              target: {
+                type: 'custom',
+                name: 'blocks',
+                value: values.blocks ? [...values.blocks, newBlock] : [newBlock],
+              },
+            })
+          }}
+          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>}
+      </fieldset>
+
+      <style jsx>{`
+        .fields-training {
+          display: grid;
+        }
+      `}</style>
     </form>
   )
 }

+ 119 - 19
frontend/src/training/components/RegistrationButton.tsx

@@ -1,41 +1,141 @@
 import { useRegisterMutation, useDeregisterMutation, User, TrainingDocument } from '../../gql'
 import { FunctionComponent, useContext } from 'react'
 import { UserContext } from '../../user/hooks'
+import { TTraining } from '../types'
+import theme from '../../styles/theme'
 
-const RegistrationButton: FunctionComponent<{ registrations?: User[] }> = ({ registrations }) => {
+type TRegisterButton = FunctionComponent<{ training: TTraining }>
+
+const RegistrationButton: TRegisterButton = ({ training: { id, registrations } }) => {
   const [register, registerData] = useRegisterMutation({
+    variables: { training: id },
     refetchQueries: [{ query: TrainingDocument }],
-    update: (cache, data) => {
-      console.log('salie', cache, data)
-    },
   })
   const [deregister, deregisterData] = useDeregisterMutation({
+    variables: { training: id },
     refetchQueries: [{ query: TrainingDocument }],
-    update: (cache, data) => {
-      console.log('salie', cache, data)
-    },
   })
   const { user } = useContext(UserContext)
 
   const isRegistered =
-    registrations &&
-    user?.data?.currentUser.id &&
-    registrations.find((registeredUser) => registeredUser.id === user.data?.currentUser.id)
-  const action = isRegistered ? () => deregister() : () => register()
-  const label = isRegistered ? 'cancel' : 'register'
+    !!registrations &&
+    !!user?.data?.currentUser.id &&
+    !!registrations.find((registeredUser) => registeredUser.id === user.data?.currentUser.id)
+  const onChange = isRegistered ? () => deregister() : () => register()
+  const label = isRegistered ? 'Cancel 😱' : 'Register now 😀'
+  const className = isRegistered ? 'registered' : 'not-registered'
 
   return (
-    <>
-      <button
-        type='button'
-        onClick={action}
+    <span className='register-container'>
+      <input
+        id='register'
+        name='register'
+        className='register'
+        type='checkbox'
+        onChange={onChange}
+        checked={isRegistered}
         disabled={registerData.loading || deregisterData.loading}
-      >
+      />
+
+      <label htmlFor='register' className='register-label'>
         {label}
-      </button>
+        <span />
+      </label>
       {registerData.error && registerData.error.message}
       {deregisterData.error && deregisterData.error.message}
-    </>
+      <style jsx>{`
+        .register-container {
+          position: relative;
+          display: inline-block;
+        }
+
+        .register {
+          display: none;
+        }
+
+        .register-label {
+          display: inline;
+          position: relative;
+          align-items: center;
+          margin-left: 1em;
+          height: 100%;
+          cursor: pointer;
+          color: ${theme.colors.darkblue};
+        }
+
+        .register-label span {
+          position: absolute;
+          display: grid;
+          padding: 0;
+          margin: 0;
+          align-items: center;
+          transition: all 400ms ease-in-out;
+
+          height: 2em;
+          width: 2em;
+          background-color: #5d6a6b00;
+          border: 2px solid #60000055;
+          border-radius: 0px;
+        }
+        .register-label span::before,
+        .register-label span::after {
+          content: '';
+          height: 4px;
+          background-color: #600000;
+          border-radius: 2px;
+          position: absolute;
+          left: 0.2em;
+          right: 0.2em;
+          transform: rotate(0deg);
+          transition: all 200ms ease-in-out 200ms;
+        }
+        .register-label span::before {
+          transform: translate(0) rotate(45deg);
+        }
+        .register-label span::after {
+          transform: translate(0) rotate(-45deg);
+        }
+
+        .register:checked ~ .register-label span {
+          height: 2em;
+          width: 2em;
+          border: 2px solid #00600055;
+          border-radius: 0px;
+        }
+
+        .register:checked ~ .register-label span::before {
+          left: 0.45em;
+          right: 0.45em;
+          background-color: #006000;
+          transform: translate(-35%, 80%) rotate(50deg);
+        }
+
+        .register:checked ~ .register-label span::after {
+          left: 0.25em;
+          right: 0.25em;
+          background-color: #006000;
+          transform: translate(16%, 0%) rotate(-55deg);
+        }
+
+        .register:disabled ~ .register-label span {
+          border: 2px solid #00000022;
+        }
+
+        .register:disabled ~ .register-label span::before {
+          left: 0.2em;
+          right: 0.2em;
+          background-color: #00000022;
+          transform: translate(0%, -100%) rotate(0deg);
+        }
+
+        .register:disabled ~ .register-label span::after {
+          left: 0.2em;
+          right: 0.2em;
+          background-color: #00000022;
+          transform: translate(0%, 100%) rotate(0deg);
+        }
+      `}</style>
+    </span>
   )
 }
 

+ 2 - 9
frontend/src/training/components/TrainingMeta.tsx

@@ -3,6 +3,7 @@ import { calculateRating, formatTime, calculateDuration } from '../utils'
 import { useContext } from 'react'
 import { UserContext } from '../../user/hooks'
 import { useRegisterMutation, useDeregisterMutation } from '../../gql'
+import RegistrationButton from './RegistrationButton'
 
 const TrainingMeta = ({ training }: { training: TTraining }) => {
   const { user } = useContext(UserContext)
@@ -36,15 +37,7 @@ const TrainingMeta = ({ training }: { training: TTraining }) => {
         <span className='caption'>Registrations</span>
         <span className='data'>
           {training.registrations?.length ?? 0}
-          {training.registrations &&
-          training.registrations.find(
-            (registeredUser) =>
-              user?.data?.currentUser && registeredUser.id === user.data.currentUser.id
-          ) ? (
-            <button onClick={() => deregister()}>Deregister</button>
-          ) : (
-            <button onClick={() => register()}>Register now!</button>
-          )}
+          <RegistrationButton training={training} />
         </span>
       </div>
       {/*<div className="info">

+ 9 - 3
frontend/src/training/training.graphql

@@ -144,15 +144,21 @@ mutation createFormat($name: String!, $description: String!) {
 }
 
 mutation register($training: ID!) {
-  register(training: $training)
+  register(training: $training) {
+    id
+  }
 }
 
 mutation deregister($training: ID!) {
-  deregister(training: $training)
+  deregister(training: $training) {
+    id
+  }
 }
 
 mutation publish($training: ID!, $status: Boolean) {
-  publish(training: $training, status: $status)
+  publish(training: $training, status: $status) {
+    id
+  }
 }
 
 fragment exerciseContent on Exercise {

+ 52 - 26
frontend/src/user/components/UserNav.tsx

@@ -5,39 +5,65 @@ import LoginForm from './LoginForm'
 import { UserContext } from '../hooks'
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
 import { faUser } from '@fortawesome/free-solid-svg-icons'
+import { Permission } from '../../gql'
+import theme from '../../styles/theme'
 
 const UserNav = () => {
   const [menu, setMenu] = useState(false)
   const { user } = useContext(UserContext)
 
   return (
-    <li>
-      <a
-        href=''
-        onClick={(ev) => {
-          ev.preventDefault()
-          setMenu(!menu)
-        }}
-      >
-        <FontAwesomeIcon icon={faUser} />
-        {user?.data ? user.data.currentUser.name : 'Login'}
-      </a>
+    <>
+      {user?.data?.currentUser.permissions.includes(Permission.Admin) && (
+        <li>
+          <Link href='admin'>
+            <a>Admin</a>
+          </Link>
+        </li>
+      )}
+      <li className='menupoint'>
+        <a
+          href=''
+          onClick={(ev) => {
+            ev.preventDefault()
+            setMenu(!menu)
+          }}
+        >
+          <FontAwesomeIcon icon={faUser} />
+          {user?.data ? user.data.currentUser.name : 'Login'}
+        </a>
 
-      {menu ? (
-        <section className='usermenu'>
-          {user?.data && (
-            <>
-              <h2>Welcome, {user.data.currentUser.name}</h2>
-              <Link href={{ pathname: 'user' }}>
-                <a>Edit user data</a>
-              </Link>
-              <LogoutButton />
-            </>
-          )}
-          {user?.error && <LoginForm />}
-        </section>
-      ) : null}
-    </li>
+        {menu ? (
+          <section className='usermenu'>
+            {user?.data && (
+              <>
+                <h2>Welcome, {user.data.currentUser.name}</h2>
+                <Link href={{ pathname: 'user' }}>
+                  <a>Edit user data</a>
+                </Link>
+                <LogoutButton />
+              </>
+            )}
+            {user?.error && <LoginForm />}
+          </section>
+        ) : null}
+      </li>
+
+      <style jsx>{`
+        .usermenu {
+          position: absolute;
+          right: 0;
+          top: 100%;
+
+          color: ${theme.colors.offWhite};
+          background-color: ${theme.colors.nav};
+          align-content: end;
+        }
+        .usermenu :global(a) {
+          color: ${theme.colors.offWhite};
+        }
+      `}</style>
+    </>
   )
 }