فهرست منبع

work in progress

Tomi Cvetic 5 سال پیش
والد
کامیت
43e5de4db0

+ 4 - 2
.vscode/settings.json

@@ -1,6 +1,8 @@
 {
-    "prettier.semi": false,
     "standard.autoFixOnSave": true,
     "editor.formatOnSave": true,
-    "prettier.bracketSpacing": false
+    "javascript.validate.enable": false,
+    "emmet.includeLanguages": {
+        "javascript": "javascriptreact"
+    }
 }

+ 5 - 8
backend/index.js

@@ -12,7 +12,7 @@ const bodyParser = require('body-parser')
 // const cors = require("cors");
 const { merge } = require('lodash')
 const { db, populateUser } = require('./src/db')
-// const { authenticate } = require('./src/authenticate')
+const { authenticate } = require('./src/authenticate')
 
 const prismaResolvers = require('./src/resolvers')
 
@@ -32,19 +32,16 @@ const server = new GraphQLServer({
 server.express.use(cookieParser())
 server.express.use(bodyParser.json())
 // server.express.use(quickMiddleware)
-// server.express.use(authenticate)
+server.express.use(authenticate)
 server.express.use(populateUser)
-/* server.express.use(
-  cors({ origin: process.env.FRONTEND_URL, credentials: false })
-); */
 // server.express.use("/static", express.static("static"));
 
 server.start(
-  /* {
+  {
     cors: {
-      credentials: false,
+      credentials: true,
       origin: process.env.FRONTEND_URL
     }
-  }, */
+  },
   server => console.log(`Server is running on http://localhost:${server.port}`)
 )

+ 4 - 1
backend/schema.graphql

@@ -10,6 +10,9 @@ type Query {
     first: Int
     last: Int
   ): [User]!
+  training(
+    where: TrainingWhereUniqueInput!
+  ): Training
   trainings(
     where: TrainingWhereInput
     orderBy: TrainingOrderByInput
@@ -24,7 +27,7 @@ type Query {
 
 type Mutation {
   createUser(name: String!, email: String!, password: String!): User!
-  createTraining(data: TrainingCreateInput!): Training!
+  createTraining(title: String!): Training!
   login(email: String!, password: String!): User!
   signup(name: String!, email: String!, password: String!): User!
   logout: String!

+ 10 - 3
backend/src/resolvers.js

@@ -6,7 +6,15 @@ const LoginError = new Error('ERR_LOGIN: You must be logged in.')
 
 const Query = {
   users: forwardTo('db'),
-  trainings: forwardTo('db'),
+  training: async (parent, args, context, info) => {
+    if (!context.request.userId) throw LoginError
+    return context.db.query.training({ data: args }, info)
+  },
+  trainings: async (parent, args, context, info) => {
+    console.log(context.request)
+    if (!context.request.userId) throw LoginError
+    return context.db.query.trainings()
+  },
   me: (parent, args, context, info) => {
     if (!context.request.userId) throw LoginError
     return context.db.query.user({ where: { id: context.request.userId } }, info)
@@ -17,7 +25,6 @@ const Mutation = {
   createUser: async (parent, args, context, info) => {
     const email = args.email.toLowerCase()
     const password = await bcrypt.hash(args.password, 10)
-    console.log(email, password)
     const user = await context.db.mutation.createUser(
       {
         data: {
@@ -74,7 +81,7 @@ const Mutation = {
   },
   createTraining: async (parent, args, context, info) => {
     const { userId } = context.request
-    // if (!userId) throw LoginError
+    if (!userId) throw LoginError
     const training = await context.db.mutation.createTraining(
       {
         data: args

+ 0 - 12
frontend/components/header.js

@@ -1,20 +1,8 @@
 import Logo from './logo'
-import Search from './search'
 
 const Header = props => (
   <header>
     <Logo />
-    <Search />
-
-    <style jsx>
-      {`
-        header {
-          display: grid;
-          grid-template-columns: 150px 1fr;
-          padding: 5px 20px;
-        }
-      `}
-    </style>
   </header>
 )
 

+ 49 - 44
frontend/components/login.js

@@ -1,50 +1,55 @@
 import { Mutation } from 'react-apollo'
-import gql from 'graphql-tag'
-//import { CURRENT_USER_QUERY } from './User'
+import { adopt } from 'react-adopt'
+import { Formik, Form } from 'formik'
 
-const LOGIN_MUTATION = gql`
-    mutation LOGIN_MUTATION($email: String!, $password: String!) {
-        login(email: $email, password: $password) { 
-            id
-            email
-            name 
-        }
-    }
-`
+import { USER_LOGIN, CURRENT_USER } from '../lib/graphql'
+import { TextInput } from '../lib/forms'
 
-class Login extends React.Component {
-    state = {
-        email: "",
-        password: ""
-    }
+const LoginAdoption = adopt({
+  login: ({ render }) => (
+    <Mutation mutation={USER_LOGIN} refetchQueries={[{ query: CURRENT_USER }]}>{render}</Mutation>
+  ),
+  form: ({ login, render }) => (
+    <Formik
+      initialValues={{
+        email: '',
+        password: ''
+      }}
+      onSubmit={async values => {
+        try {
+          const user = await login({ variables: values })
+          console.log(user)
+        } catch (error) {
+          console.log(error)
+        }
+      }}
+    >
+      {render}
+    </Formik>
+  )
 
-    saveToState = (ev) => {
-        this.setState({ [ev.target.name]: ev.target.value })
-    }
+})
 
-    render() {
-        return <Mutation mutation={LOGIN_MUTATION} variables={this.state} refetchQueries={[{ query: CURRENT_USER_QUERY }]}>
-            {(signin, { error, loading }) => {
-                return <form method="post" onSubmit={async ev => {
-                    ev.preventDefault()
-                    const res = await signin()
-                    this.setState({ name: '', email: '', password: '' })
-                }}>
-                    <fieldset disabled={loading} aria-busy={loading}>
-                        <h2>Sign in.</h2>
-                        <Error error={error} />
-                        <label htmlFor="email">email
-                            <input type="email" name="email" placeholder="email" value={this.state.email} onChange={this.saveToState} />
-                        </label>
-                        <label htmlFor="password">password
-                            <input type="password" name="password" placeholder="password" value={this.state.password} onChange={this.saveToState} />
-                        </label>
-                        <button type="submit">Sign In!</button>
-                    </fieldset>
-                </form>
-            }}
-        </Mutation>
-    }
-}
+const LoginForm = props => (
+  <LoginAdoption>
+    {({ form, mutation }) => (
+      <Form>
+        <TextInput
+          label='Email'
+          name='email'
+          type='email'
+          placeholder='email'
+        />
+        <TextInput
+          label='Password'
+          name='password'
+          type='password'
+          placeholder='password'
+        />
+        <button type='submit'>Login!</button>
+      </Form>
+    )}
+  </LoginAdoption>
+)
 
-export default Login
+export default LoginForm

+ 58 - 39
frontend/components/nav.js

@@ -1,51 +1,70 @@
 import React from 'react'
 import Link from 'next/link'
 
-import { UserNav } from '../components/user'
+import { UserNav } from './user'
+import theme from '../styles/theme'
 
 const Nav = () => (
-  <>
-    <nav>
-      <ul>
-        <li>
-          <Link href='/'>
-            <a>Home</a>
-          </Link>
-        </li>
-        <li>
-          <Link href='/archive'>
-            <a>Archive</a>
-          </Link>
-        </li>
-        <li>
-          <Link href='/exercises'>
-            <a>Exercises</a>
-          </Link>
-        </li>
-        <li>
-          <Link href='/polls'>
-            <a>Polls</a>
-          </Link>
-        </li>
-        <li>
-          <UserNav />
-        </li>
-      </ul>
-    </nav>
+  <nav>
+    <ul>
+      <li>
+        <Link href='/'>
+          <a>Home</a>
+        </Link>
+      </li>
+      <li>
+        <Link href='/archive'>
+          <a>Archive</a>
+        </Link>
+      </li>
+      <li>
+        <Link href='/exercises'>
+          <a>Exercises</a>
+        </Link>
+      </li>
+      <li>
+        <Link href='/polls'>
+          <a>Polls</a>
+        </Link>
+      </li>
+      <li id='user'>
+        <UserNav />
+      </li>
+    </ul>
 
     <style jsx>
       {`
-        ul {
-          padding: 0 0.5em;
-        }
-
-        li {
-          display: inline;
-          margin: 0 0.5em;
-        }
-      `}
+ul {
+  display: grid;
+}
+
+li {
+  padding: 0 0.5em;
+  border-bottom: 1px solid ${theme.colors.lightgrey};
+}
+
+@media (min-width: 500px) {
+  ul {
+    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+    border-bottom: 1px solid ${theme.colors.lightgrey};
+  }
+  li {
+    display: inline;
+    border-bottom: none;
+  }
+}
+
+#search {
+  grid-column-end: -2;
+}
+
+#user {
+  grid-column-end: -1;
+}
+  `}
     </style>
-  </>
+  </nav>
+
 )
 
 export default Nav

+ 6 - 3
frontend/components/search.js

@@ -55,7 +55,7 @@ class Search extends React.Component {
     this.setState({ query: '' })
   }
 
-  render () {
+  render() {
     return (
       <>
         <div id='searchbar'>
@@ -85,14 +85,17 @@ class Search extends React.Component {
                 ))}
               </ul>
             ) : (
-              <p>Nothing found.</p>
-            ))}
+                <p>Nothing found.</p>
+              ))}
         </div>
 
         <style jsx>
           {`
             #searchbar {
+              display: grid;
               border: 1px solid ${theme.colors.lightgrey};
+              align-self: end;
+              justify-self: right;
             }
 
             input[type='text'] {

+ 110 - 0
frontend/components/signup.js

@@ -0,0 +1,110 @@
+import { Formik, Form } from 'formik'
+import * as Yup from 'yup'
+import { Mutation } from 'react-apollo'
+import { adopt } from 'react-adopt'
+
+import { TextInput } from '../lib/forms'
+import { USER_SIGNUP, CURRENT_USER } from '../lib/graphql'
+
+const SignupAdoption = adopt({
+  mutation: ({ render }) => (
+    <Mutation
+      mutation={USER_SIGNUP}
+      refetchQueries={[{ query: CURRENT_USER }]}
+    >{render}
+    </Mutation>
+  ),
+  form: ({ mutation, render }) => (
+    <Formik
+      initialValues={{
+        name: 'Tomi',
+        email: 'tomi@cvetic.ch',
+        password: '1234',
+        passwordAgain: '1234'
+      }}
+      validationSchema={Yup.object({
+        name: Yup.string()
+          .required('Required')
+          .max(40, 'Must be 40 characters or less'),
+        email: Yup.string()
+          .email('Invalid email address')
+          .required('Required'),
+        password: Yup.string()
+          .min(4, 'Must have at least 8 characters'),
+        passwordAgain: Yup.string()
+          .oneOf([Yup.ref('password'), null], 'Passwords must match')
+      })}
+      onSubmit={async values => {
+        try {
+          const user = await mutation({ variables: values })
+          console.log(user)
+        } catch (error) {
+          console.log(error)
+        }
+      }}
+    >
+      {render}
+    </Formik>
+  )
+})
+
+const SignupForm = props => (
+  <SignupAdoption>
+    {({ form, mutation }) => (
+      <Form>
+        <TextInput
+          label='Name'
+          name='name'
+          type='text'
+          placeholder='Name'
+        />
+        <TextInput
+          label='Email'
+          name='email'
+          type='email'
+          placeholder='Email Address'
+        />
+        <TextInput
+          label='Password'
+          name='password'
+          type='password'
+          placeholder='1234'
+        />
+        <TextInput
+          label='Repeat password'
+          name='passwordAgain'
+          type='password'
+          placeholder='1234'
+        />
+        <button type='submit'>Sign Up!</button>
+
+        <style jsx>
+          {`
+            select, input {
+              color: rgba(0,0,127,1);
+            }
+
+            .error {
+              font-size: 12px;
+              color: rgba(127,0,0,1);
+              width: 400px;
+              margin-top: 0.25rem;
+            }
+
+            .error:before {
+              content: "❌ ";
+              font-size: 10px;
+            }
+
+            label {
+              color: red;
+              margin-top: 1rem;
+            }
+          `}
+        </style>
+      </Form>
+    )}
+  </SignupAdoption>
+)
+
+export default SignupForm

+ 39 - 18
frontend/components/training.js

@@ -1,4 +1,9 @@
 import theme from '../styles/theme'
+import { Formik, Form } from 'formik'
+import { Query } from 'react-apollo'
+
+import { TRAINING } from '../lib/graphql'
+import { TextInput } from '../lib/forms'
 
 function calculateRating(ratings) {
   const numberOfRatings = ratings.length
@@ -47,7 +52,7 @@ const Training = props => (
       </div>
       <div id='trainingRegistrations'>
         <span className='caption'>Registrations: </span>
-        <span className='data'>          {props.registration.length}        </span>
+        <span className='data'> {props.registration.length} </span>
       </div>
       <div id='trainingAttendance'>
         <span className='caption'>Attendance: </span>
@@ -82,10 +87,11 @@ const Training = props => (
       {`
         article {
           display: grid;
-          grid-template-areas: 
-            "title"
-            "information"
-            "content";
+          grid-template-areas:
+            'title title'
+            'information placeholder'
+            'content content';
+          grid-template-columns: 1fr 2fr;
           background-color: rgba(127, 127, 127, 0.5);
           background-image: url('media/man_working_out.jpg');
           background-size: auto 400px;
@@ -107,8 +113,10 @@ const Training = props => (
 
         aside {
           grid-area: information;
+          background: rgba(0, 127, 0, 0.5);
           padding: 1em 2em;
-          height: 400px;
+          margin: 0 1em;
+          min-height: 350px;
         }
 
         section {
@@ -142,14 +150,13 @@ const Youtube = props => {
 }
 
 const Spotify = props => {
-  const { link, rest } = props
-  const [crap, src] = props.link.match(/track\/(.*)/)
+  const src = props.link.match(/track\/(.*)/)[1]
   return (
     <iframe
       src={`https://open.spotify.com/embed/track/${src}`}
       width='300'
       height='80'
-      frameborder='0'
+      frameBorder='0'
       allowtransparency='true'
       allow='encrypted-media'
     />
@@ -237,10 +244,24 @@ const Exercise = props => {
     </section>
   )
 }
-class TrainingCreateForm extends React.Component {
-  render() {
-    return (
-      <form>
+
+const TrainingCreateForm = props => (
+  <Formik
+    initialVariables={{
+      title: '',
+      type: '',
+      content: [],
+      trainingDate: '',
+      location: '',
+      registration: [],
+      attendance: 0,
+      ratings: [],
+      published: false
+    }}
+    onSubmit={ev => console.log(ev)}
+  >
+    {() => (
+      <Form>
         <label htmlFor='title'>
           Title
           <input type='text' id='title' />
@@ -253,10 +274,10 @@ class TrainingCreateForm extends React.Component {
           Title
           <input type='text' id='title' />
         </label>
-      </form>
-    )
-  }
-}
+      </Form>
+    )}
+  </Formik>
+)
 
-export { TrainingArchive }
+export { TrainingArchive, TrainingCreateForm }
 export default Training

+ 60 - 76
frontend/components/user.js

@@ -1,88 +1,72 @@
 import { Query, Mutation } from 'react-apollo'
-import Link from 'next/link'
-import { Formik, Form, Field, ErrorMessage } from 'formik'
-import { email } from '../lib/regex'
 import { adopt } from 'react-adopt'
+import Link from 'next/link'
 
-import { USER_LOGIN, CURRENT_USER } from '../lib/graphql'
-
-const UserLoginForm = props => <p>Login Form</p>
+import { CURRENT_USER, USER_LOGOUT } from '../lib/graphql'
+import LoginForm from './login'
 
-const LoginAdoption = adopt({
-  mutation: ({ render, formik }) => (
-    <Mutation
-      mutation={USER_LOGIN}
-      refetchQueries={[{ query: CURRENT_USER }]}
-    >{render}
-    </Mutation>
-  ),
-  formik: ({ render, mutation }) => (
-    <Formik
-      initialValues={{ email: '', password: '' }}
-      validate={values => {
-        const errors = {}
-        if (!values.email) errors.email = 'Required'
-        else if (!email.test(values.email)) errors.email = 'Invalid email address'
-        return errors
-      }}
-      onSubmit={(values, { setSubmitting }) => {
-        mutation({ variables: values })
-      }}
-    >
-      {render}
-    </Formik>
-  )
+const UserAdoption = adopt({
+  user: ({ render }) => <Query query={CURRENT_USER}>{render}</Query>,
+  logout: ({ render }) => <Mutation mutation={USER_LOGOUT} refetchQueries={[{ query: CURRENT_USER }]}>{render}</Mutation>
 })
 
-const LoginForm = props => (
-  <LoginAdoption>
-    {({ formik, mutation }) => {
-      return (
-        <Form>
-          <Field type='email' name='email' />
-          <ErrorMessage name='email' component='span' />
-          <Field type='password' name='password' />
-          <ErrorMessage name='password' component='span' />
-          <button type='submit' disabled={formik.isSubmitting}>Login</button>
-        </Form>
-      )
-    }}
-  </LoginAdoption>
-)
+class UserNav extends React.Component {
+  state = {
+    menu: false
+  }
 
-const UserNav = props => (
-  <Query query={CURRENT_USER}>
-    {
-      ({ data, error, loading }) => {
-        if (error) {
-          return <LoginForm />
-        }
-        if (loading) return (<p>Loading...</p>)
-        console.log(data)
-        return (
-          <Link href={{ pathname: 'user', query: { id: 12 } }}>
-            <a>test {props.query}</a>
-          </Link>
-        )
-      }
-    }
-  </Query>
-)
+  toggleMenu = ev => {
+    ev.preventDefault()
+    this.setState({ menu: !this.state.menu })
+  }
 
-const SignupForm = props => (
-  <Formik
-    initialValues={{ email: '', name: '', password: '', passwordAgain: '' }}
-    onSubmit={values => {
-      console.log('submitted', values)
-    }}
-  >
-    {
-      ({ isSubmitting }) => (
-        <Form />
-      )
+  logout = async (ev, logout) => {
+    ev.preventDefault()
+    try {
+      const id = await logout()
+    } catch (error) {
+      console.log(error)
     }
-  </Formik>
-)
+  }
+
+  render() {
+    return (
+      <UserAdoption>{({ user, logout }) => {
+        console.log(user, logout)
+        if (user.error) return <LoginForm />
+        if (user.loading) return <p>Loading...</p>
+        const { name, email, id } = user.data.me
+        return (
+          <>
+            <a href='' onClick={this.toggleMenu}>{name}</a>
+            {
+              this.state.menu ? (
+                <section className='usermenu'>
+                  <h2>Welcome, {name}</h2>
+                  <Link href={{ path: 'user' }}><a>Edit user data</a></Link>
+                  <a href='' onClick={ev => {
+                    ev.preventDefault()
+                    this.logout(ev, logout)
+                  }}>Logout</a>
+                </section>
+              ) : null
+            }
+
+            <style jsx>
+              {`
+section.usermenu {
+  position: absolute;
+  background: rgba(127,0,0,0.5);
+}
+                `}
+            </style>
+          </>
+        )
+      }}
+      </UserAdoption>
+    )
+  }
+}
 
 const User = props => <a />
 

+ 44 - 0
frontend/lib/forms.js

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

+ 45 - 4
frontend/lib/graphql.js

@@ -12,9 +12,7 @@ const USER_LOGIN = gql`
 
 const USER_LOGOUT = gql`
   mutation USER_LOGOUT {
-    logout {
-      id
-    }
+    logout
   }
 `
 
@@ -22,6 +20,8 @@ const USER_SIGNUP = gql`
   mutation USER_SIGNUP($email: String!, $password: String!, $name: String!) {
     signup(email: $email, password: $password, name: $name) {
       id
+      email
+      name
     }
   }
 `
@@ -36,4 +36,45 @@ const CURRENT_USER = gql`
   }
 `
 
-export { USER_LOGIN, USER_LOGOUT, USER_SIGNUP, CURRENT_USER }
+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
+      trainingDate
+    }
+  }
+`
+
+const CREATE_TRAINING = gql`
+  mutation CREATE_TRAINING($title: String!, $trainingDate: DateTime!) {
+    createTraining (title: $title, trainingDate: $trainingDate) {
+      id
+    }
+  }
+`
+
+export {
+  USER_LOGIN, USER_LOGOUT, USER_SIGNUP, CURRENT_USER,
+  TRAINING, TRAININGS, CREATE_TRAINING
+}

+ 3 - 2
frontend/lib/withApollo.js

@@ -15,7 +15,8 @@ import { onError } from 'apollo-link-error'
 import { ApolloLink } from 'apollo-link'
 
 const cache = new InMemoryCache()
-const link = new HttpLink({ uri: 'http://localhost:8801/' })
+const link = new HttpLink({ uri: 'http://localhost:8801/', credentials: 'include' })
+
 const oldLink = ApolloLink.from([
   onError(({ graphQLErrors, networkError }) => {
     if (graphQLErrors) {
@@ -29,7 +30,7 @@ const oldLink = ApolloLink.from([
   })
 ])
 
-function createClient({ ctx, headers }) {
+function createClient ({ ctx, headers }) {
   return new ApolloClient({
     link,
     cache

+ 47 - 0
frontend/package-lock.json

@@ -1597,6 +1597,20 @@
       "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz",
       "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="
     },
+    "babel-eslint": {
+      "version": "10.0.3",
+      "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.3.tgz",
+      "integrity": "sha512-z3U7eMY6r/3f3/JB9mTsLjyxrv0Yb1zb8PCWCLpguxfCzBIZUwy23R1t/XKewP+8mEN2Ck8Dtr4q20z6ce6SoA==",
+      "dev": true,
+      "requires": {
+        "@babel/code-frame": "^7.0.0",
+        "@babel/parser": "^7.0.0",
+        "@babel/traverse": "^7.0.0",
+        "@babel/types": "^7.0.0",
+        "eslint-visitor-keys": "^1.0.0",
+        "resolve": "^1.12.0"
+      }
+    },
     "babel-loader": {
       "version": "8.0.6",
       "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.6.tgz",
@@ -3524,6 +3538,11 @@
         "readable-stream": "^2.3.6"
       }
     },
+    "fn-name": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz",
+      "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc="
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -6437,6 +6456,11 @@
         "reflect.ownkeys": "^0.2.0"
       }
     },
+    "property-expr": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz",
+      "integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g=="
+    },
     "prr": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -7463,6 +7487,11 @@
       "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
       "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
     },
+    "synchronous-promise": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.10.tgz",
+      "integrity": "sha512-6PC+JRGmNjiG3kJ56ZMNWDPL8hjyghF5cMXIFOKg+NiwwEZZIvxTWd0pinWKyD227odg9ygF8xVhhz7gb8Uq7A=="
+    },
     "table": {
       "version": "5.4.6",
       "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
@@ -7719,6 +7748,11 @@
       "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
       "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
     },
+    "toposort": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
+      "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA="
+    },
     "traverse": {
       "version": "0.6.6",
       "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz",
@@ -8171,6 +8205,19 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
       "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
     },
+    "yup": {
+      "version": "0.27.0",
+      "resolved": "https://registry.npmjs.org/yup/-/yup-0.27.0.tgz",
+      "integrity": "sha512-v1yFnE4+u9za42gG/b/081E7uNW9mUj3qtkmelLbW5YPROZzSH/KUUyJu9Wt8vxFJcT9otL/eZopS0YK1L5yPQ==",
+      "requires": {
+        "@babel/runtime": "^7.0.0",
+        "fn-name": "~2.0.1",
+        "lodash": "^4.17.11",
+        "property-expr": "^1.5.0",
+        "synchronous-promise": "^2.0.6",
+        "toposort": "^2.0.2"
+      }
+    },
     "zen-observable": {
       "version": "0.8.14",
       "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.14.tgz",

+ 9 - 2
frontend/package.json

@@ -28,6 +28,13 @@
     "react-apollo": "^3.1.3",
     "react-dom": "16.11.0",
     "standard": "^14.3.1",
-    "styled-components": "^4.4.1"
+    "styled-components": "^4.4.1",
+    "yup": "^0.27.0"
+  },
+  "devDependencies": {
+    "babel-eslint": "^10.0.3"
+  },
+  "standard": {
+    "parser": "babel-eslint"
   }
-}
+}

+ 2 - 3
frontend/pages/_app.js

@@ -13,7 +13,7 @@ import Page from '../components/page'
  */
 
 class MyApp extends App {
-  static async getInitialProps({ Component, ctx }) {
+  static async getInitialProps ({ Component, ctx }) {
     let pageProps = {}
 
     if (Component.getInitialProps) {
@@ -23,11 +23,10 @@ class MyApp extends App {
     // Add the query object to the pageProps
     // https://github.com/wesbos/Advanced-React/blob/master/finished-application/frontend/pages/_app.js
     pageProps.query = ctx.query
-
     return { pageProps }
   }
 
-  render() {
+  render () {
     const { Component, apollo, pageProps } = this.props
 
     return (

+ 49 - 0
frontend/pages/createTraining.js

@@ -0,0 +1,49 @@
+import { Formik, Form } from 'formik'
+import { Mutation } from 'react-apollo'
+import { adopt } from 'react-adopt'
+import * as Yup from 'yup'
+
+import { TextInput } from '../lib/forms'
+import { CREATE_TRAINING } from '../lib/graphql'
+
+const TrainingAdoption = adopt({
+  training: ({ render }) => (<Mutation mutation={CREATE_TRAINING}>{render}</Mutation>),
+  form: ({ render }) => (
+    <Formik
+      initialVariables={{
+        title: '',
+        trainingDate: ''
+      }}
+      validationSchema={{
+        title: Yup.string(),
+        trainingDate: Yup.date()
+      }}
+      onSubmit={ev => console.log(ev.target)}
+    >
+      {render}
+    </Formik>)
+})
+
+const CreateTraining = props => (
+  <TrainingAdoption>
+    {({ training, form }) => (
+      <Form>
+        <h2>Create Training</h2>
+        <TextInput
+          label='title'
+          name='title'
+          type='text'
+          placeholder='title'
+        />
+        <TextInput
+          label='Training date'
+          name='trainingDate'
+          type='date'
+        />
+        <button type='submit'>Create!</button>
+      </Form>
+    )}
+  </TrainingAdoption>
+)
+
+export default CreateTraining

+ 26 - 6
frontend/pages/index.js

@@ -1,6 +1,9 @@
+import { Query } from 'react-apollo'
+import Link from 'next/link'
+
 import Training from '../components/training'
 import data from '../initial-data.js'
-import theme from '../styles/theme'
+import { TRAININGS } from '../lib/graphql'
 
 console.log(data)
 
@@ -22,12 +25,29 @@ const Home = () => (
     </section>
 
     <section id='nextTraining'>
-      <Training {
-        ... {
-          ...data.trainings[data.trainings.length - 1],
-          title: `Your Next Training: ${data.trainings[data.trainings.length - 1].title}`,
+      <Query query={TRAININGS}>{
+        ({ data, error, loading }) => {
+          console.log(data, error, loading)
+          if (error) return (<p>Error {error.message}</p>)
+          if (loading) return (<p>Loading...</p>)
+          if (data.trainings.length) {
+            return (
+              <>
+                <Training
+                  {...{
+                    ...data.trainings[data.trainings.length - 1],
+                    title: `Your Next Training: ${
+                      data.trainings[data.trainings.length - 1].title
+                      }`
+                  }}
+                />
+              </>
+            )
+          } else return <p>Nothing found...</p>
         }
-      } />
+      }
+      </Query>
+      <Link href={{ pathname: '/createTraining' }}><a>create training...</a></Link>
     </section>
   </>
 )

+ 5 - 0
frontend/pages/signup.js

@@ -0,0 +1,5 @@
+import SignupForm from '../components/signup'
+
+const Signup = props => <SignupForm />
+
+export default Signup

+ 35 - 1
frontend/styles/global.js

@@ -13,7 +13,14 @@ html {
   box-sizing: inherit;
 }
 
-body {
+body #__next {
+  display: grid;
+  grid-template-areas: 
+    "header"
+    "nav"
+    "main"
+    "footer";
+  grid-template-rows: auto auto 1fr minmax(180px, auto);
   padding: 0;
   margin: 0;
   font-size: 1.5rem;
@@ -24,6 +31,33 @@ body {
   margin: 0 auto;
 }
 
+@media (min-width: 500px) {
+  body #__next {
+    grid-template-areas:
+      "header nav"
+      "main main"
+      "footer footer";
+    grid-template-columns: auto 1fr;
+    grid-template-rows: auto 1fr minmax(180px, auto);
+  }
+}
+
+header {
+  grid-area: header;
+}
+
+nav {
+  grid-area: nav;
+}
+
+main {
+  grid-area: main;
+}
+
+footer {
+  grid-area: footer;
+}
+
 #__next {
   background: ${theme.colors.offWhite};
   color: ${theme.colors.black};

+ 18 - 18
frontend/styles/theme.js

@@ -1,21 +1,21 @@
 const theme = {
-    colors: {
-        lightred: '#f0b0b0',
-        red: '#f03535',
-        black: '#393939',
-        darkgrey: '#5d6a6b',
-        grey: '#7f8c8d',
-        lightgrey: '#95a5a5',
-        lighterblue: '#d6e4f0',
-        lightblue: '#b0d3f0',
-        blue: '#4482c3',
-        darkblue: '#285680',
-        darkerblue: '#204567',
-        offWhite: '#EDEDED'
-    },
-    maxWidth: '1000px',
-    bs: '0 12px 24px 0 rgba(0,0,0,0.09)',
-    bsSmall: '0 5px 10px 0 rgba(0,0,0,0.19)'
+  colors: {
+    lightred: '#f0b0b0',
+    red: '#f03535',
+    black: '#393939',
+    darkgrey: '#5d6a6b',
+    grey: '#7f8c8d',
+    lightgrey: '#95a5a5',
+    lighterblue: '#d6e4f0',
+    lightblue: '#b0d3f0',
+    blue: '#4482c3',
+    darkblue: '#285680',
+    darkerblue: '#204567',
+    offWhite: '#EDEDED'
+  },
+  maxWidth: '1000px',
+  bs: '0 12px 24px 0 rgba(0,0,0,0.09)',
+  bsSmall: '0 5px 10px 0 rgba(0,0,0,0.19)'
 }
 
-export default theme
+export default theme