Ver código fonte

refactored a lot.

Tomi Cvetic 5 anos atrás
pai
commit
8d6413d4e6
61 arquivos alterados com 511 adições e 659 exclusões
  1. 3 0
      .gitignore
  2. 3 3
      backend/index.js
  3. 1 1
      backend/schema.graphql
  4. 2 139
      backend/src/resolvers.js
  5. 0 0
      backend/src/user/authenticate.js
  6. 7 0
      backend/src/user/index.js
  7. 154 0
      backend/src/user/resolvers.js
  8. 0 100
      backend/src/user/resolvers.ts
  9. 2 2
      codegen.yml
  10. 0 3
      frontend/.vscode/settings.json
  11. 0 14
      frontend/components/form/TextInput.tsx
  12. 0 27
      frontend/components/form/__tests__/forms.test.tsx
  13. 0 68
      frontend/components/user/UserAdmin.tsx
  14. 0 44
      frontend/lib/forms.js.bak
  15. 0 155
      frontend/lib/graphql.js
  16. 43 0
      frontend/package-lock.json
  17. 1 0
      frontend/package.json
  18. 4 3
      frontend/pages/_app.js
  19. 3 5
      frontend/pages/admin/users.js
  20. 2 2
      frontend/pages/login.js
  21. 2 2
      frontend/src/app/components/Footer.tsx
  22. 2 2
      frontend/src/app/components/Header.tsx
  23. 0 0
      frontend/src/app/components/Logo.tsx
  24. 0 0
      frontend/src/app/components/Meta.tsx
  25. 2 3
      frontend/src/app/components/Nav.tsx
  26. 6 6
      frontend/src/app/components/Page.tsx
  27. 42 0
      frontend/src/form/__tests__/useFormHandler.test.tsx
  28. 15 0
      frontend/src/form/components/Checkbox.tsx
  29. 17 0
      frontend/src/form/components/Select.tsx
  30. 18 0
      frontend/src/form/components/TextInput.tsx
  31. 9 0
      frontend/src/form/index.ts
  32. 14 0
      frontend/src/form/types.ts
  33. 12 30
      frontend/src/form/useFormHandler.ts
  34. 0 0
      frontend/src/gql/index.tsx
  35. 1 1
      frontend/src/lib/__tests__/nestedValues.test.ts
  36. 0 0
      frontend/src/lib/__tests__/regex.test.ts
  37. 0 0
      frontend/src/lib/apollo.js
  38. 0 0
      frontend/src/lib/localState.js
  39. 1 1
      frontend/src/lib/nestedValues.ts
  40. 0 0
      frontend/src/lib/regex.js
  41. 0 0
      frontend/src/lib/store.tsx
  42. 0 0
      frontend/src/styles/global.js
  43. 0 0
      frontend/src/styles/theme.js
  44. 0 0
      frontend/src/user/__tests__/signup.js.bak
  45. 1 0
      frontend/src/user/components/DeleteUserButton.tsx
  46. 3 6
      frontend/src/user/components/LoginForm.tsx
  47. 4 8
      frontend/src/user/components/LogoutButton.tsx
  48. 4 8
      frontend/src/user/components/RequestPassword.tsx
  49. 4 8
      frontend/src/user/components/ResetPassword.tsx
  50. 5 8
      frontend/src/user/components/SignupForm.tsx
  51. 100 0
      frontend/src/user/components/UserAdmin.tsx
  52. 1 1
      frontend/src/user/components/UserDetails.tsx
  53. 4 8
      frontend/src/user/components/UserEditForm.tsx
  54. 0 0
      frontend/src/user/graphql.ts
  55. 17 0
      frontend/src/user/index.ts
  56. 0 0
      frontend/src/user/props.ts
  57. 0 0
      frontend/src/user/signup.js.bak
  58. 0 0
      frontend/src/user/user.graphql
  59. 0 0
      frontend/src/user/user.js
  60. 0 0
      frontend/src/user/validation.ts
  61. 2 1
      frontend/tsconfig.json

+ 3 - 0
.gitignore

@@ -12,6 +12,9 @@ node_modules
 .next/
 out/
 
+# graphql-codegen
+gql/index.tsx
+
 # production
 build
 

+ 3 - 3
backend/index.js

@@ -11,11 +11,11 @@ const cookieParser = require('cookie-parser')
 const bodyParser = require('body-parser')
 const { merge } = require('lodash')
 const { db, populateUser } = require('./src/db')
-const { authenticate } = require('./src/authenticate')
+const user = require('./src/user')
 
 const prismaResolvers = require('./src/resolvers')
 
-const resolvers = merge(prismaResolvers.resolvers)
+const resolvers = merge(prismaResolvers.resolvers, user.resolvers)
 const typeDefs = ['./schema.graphql']
 
 const server = new GraphQLServer({
@@ -31,7 +31,7 @@ const server = new GraphQLServer({
 server.express.use(cookieParser())
 server.express.use(bodyParser.json())
 // server.express.use(quickMiddleware)
-server.express.use(authenticate)
+server.express.use(user.authenticate)
 server.express.use(populateUser)
 // server.express.use("/static", express.static("static"));
 

+ 1 - 1
backend/schema.graphql

@@ -6,7 +6,7 @@ type Query {
   trainings(where: TrainingWhereInput, orderBy: TrainingOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Training]!
   trainingTypes(where: TrainingTypeWhereInput, orderBy: TrainingTypeOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [TrainingType]!
   blocks(where: BlockWhereInput, orderBy: BlockOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Block]!
-  me: User!
+  currentUser: User!
 }
 
 type Mutation {

+ 2 - 139
backend/src/resolvers.js

@@ -1,19 +1,9 @@
-// const { forwardTo } = require('prisma-binding')
-const bcrypt = require('bcryptjs')
-const randombytes = require('randombytes')
-const { promisify } = require('util')
-const jwt = require('jsonwebtoken')
-const { transport, emailTemplate } = require('./mail')
+// const { transport, emailTemplate } = require('./mail')
 
 const LoginError = new Error('You must be logged in.')
-const PermissionError = new Error('Insufficient permissions.')
+// const PermissionError = new Error('Insufficient permissions.')
 
 const Query = {
-  users: async (parent, args, context, info) => {
-    if (!context.request.userId) throw LoginError
-    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
-    return context.db.query.users(args, info)
-  },
   training: async (parent, args, context, info) => {
     if (!context.request.userId) throw LoginError
     return context.db.query.training({ data: args }, info)
@@ -29,137 +19,10 @@ const Query = {
   blocks: async (parent, args, context, info) => {
     if (!context.request.userId) throw LoginError
     return context.db.query.trainingTypes()
-  },
-  me: (parent, args, context, info) => {
-    if (!context.request.userId) throw LoginError
-    return context.db.query.user({ where: { id: context.request.userId } }, info)
   }
 }
 
 const Mutation = {
-  createUser: async (parent, { data }, context, info) => {
-    if (!context.request.userId) throw LoginError
-    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
-
-    const email = data.email.toLowerCase()
-    const password = await bcrypt.hash(data.password, 10)
-    return context.db.mutation.createUser({
-      data: {
-        ...data,
-        email,
-        password
-      }
-    }, info)
-  },
-  updateUser: async (parent, { data, where }, context, info) => {
-    if (!context.request.userId) throw LoginError
-    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
-
-    const updateData = { ...data }
-    if (data.email) updateData.email = data.email.toLowerCase()
-    if (data.password) updateData.password = await bcrypt.hash(data.password, 10)
-    return context.db.mutation.updateUser({
-      data: updateData,
-      where
-    }, info)
-  },
-  deleteUser: (parent, { where }, context, info) => {
-    if (!context.request.userId) throw LoginError
-    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
-
-    return context.db.mutation.deleteUser({ where })
-  },
-  signup: async (parent, args, ctx, info) => {
-    const email = args.email.toLowerCase()
-    const password = await bcrypt.hash(args.password, 10)
-    const user = await ctx.db.mutation.createUser(
-      {
-        data: {
-          ...args,
-          email,
-          password
-        }
-      },
-      info
-    )
-    const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET)
-    ctx.response.cookie('token', token, {
-      httpOnly: true,
-      maxAge: 24 * 60 * 60 * 1000
-    })
-    return user
-  },
-  login: async (parent, args, context, info) => {
-    const { email, password } = args
-    const user = await context.db.query.user({ where: { email } })
-    if (!user) throw new Error('User 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.response.cookie(
-      'token',
-      token,
-      {
-        httpOnly: true,
-        maxAge: 7 * 24 * 3600 * 1000
-      },
-      info
-    )
-    return user
-  },
-  logout: async (parent, args, context, info) => {
-    context.response.clearCookie('token')
-    return 'Logged out.'
-  },
-  requestReset: async (parent, { email }, context, info) => {
-    const user = await context.db.query.user({ where: { email } })
-    if (!user) {
-      return 'Success.'
-    }
-    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, args, context, info) => {
-    const [user] = await context.db.query.users({
-      where: {
-        resetToken: args.token,
-        resetTokenExpiry_gte: Date.now() - 3600000
-      }
-    })
-    if (!user) {
-      throw Error('Token invalid or expired.')
-    }
-    const password = await bcrypt.hash(args.password, 10)
-    const updatedUser = await context.db.mutation.updateUser({
-      where: { email: user.email },
-      data: {
-        password,
-        resetToken: null,
-        resetTokenExpiry: null
-      }
-    })
-    const token = jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET)
-    context.response.cookie('token', token, {
-      httpOnly: true,
-      maxAge: 1000 * 60 * 60 * 24 * 365
-    })
-    return updatedUser
-  },
   createTraining: async (parent, args, context, info) => {
     if (!context.request.userId) throw LoginError
     const training = await context.db.mutation.createTraining(

+ 0 - 0
backend/src/authenticate.js → backend/src/user/authenticate.js


+ 7 - 0
backend/src/user/index.js

@@ -0,0 +1,7 @@
+const { authenticate } = require('./authenticate')
+const { resolvers } = require('./resolvers')
+
+module.exports = {
+  authenticate,
+  resolvers
+}

+ 154 - 0
backend/src/user/resolvers.js

@@ -0,0 +1,154 @@
+const bcrypt = require('bcryptjs')
+const jwt = require('jsonwebtoken')
+const { promisify } = require('util')
+const randombytes = require('randombytes')
+
+const LoginError = new Error('Login required.')
+const PermissionError = new Error('No permission.')
+
+const Query = {
+  users: async (parent, args, context, info) => {
+    if (!context.request.userId) throw LoginError
+    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
+    return context.db.query.users(args, info)
+  },
+  currentUser: (parent, args, context, info) => {
+    if (!context.request.userId) throw LoginError
+    return context.db.query.user({
+      where: { id: context.request.userId }
+    }, info)
+  }
+}
+
+const Mutation = {
+  createUser: async (parent, { data }, context, info) => {
+    if (!context.request.userId) throw LoginError
+    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
+
+    const email = data.email.toLowerCase()
+    const password = await bcrypt.hash(data.password, 10)
+    return context.db.mutation.createUser({
+      data: {
+        ...data,
+        email,
+        password
+      }
+    }, info)
+  },
+  updateUser: async (parent, { data, where }, context, info) => {
+    if (!context.request.userId) throw LoginError
+    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
+
+    const updateData = { ...data }
+    if (data.email) updateData.email = data.email.toLowerCase()
+    if (data.password) updateData.password = await bcrypt.hash(data.password, 10)
+    return context.db.mutation.updateUser({
+      data: updateData,
+      where
+    }, info)
+  },
+  deleteUser: (parent, { where }, context, info) => {
+    if (!context.request.userId) throw LoginError
+    if (!context.request.user.permissions.find(permission => permission === 'ADMIN')) throw PermissionError
+
+    return context.db.mutation.deleteUser({ where })
+  },
+  signup: async (parent, args, ctx, info) => {
+    const email = args.email.toLowerCase()
+    const password = await bcrypt.hash(args.password, 10)
+    const user = await ctx.db.mutation.createUser(
+      {
+        data: {
+          ...args,
+          email,
+          password
+        }
+      },
+      info
+    )
+    const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET)
+    ctx.response.cookie('token', token, {
+      httpOnly: true,
+      maxAge: 24 * 60 * 60 * 1000
+    })
+    return user
+  },
+  login: async (parent, args, context, info) => {
+    const { email, password } = args
+    const user = await context.db.query.user({ where: { email } })
+    if (!user) throw new Error('User 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.response.cookie(
+      'token',
+      token,
+      {
+        httpOnly: true,
+        maxAge: 7 * 24 * 3600 * 1000
+      },
+      info
+    )
+    return user
+  },
+  logout: async (parent, args, context, info) => {
+    context.response.clearCookie('token')
+    return 'Logged out.'
+  },
+  requestReset: async (parent, { email }, context, info) => {
+    const user = await context.db.query.user({ where: { email } })
+    if (!user) {
+      return 'Success.'
+    }
+    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, args, context, info) => {
+    const [user] = await context.db.query.users({
+      where: {
+        resetToken: args.token,
+        resetTokenExpiry_gte: Date.now() - 3600000
+      }
+    })
+    if (!user) {
+      throw Error('Token invalid or expired.')
+    }
+    const password = await bcrypt.hash(args.password, 10)
+    const updatedUser = await context.db.mutation.updateUser({
+      where: { email: user.email },
+      data: {
+        password,
+        resetToken: null,
+        resetTokenExpiry: null
+      }
+    })
+    const token = jwt.sign({ userId: updatedUser.id }, process.env.APP_SECRET)
+    context.response.cookie('token', token, {
+      httpOnly: true,
+      maxAge: 1000 * 60 * 60 * 24 * 365
+    })
+    return updatedUser
+  }
+}
+
+const resolvers = {
+  Query,
+  Mutation
+}
+
+module.exports = { resolvers }

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

@@ -1,100 +0,0 @@
-const { forwardTo } = require('prisma-binding')
-const bcrypt = require('bcryptjs')
-const jwt = require('jsonwebtoken')
-
-const LoginError = new Error('Login required.')
-const PermissionError = new Error('No permission.')
-
-const Query = {
-  currentUser: (parent, args, context, info) => {
-    if (!context.request.userId) throw LoginError
-    return context.db.query.user({
-      where: { id: context.request.userId }
-    }, info)
-  }
-}
-
-const Mutation = {
-  createUser: async (parent, args, context, info) => {
-    if (!context.request.userId) throw LoginError
-    const user = await context.db.query.user({
-      where: { id: context.request.userId }
-    }, info)
-    if (!user.)
-      const email = args.email.toLowerCase()
-    const password = await bcrypt.hash(args.password, 10)
-    return context.db.mutation.createUser(
-      {
-        data: {
-          ...args,
-          email,
-          password
-        }
-      },
-      info
-    )
-  },
-
-  signup: async (parent, args, ctx, info) => {
-    const email = args.email.toLowerCase()
-    const password = await bcrypt.hash(args.password, 10)
-    const user = await ctx.db.mutation.createUser(
-      {
-        data: {
-          ...args,
-          email,
-          password
-        }
-      },
-      info
-    )
-    const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET)
-    ctx.response.cookie('token', token, {
-      httpOnly: true,
-      maxAge: 24 * 60 * 60 * 1000
-    })
-    return user
-  },
-
-  login: async (parent, args, context, info) => {
-    const { email, password } = args
-    const user = await context.db.query.user({ where: { email } })
-    if (!user) throw new Error('User 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.response.cookie(
-      'token',
-      token,
-      {
-        httpOnly: true,
-        maxAge: 7 * 24 * 3600 * 1000
-      },
-      info
-    )
-    return user
-  },
-  logout: async (parent, args, context, info) => {
-    context.response.clearCookie('token')
-    return 'Logged out.'
-  },
-  requestPassword: async (parent, args, context, info) => {
-
-  },
-  resetPassword: async (parent, args, context, info) => {
-
-  },
-  updateUser: async (parent, args, context, info) => {
-
-  },
-  deleteUser: async (parent, args, context, info) => {
-
-  }
-}
-
-const resolvers = {
-  Query,
-  Mutation
-}
-
-module.exports = { resolvers }

+ 2 - 2
codegen.yml

@@ -1,8 +1,8 @@
 overwrite: true
 schema: 'backend/schema.graphql'
-documents: 'frontend/components/**/*.graphql'
+documents: 'frontend/src/**/*.graphql'
 generates:
-  frontend/graphql/index.tsx:
+  frontend/src/gql/index.tsx:
     config:
       withHooks: true
       withComponent: false

+ 0 - 3
frontend/.vscode/settings.json

@@ -1,3 +0,0 @@
-{
-    "editor.tabSize": 2
-}

+ 0 - 14
frontend/components/form/TextInput.tsx

@@ -1,14 +0,0 @@
-import { InputProps } from './forms'
-
-interface TextInputProps extends InputProps {
-  label: string
-}
-
-const TextInput = ({ label, ...inputProps }: TextInputProps) => (
-  <>
-    <label htmlFor={inputProps.name}>{label}</label>
-    <input {...inputProps} />
-  </>
-)
-
-export default TextInput

+ 0 - 27
frontend/components/form/__tests__/forms.test.tsx

@@ -1,27 +0,0 @@
-import { renderHook } from '@testing-library/react-hooks'
-
-import { useFormHandler } from '../forms'
-
-describe('form hook return values', () => {
-
-  const Component = () => useFormHandler({ var: 'val' }, values => { return {} })
-  const { result } = renderHook(Component)
-
-  it('returns correct initial states.', () => {
-    expect(result.current.values.var).toBe('val')
-    expect(result.current.errors).toEqual({})
-    expect(result.current.isSubmitting).toBe(false)
-  })
-
-  it('returns input element properties for valid input names.', () => {
-    expect(result.current.inputProps('var')).toMatchObject({
-      name: 'var', id: 'var', onBlur: expect.anything(), onChange: expect.anything()
-    })
-  })
-
-  it('throws error for invalid input names.', () => {
-    expect(() => result.current.inputProps('doh!')).toThrow()
-  })
-})
-
-export default true

+ 0 - 68
frontend/components/user/UserAdmin.tsx

@@ -1,68 +0,0 @@
-import { useUsersQuery, UsersQuery, Permission } from '../../graphql'
-import { useFormHandler, InputPropsOptions } from '../form/forms'
-import { useEffect } from 'react'
-
-interface UserInputProps {
-  inputProps: (name: string, options?: InputPropsOptions) => any
-  subtree: string
-}
-
-const UserInput = ({ inputProps, subtree }: UserInputProps) => {
-  console.log('user input', inputProps('name', { subtree }))
-  return (
-    <tr>
-      <td><input type='checkbox' /></td>
-      <td><input {...inputProps('name', { subtree })} /></td>
-      <td><input {...inputProps('email', { subtree, label: 'e-mail' })} /></td>
-      <td><input type='checkbox' {...inputProps('admin')} /></td>
-      <td><input type='checkbox' {...inputProps('instructor')} /></td>
-      <td><input {...inputProps('interests', { subtree })} /></td>
-      <td><button>X</button></td>
-    </tr>
-
-  )
-}
-
-const initialData: UsersQuery = {
-  users: []
-}
-
-const UserAdmin = () => {
-  const { data, error, loading } = useUsersQuery()
-  const { inputProps, values, setValues } = useFormHandler(initialData)
-
-  useEffect(() => {
-    if (data) console.log('data', { ...data })
-    if (data && data.users) setValues({ ...data })
-  }, [data])
-
-  if (error) return <p>Error: {error.message}</p>
-  if (loading) return <p>Loading...</p>
-
-  return (
-    <form>
-      <table>
-        <thead>
-          <tr>
-            <th></th>
-            <th>Name</th>
-            <th>Email</th>
-            <th>Admin</th>
-            <th>Instructor</th>
-            <th>Interests</th>
-            <th>Actions</th>
-          </tr>
-        </thead>
-        <tbody>
-          {
-            values.users.map((user, index) => (
-              user && <UserInput key={user.id} inputProps={inputProps} subtree={`users.${index}`} />
-            ))
-          }
-        </tbody>
-      </table>
-    </form>
-  )
-}
-
-export default UserAdmin

+ 0 - 44
frontend/lib/forms.js.bak

@@ -1,44 +0,0 @@
-import { useField } from 'formik'
-
-const TextInput = ({ label, ...props }) => {
-  const [field, meta] = useField(props)
-  return (
-    <>
-      <label htmlFor={props.id || props.name}>{label}</label>
-      <input className='text-input' {...field} {...props} />
-      {meta.touched && meta.error ? (
-        <div className='error'>{meta.error}</div>
-      ) : null}
-    </>
-  )
-}
-
-const Checkbox = ({ children, ...props }) => {
-  const [field, meta] = useField({ ...props, type: 'checkbox' })
-  return (
-    <>
-      <label className='checkbox'>
-        <input {...field} {...props} type='checkbox' />
-        {children}
-      </label>
-      {meta.touched && meta.error ? (
-        <div className='error'>{meta.error}</div>
-      ) : null}
-    </>
-  )
-}
-
-const Select = ({ label, ...props }) => {
-  const [field, meta] = useField(props)
-  return (
-    <>
-      <label htmlFor={props.id || props.name}>{label}</label>
-      <select {...field} {...props} />
-      {meta.touched && meta.error ? (
-        <div className='error'>{meta.error}</div>
-      ) : null}
-    </>
-  )
-}
-
-export { TextInput, Checkbox, Select }

+ 0 - 155
frontend/lib/graphql.js

@@ -1,155 +0,0 @@
-import gql from 'graphql-tag'
-
-const USER_LOGIN = gql`
-  mutation USER_LOGIN($email: String!, $password: String!) {
-    login(email: $email, password: $password) {
-      id
-      email
-      name
-    }
-  }
-`
-
-const USER_LOGOUT = gql`
-  mutation USER_LOGOUT {
-    logout
-  }
-`
-
-const USER_SIGNUP = gql`
-  mutation USER_SIGNUP($email: String!, $password: String!, $name: String!) {
-    signup(email: $email, password: $password, name: $name) {
-      id
-      email
-      name
-    }
-  }
-`
-
-const CURRENT_USER = gql`
-  query {
-    me {
-      id
-      email
-      name
-    }
-  }
-`
-
-const TRAINING = gql`
-  query TRAINING($id: ID!){
-    training(id: $id) {
-      id
-      title
-      type {
-        name
-        description
-      }
-      createdAt
-      trainingDate
-      location
-      registration {
-        id
-      }
-      attendance
-    }
-  }
-`
-
-const TRAININGS = gql`
-  query TRAININGS {
-    trainings {
-      id
-      title
-      type {
-        name
-        description
-      }
-      content {
-        title
-      }
-      trainingDate
-      location
-      registration {
-        name
-      }
-      attendance
-      ratings {
-        value
-      }
-      published
-    }
-  }
-`
-
-const TRAINING_TYPES = gql`
-  query TRAINING_TYPES {
-    trainingTypes {
-      id
-      name
-      description
-    }
-  }
-`
-
-const CREATE_TRAINING = gql`
-  mutation CREATE_TRAINING($title: String!, $trainingDate: DateTime!) {
-    createTraining (title: $title, trainingDate: $trainingDate) {
-      id
-    }
-  }
-`
-
-const CREATE_TRAINING_TYPE = gql`
-  mutation CREATE_TRAINING_TYPE($name: String!, $description: String!) {
-    createTrainingType (name: $name, description: $description) {
-      id
-    }
-  }
-`
-
-const BLOCKS = gql`
-  query BLOCKS {
-    blocks {
-      sequence
-      title
-      duration
-      variation
-      format {
-        name
-        description
-      }
-      tracks {
-        title
-        artist
-        duration
-        link
-      }
-      exercise {
-        name
-        description
-        video
-        targets
-        baseExercise {
-          name
-        }
-      }
-      description
-    }
-  }
-`
-
-const CREATE_BLOCK = gql`
-  mutation CREATE_BLOCK($sequence: Int!, $title: String!, $duration: Int!, $variation: String, $format: ID, $tracks: [ID]!, $exercises: [ID]!, $description: String!) {
-    createBlock(sequence: $sequence, title: $title, duration: $duration, variation: $variation, format: $format, tracks: $tracks, exercises: $exercises, description: $description) {
-      id
-    }
-  }
-`
-
-export {
-  USER_LOGIN, USER_LOGOUT, USER_SIGNUP, CURRENT_USER,
-  TRAINING, TRAININGS, CREATE_TRAINING,
-  TRAINING_TYPES, CREATE_TRAINING_TYPE,
-  BLOCKS, CREATE_BLOCK
-}

+ 43 - 0
frontend/package-lock.json

@@ -3493,6 +3493,11 @@
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
       "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
     },
+    "deepmerge": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
+      "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
+    },
     "defaults": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@@ -4710,6 +4715,21 @@
         "mime-types": "^2.1.12"
       }
     },
+    "formik": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.1.tgz",
+      "integrity": "sha512-PdpgIPn7ipCzU5KBA+ED6Yy0kPqpZsWzoqI69ZJvcMtUfTzWrNfDeEdv2kd5aMAU3Iqs4PYFREAZjcthS/nqQw==",
+      "requires": {
+        "deepmerge": "^2.1.1",
+        "hoist-non-react-statics": "^3.3.0",
+        "lodash": "^4.17.14",
+        "lodash-es": "^4.17.14",
+        "react-fast-compare": "^2.0.1",
+        "scheduler": "^0.17.0",
+        "tiny-warning": "^1.0.2",
+        "tslib": "^1.10.0"
+      }
+    },
     "fragment-cache": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -5505,6 +5525,14 @@
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
+    "hoist-non-react-statics": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
+      "integrity": "sha512-wbg3bpgA/ZqWrZuMOeJi8+SKMhr7X9TesL/rXMjTzh0p0JUBo3II8DHboYbuIXWRlttrUFxwcu/5kygrCw8fJw==",
+      "requires": {
+        "react-is": "^16.7.0"
+      }
+    },
     "hosted-git-info": {
       "version": "2.8.5",
       "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz",
@@ -6843,6 +6871,11 @@
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
       "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
     },
+    "lodash-es": {
+      "version": "4.17.15",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
+      "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
+    },
     "lodash._reinterpolate": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@@ -8938,6 +8971,11 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-5.1.6.tgz",
       "integrity": "sha512-X1Y+0jR47ImDVr54Ab6V9eGk0Hnu7fVWGeHQSOXHf/C2pF9c6uy3gef8QUeuUiWlNb0i08InPSE5a/KJzNzw1Q=="
     },
+    "react-fast-compare": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
+      "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
+    },
     "react-is": {
       "version": "16.8.6",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
@@ -10346,6 +10384,11 @@
         "setimmediate": "^1.0.4"
       }
     },
+    "tiny-warning": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+      "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

+ 1 - 0
frontend/package.json

@@ -20,6 +20,7 @@
     "apollo-link": "^1.2.13",
     "apollo-link-error": "^1.1.12",
     "dotenv": "^8.2.0",
+    "formik": "^2.1.1",
     "fuse.js": "3.4.5",
     "graphql": "^14.5.8",
     "isomorphic-unfetch": "^3.0.0",

+ 4 - 3
frontend/pages/_app.js

@@ -1,8 +1,9 @@
 import App from 'next/app'
-import client from '../lib/apollo'
 import { ApolloProvider } from '@apollo/client'
-import Page from '../components/page'
-import { StoreProvider } from '../lib/store'
+
+import Page from '../src/app/components/Page'
+import client from '../src/lib/apollo'
+import { StoreProvider } from '../src/lib/store'
 
 class MyApp extends App {
   static async getInitialProps ({ Component, ctx }) {

+ 3 - 5
frontend/pages/admin/users.js

@@ -1,12 +1,10 @@
-import { useQuery } from '@apollo/client'
 import { withRouter } from 'next/router'
 
-import UserAdmin from '../../components/user/UserAdmin'
-
-import { CURRENT_USER } from '../../components/user/graphql'
+import { useCurrentUserQuery } from '../../src/gql'
+import { UserAdmin } from '../../src/user'
 
 const UserAdminPage = () => {
-  const { data, loading, error } = useQuery(CURRENT_USER)
+  const { data, loading, error } = useCurrentUserQuery()
   console.log('UserPage', data, loading, error && error.message)
 
   return (

+ 2 - 2
frontend/pages/login.js

@@ -1,8 +1,8 @@
-import LoginForm from '../components/user/LoginForm'
+import { LoginForm } from '../src/user'
 
 const LoginPage = props => (
   <>
-    <h1>Hello</h1>
+    <h1>Login</h1>
     <LoginForm />
   </>
 )

+ 2 - 2
frontend/components/footer.js → frontend/src/app/components/Footer.tsx

@@ -1,6 +1,6 @@
-import theme from '../styles/theme'
+import theme from '../../../src/styles/theme'
 
-const Footer = props => (
+const Footer = () => (
   <footer>
     <p>Footer</p>
     <style jsx>{`

+ 2 - 2
frontend/components/header.js → frontend/src/app/components/Header.tsx

@@ -1,6 +1,6 @@
-import Logo from './logo'
+import Logo from './Logo'
 
-const Header = props => (
+const Header = () => (
   <header>
     <Logo />
   </header>

+ 0 - 0
frontend/components/logo.js → frontend/src/app/components/Logo.tsx


+ 0 - 0
frontend/components/meta.js → frontend/src/app/components/Meta.tsx


+ 2 - 3
frontend/components/nav.js → frontend/src/app/components/Nav.tsx

@@ -1,8 +1,7 @@
-import React from 'react'
 import Link from 'next/link'
 
-import { UserNav } from './user/user'
-import theme from '../styles/theme'
+import { UserNav } from '../../../src/user/user'
+import theme from '../../styles/theme'
 
 const Nav = () => (
   <nav>

+ 6 - 6
frontend/components/page.js → frontend/src/app/components/Page.tsx

@@ -1,11 +1,11 @@
 import Head from 'next/head'
-import Header from './header'
-import Meta from './meta'
-import Nav from './nav'
-import Footer from './footer'
-import GlobalStyle from '../styles/global'
+import Header from './Header'
+import Meta from './Meta'
+import Nav from './Nav'
+import Footer from './Footer'
+import GlobalStyle from '../../styles/global'
 
-const Page = props => (
+const Page = (props: any) => (
   <>
     <Meta />
     <Head>

+ 42 - 0
frontend/src/form/__tests__/useFormHandler.test.tsx

@@ -0,0 +1,42 @@
+import { renderHook } from '@testing-library/react-hooks'
+
+import { useFormHandler } from '../useFormHandler'
+
+describe('form hook return values', () => {
+
+  const values = {
+    text: 'sample-text',
+    number: 42,
+    boolean: true,
+    textArray: ['element1', 'element2'],
+    objectArray: [{ text: 'sample1', boolean: true }, { text: 'sample2', boolean: false }],
+    object: { text: 'sample', array: [12, 13], nestedObject: { text: 'nested-sample', number: 18 } }
+  }
+
+  const Component = () => useFormHandler(values, values => { return {} })
+  const { result } = renderHook(Component)
+
+  it('returns correct initial states.', () => {
+    expect(result.current.values.text).toBe('sample-text')
+    expect(result.current.values.object.nestedObject.number).toBe(18)
+    expect(result.current.values.textArray[0]).toBe('element1')
+    expect(result.current.values.objectArray[1].text).toBe('sample2')
+    expect(result.current.errors).toEqual({})
+    expect(result.current.isSubmitting).toBe(false)
+  })
+
+  it('returns input element properties for text inputs.', () => {
+    const textProps = result.current.inputProps('text')
+    expect(textProps).toMatchObject({
+      name: 'text',
+      id: 'text',
+      value: 'sample-text',
+      placeholder: 'text',
+      label: 'text',
+      onBlur: expect.anything(),
+      onChange: expect.anything()
+    })
+  })
+})
+
+export default true

+ 15 - 0
frontend/src/form/components/Checkbox.tsx

@@ -0,0 +1,15 @@
+import { useField } from 'formik'
+import { InputProps } from '../types'
+
+const Checkbox = ({ children, ...props }: any) => {
+  const [field, meta] = useField({ ...props, type: 'checkbox' })
+  return (
+    <>
+      <label className='checkbox'>
+        <input type='checkbox' {...field} {...props} />
+      </label>
+    </>
+  )
+}
+
+export default Checkbox

+ 17 - 0
frontend/src/form/components/Select.tsx

@@ -0,0 +1,17 @@
+import { useField } from 'formik'
+import { InputProps } from '../types'
+
+const Select = ({ label, ...props }: any) => {
+  const [field, meta] = useField(props)
+  return (
+    <>
+      <label htmlFor={props.id || props.name}>{label}</label>
+      <select {...field} {...props} />
+      {(meta.touched && meta.error) && (
+        <div className='error'>{meta.error}</div>
+      )}
+    </>
+  )
+}
+
+export default Select

+ 18 - 0
frontend/src/form/components/TextInput.tsx

@@ -0,0 +1,18 @@
+import { useField } from 'formik'
+import { InputProps } from '../types'
+
+const TextInput = ({ label, ...props }: any) => {
+  const [field, meta] = useField(props)
+  console.log(field, meta)
+  return (
+    <>
+      {label && <label htmlFor={props.id || props.name}>{label}</label>}
+      <input {...field} {...props} />
+      {(meta.touched && meta.error) && (
+        <div className='error'>{meta.error}</div>
+      )}
+    </>
+  )
+}
+
+export default TextInput

+ 9 - 0
frontend/src/form/index.ts

@@ -0,0 +1,9 @@
+import TextInput from './components/TextInput'
+import Checkbox from './components/Checkbox'
+import { useFormHandler } from './useFormHandler'
+
+export {
+  TextInput,
+  Checkbox,
+  useFormHandler
+}

+ 14 - 0
frontend/src/form/types.ts

@@ -0,0 +1,14 @@
+export interface InputProps {
+  id: string,
+  name: string,
+  value: any,
+  onChange: (event: React.ChangeEvent) => void,
+  onBlur: (event: React.SyntheticEvent) => void,
+  placeholder: string,
+  label?: string
+}
+
+export interface InputPropsOptions {
+  label?: string
+  subtree?: string
+}

+ 12 - 30
frontend/components/form/forms.ts → frontend/src/form/useFormHandler.ts

@@ -1,28 +1,13 @@
-import { useEffect, useState, FormEvent, ChangeEvent, SyntheticEvent } from 'react'
+import { useState, FormEvent, ChangeEvent, SyntheticEvent } from 'react'
 
-import { getValue, setValue } from '../../lib/array'
-import TextInput from './TextInput'
-
-export interface InputProps {
-  id: string,
-  name: string,
-  value: any,
-  onChange: (event: ChangeEvent) => void,
-  onBlur: (event: SyntheticEvent) => void,
-  placeholder: string,
-  label: string
-}
-
-export interface InputPropsOptions {
-  label?: string
-  subtree?: string
-}
+import { InputProps, InputPropsOptions } from './types'
+import { getValue, setValue } from '../lib/nestedValues'
 
 interface FormHandler<ValueObject> {
   values: ValueObject,
   errors: Dict,
   isSubmitting: boolean,
-  setValues: (values: ValueObject) => void
+  setValues: any,
   inputProps: (name: string) => InputProps,
   handleChange: (event: ChangeEvent) => void,
   handleBlur: (event: SyntheticEvent) => void,
@@ -42,7 +27,7 @@ interface FormHandler<ValueObject> {
  * @param validate - validation function for the form
  * @returns hooks to handle the form
  */
-function useFormHandler<ValueObject>(
+export function useFormHandler<ValueObject>(
   initialValues: ValueObject,
   validate?: (values: Dict) => {},
   submit?: (data: any) => {}
@@ -54,10 +39,9 @@ function useFormHandler<ValueObject>(
 
   function handleChange(event: ChangeEvent): void {
     const target = (event.target as HTMLInputElement)
-    console.log('change', values, target.name)
-    setValues({
-      ...values,
-      [target.name]: target.value
+    console.log('change', values, target.name, target.value)
+    setValues(previousValues => {
+      return setValue(previousValues, target.name, target.value)
     })
   }
 
@@ -77,12 +61,12 @@ function useFormHandler<ValueObject>(
 
   // users[3].name
   function inputProps(name: string, options?: InputPropsOptions): InputProps {
-    const subtree = options && options.subtree
+    const path = (options && options.subtree) ? `${options.subtree}.${name}` : name
 
     return {
-      id: [subtree, name].join('.'),
-      name: name,
-      value: getValue(values, name),
+      id: path,
+      name: path,
+      value: getValue(values, path),
       onChange: handleChange,
       onBlur: handleBlur,
       placeholder: options && options.label || name,
@@ -101,5 +85,3 @@ function useFormHandler<ValueObject>(
     handleSubmit
   }
 }
-
-export { useFormHandler, TextInput }

+ 0 - 0
frontend/graphql/index.tsx → frontend/src/gql/index.tsx


+ 1 - 1
frontend/lib/__tests__/array.test.ts → frontend/src/lib/__tests__/nestedValues.test.ts

@@ -1,4 +1,4 @@
-import { getValue, setValue } from '../array'
+import { getValue, setValue } from '../nestedValues'
 
 const testUser1 = {
   name: 'Test User 1',

+ 0 - 0
frontend/lib/__tests__/regex.test.ts → frontend/src/lib/__tests__/regex.test.ts


+ 0 - 0
frontend/lib/apollo.js → frontend/src/lib/apollo.js


+ 0 - 0
frontend/lib/localState.js → frontend/src/lib/localState.js


+ 1 - 1
frontend/lib/array.ts → frontend/src/lib/nestedValues.ts

@@ -29,7 +29,7 @@ function recursiveSet<ValueObject>(object: ValueObject, path: string[], value: a
   }
 }
 
-function setValue(container: object | any[], path: string, value: any) {
+function setValue<ValueObject>(container: ValueObject, path: string, value: any) {
   const elements = toPath(path)
   return recursiveSet(container, elements, value)
 }

+ 0 - 0
frontend/lib/regex.js → frontend/src/lib/regex.js


+ 0 - 0
frontend/lib/store.tsx → frontend/src/lib/store.tsx


+ 0 - 0
frontend/styles/global.js → frontend/src/styles/global.js


+ 0 - 0
frontend/styles/theme.js → frontend/src/styles/theme.js


+ 0 - 0
frontend/components/user/__tests__/signup.js.bak → frontend/src/user/__tests__/signup.js.bak


+ 1 - 0
frontend/components/user/DeleteUserButton.tsx → frontend/src/user/components/DeleteUserButton.tsx

@@ -1,4 +1,5 @@
 import { SyntheticEvent } from "react"
+import { } from ''
 
 interface DeleteUserProps {
   user: {

+ 3 - 6
frontend/components/user/LoginForm.tsx → frontend/src/user/components/LoginForm.tsx

@@ -1,8 +1,5 @@
-import { FormEvent } from 'react'
-
-import { useUserLoginMutation, CurrentUserDocument } from '../../graphql'
-import { useFormHandler, TextInput } from '../form/forms'
-
+import { useUserLoginMutation, CurrentUserDocument } from '../../gql'
+import { useFormHandler, TextInput } from '../../form'
 
 const initialValues = {
   email: 'tomislav.cvetic@u-blox.com',
@@ -15,7 +12,7 @@ const LoginForm = () => {
   const { inputProps, values } = useFormHandler(initialValues)
 
   return (
-    <form onSubmit={async (event: FormEvent) => {
+    <form onSubmit={async (event: React.FormEvent) => {
       event.preventDefault()
       try {
         const data = await login({

+ 4 - 8
frontend/components/user/LogoutButton.tsx → frontend/src/user/components/LogoutButton.tsx

@@ -1,6 +1,4 @@
-import { useMutation } from '@apollo/client'
-import { USER_LOGOUT, CURRENT_USER } from './graphql'
-import { SyntheticEvent } from 'react'
+import { useUserLogoutMutation, CurrentUserDocument } from '../../gql'
 
 interface LogoutButtonProps {
   title?: string
@@ -8,17 +6,15 @@ interface LogoutButtonProps {
 
 const LogoutButton = ({ title }: LogoutButtonProps) => {
 
-  const [logout, { loading, error }] = useMutation(
-    USER_LOGOUT,
-    { refetchQueries: [{ query: CURRENT_USER }] }
+  const [logout, { loading, error }] = useUserLogoutMutation(
+    { refetchQueries: [{ query: CurrentUserDocument }] }
   )
 
   return (
-    <button disabled={loading} onClick={async (event: SyntheticEvent) => {
+    <button disabled={loading} onClick={async (event: React.SyntheticEvent) => {
       try {
         const data = await logout()
         console.log('LogoutButton', data)
-        //refetch()
       } catch (error) {
         console.log('LogoutButton', error)
       }

+ 4 - 8
frontend/components/user/RequestPassword.tsx → frontend/src/user/components/RequestPassword.tsx

@@ -1,9 +1,5 @@
-import { useMutation } from '@apollo/client'
-import { SyntheticEvent } from 'react'
-
-import { USER_REQUEST_PASSWORD } from './graphql'
-import { useFormHandler, TextInput } from '../form/forms'
-
+import { useRequestResetMutation } from '../../gql'
+import { useFormHandler, TextInput } from '../../form'
 
 const initialValues = {
   email: ''
@@ -11,11 +7,11 @@ const initialValues = {
 
 const RequestPassword = () => {
 
-  const [requestPassword, { loading, error }] = useMutation(USER_REQUEST_PASSWORD)
+  const [requestPassword, { loading, error }] = useRequestResetMutation()
   const { inputProps } = useFormHandler(initialValues)
 
   return (
-    <form onSubmit={async (event: SyntheticEvent) => {
+    <form onSubmit={async (event: React.SyntheticEvent) => {
       event.preventDefault()
     }}>
       <TextInput label='Email' {...inputProps('email')} />

+ 4 - 8
frontend/components/user/ResetPassword.tsx → frontend/src/user/components/ResetPassword.tsx

@@ -1,9 +1,5 @@
-import { useMutation } from '@apollo/client'
-import { FormEvent } from 'react'
-
-import { USER_RESET_PASSWORD } from './graphql'
-import { useFormHandler, TextInput } from '../form/forms'
-
+import { useResetPasswordMutation } from "../../gql"
+import { useFormHandler, TextInput } from "../../form"
 
 const initialValues = {
   password: '',
@@ -12,11 +8,11 @@ const initialValues = {
 
 const ResetPassword = () => {
 
-  const [resetPassword, { loading, error }] = useMutation(USER_RESET_PASSWORD)
+  const [resetPassword, { loading, error }] = useResetPasswordMutation()
   const { inputProps } = useFormHandler(initialValues)
 
   return (
-    <form onSubmit={async (event: FormEvent) => {
+    <form onSubmit={async (event: React.FormEvent) => {
       event.preventDefault()
     }}>
       <TextInput label='Password' {...inputProps('password')} />

+ 5 - 8
frontend/components/user/SignupForm.tsx → frontend/src/user/components/SignupForm.tsx

@@ -1,9 +1,6 @@
-import { SyntheticEvent } from 'react'
-import { useMutation } from '@apollo/react-hooks'
-
-import { useFormHandler, TextInput } from '../form/forms'
-import { USER_SIGNUP } from './graphql'
-import { userValidation } from './validation'
+import { useFormHandler, TextInput } from '../../form'
+import { useUserSignupMutation } from '../../gql'
+import { userValidation } from '../validation'
 
 
 const initialValues = {
@@ -16,10 +13,10 @@ const initialValues = {
 const SignupForm = () => {
 
   const { inputProps, values } = useFormHandler(initialValues)
-  const [userSignup, { loading, error }] = useMutation(USER_SIGNUP)
+  const [userSignup, { loading, error }] = useUserSignupMutation()
 
   return (
-    <form onSubmit={async (event: SyntheticEvent) => {
+    <form onSubmit={async (event: React.SyntheticEvent) => {
       event.preventDefault()
       try {
         const data = await userSignup({ variables: values })

+ 100 - 0
frontend/src/user/components/UserAdmin.tsx

@@ -0,0 +1,100 @@
+import { useEffect, useState } from 'react'
+import { Formik, Form } from 'formik'
+import { useUsersQuery, UsersQuery, Permission } from '../../gql'
+import { TextInput, Checkbox } from '../../form'
+
+const Permissions = Object.getOwnPropertyNames(Permission)
+console.log('Permissions', Permissions)
+
+const emptyUser = {
+  name: '',
+  email: '',
+  interests: [],
+  admin: false,
+  instructor: false,
+  id: ''
+}
+
+interface UserInputProps {
+  inputProps: (name: string, options?: any) => any
+  subtree: string
+}
+
+const UserInput = ({ path, ...props }: any) => {
+  const [touched, touch] = useState(false)
+
+  return (
+    <tr>
+      <td><input type='checkbox' /></td>
+      <td><TextInput name={`${path}.name`} type='text' placeholder='Name' /></td>
+      <td><TextInput name={`${path}.email`} type='email' placeholder='eMail' /></td>
+      <td><Checkbox name={`${path}.admin`} /></td>
+      <td><Checkbox name={`${path}.instructor`} /></td>
+      <td></td>
+      <td><button>Delete</button><button>Save</button></td>
+    </tr>
+
+  )
+}
+
+const UserAdmin = () => {
+  const { data, error, loading } = useUsersQuery()
+
+  useEffect(() => {
+  }, [data])
+
+  if (error) return <p>Error: {error.message}</p>
+  if (loading) return <p>Loading...</p>
+  if (data) {
+    const formData = data.users.map(user => {
+      if (!user) return null
+      const { permissions, ...rest } = user
+      return {
+        ...rest,
+        admin: permissions.includes(Permission.Admin),
+        instructor: permissions.includes(Permission.Instructor)
+      }
+    })
+
+    return (
+      <Formik
+        initialValues={formData}
+        onSubmit={values => console.log(values)}
+      >
+        {({ values, setValues }) => (
+          <>
+            <Form>
+              <table>
+                <thead>
+                  <tr>
+                    <th></th>
+                    <th>Name</th>
+                    <th>Email</th>
+                    {Permissions.map(permission => (
+                      <th key={permission}>{permission}</th>
+                    ))}
+                    <th>Interests</th>
+                    <th>Actions</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {values.map((user, index) => user && (
+                    <UserInput path={`[${index}]`} key={user.id} />
+                  ))}
+                </tbody>
+              </table>
+            </Form>
+            <button onClick={() => {
+              setValues([
+                ...values,
+                { ...emptyUser }
+              ])
+            }}>Add user</button>
+          </>
+        )}
+      </Formik>
+    )
+  }
+}
+
+export default UserAdmin

+ 1 - 1
frontend/components/user/UserDetails.tsx → frontend/src/user/components/UserDetails.tsx

@@ -1,4 +1,4 @@
-import { UserProps } from "./props"
+import { UserProps } from "../props"
 
 const UserDetails = ({ user }: UserProps) => {
   console.log('UserDetails', user)

+ 4 - 8
frontend/components/user/UserEditForm.tsx → frontend/src/user/components/UserEditForm.tsx

@@ -1,11 +1,7 @@
-import { SyntheticEvent } from "react"
-import { useMutation } from "@apollo/client"
+import { useFormHandler, TextInput } from "../../form"
 
-import { useFormHandler, TextInput } from "../form/forms"
-
-import { CURRENT_USER, USER_EDIT } from "./graphql"
-import { UserProps } from "./props"
-import { userValidation } from "./validation"
+import { UserProps } from "../props"
+import { userValidation } from "../validation"
 
 const initialData = {
   name: '',
@@ -19,7 +15,7 @@ const UserEditForm = ({ user }: UserProps) => {
   const { inputProps, values } = useFormHandler({ ...initialData, ...user })
 
   return (
-    <form onSubmit={async (event: SyntheticEvent) => {
+    <form onSubmit={async (event: React.SyntheticEvent) => {
       event.preventDefault()
       await updateUser({ variables: values, refetchQueries: [{ query: CURRENT_USER }] })
     }}>

+ 0 - 0
frontend/components/user/graphql.ts → frontend/src/user/graphql.ts


+ 17 - 0
frontend/src/user/index.ts

@@ -0,0 +1,17 @@
+import LoginForm from './components/LoginForm'
+import LogoutButton from './components/LogoutButton'
+import RequestPassword from './components/RequestPassword'
+import ResetPassword from './components/ResetPassword'
+import SignupForm from './components/SignupForm'
+import UserAdmin from './components/UserAdmin'
+import UserDetails from './components/UserDetails'
+
+export {
+  LoginForm,
+  LogoutButton,
+  RequestPassword,
+  ResetPassword,
+  SignupForm,
+  UserAdmin,
+  UserDetails
+}

+ 0 - 0
frontend/components/user/props.ts → frontend/src/user/props.ts


+ 0 - 0
frontend/components/user/signup.js.bak → frontend/src/user/signup.js.bak


+ 0 - 0
frontend/components/user/user.graphql → frontend/src/user/user.graphql


+ 0 - 0
frontend/components/user/user.js → frontend/src/user/user.js


+ 0 - 0
frontend/components/user/validation.ts → frontend/src/user/validation.ts


+ 2 - 1
frontend/tsconfig.json

@@ -26,6 +26,7 @@
     "next-env.d.ts",
     "**/*.ts",
     "**/*.tsx",
-    "lib/apollo.js"
+    "lib/apollo.js",
+    "pages/_app.js"
   ]
 }