소스 검색

doing too many things at once...

Tomi Cvetic 5 년 전
부모
커밋
3ec07edf65

+ 2 - 1
.vscode/settings.json

@@ -9,5 +9,6 @@
     "editor.fontFamily": "'Fira Code', 'Droid Sans Mono', 'monospace', monospace, 'Droid Sans Fallback'",
     "editor.fontLigatures": true,
     "files.trimTrailingWhitespace": true,
-    "workbench.colorTheme": "Cobalt2"
+    "workbench.colorTheme": "Cobalt2",
+    "editor.tabSize": 2
 }

+ 5 - 0
frontend/.babelrc

@@ -0,0 +1,5 @@
+{
+  "presets": [
+    "next/babel"
+  ]
+}

+ 5 - 0
frontend/babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    'next/babel'
+  ]
+}

+ 5 - 0
frontend/components/form/form.js

@@ -0,0 +1,5 @@
+const Form = (props, ref) => (
+    <form onReset={formikProps.handleReset} onSubmit={formikProps.handleSubmit} {...props} />
+)
+
+export default Form

+ 1 - 1
frontend/components/nav.js

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

+ 8 - 5
frontend/components/training.js → frontend/components/training/training.js

@@ -1,6 +1,7 @@
-import theme from '../styles/theme'
+import theme from '../../styles/theme'
 import PropTypes from 'prop-types'
 
+import TrainingType from './trainingType'
 import TrainingBlock from './trainingBlock'
 
 function calculateRating (ratings) {
@@ -115,10 +116,12 @@ const Training = props => (
 
 Training.propTypes = {
   title: PropTypes.string.isRequired,
-  type: PropTypes.shape({
-    name: PropTypes.string.isRequired,
-    description: PropTypes.string
-  })
+  type: PropTypes.shape(TrainingType.propTypes).isRequired,
+  content: PropTypes.arrayOf(TrainingBlock.propTypes).isRequired,
+  createdAt: PropTypes.number,
+  trainingDate: PropTypes.number.isRequired,
+  location: PropTypes.string.isRequired,
+  registration: PropTypes
 }
 
 export default Training

+ 0 - 0
frontend/components/trainingArchive.js → frontend/components/training/trainingArchive.js


+ 2 - 2
frontend/components/trainingBlock.js → frontend/components/training/trainingBlock.js

@@ -1,5 +1,5 @@
-import Track from './track'
-import Exercise from './exercise'
+import Track from '../track'
+import Exercise from '../exercise'
 
 const TrainingBlock = props => (
   <li>

+ 3 - 2
frontend/components/trainingForm.js → frontend/components/training/trainingForm.js

@@ -1,11 +1,12 @@
 import { Formik, Form } from 'formik'
 import { Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
+import { useMutation } from '@apollo/react-hooks'
 import * as Yup from 'yup'
 
-import { TextInput } from '../lib/forms'
+import { TextInput } from '../../lib/forms'
 import TrainingTypeInput from './trainingType'
-import { CREATE_TRAINING } from '../lib/graphql'
+import { CREATE_TRAINING } from '../../lib/graphql'
 
 const TrainingAdoption = adopt({
   mutation: ({ render }) => (

+ 0 - 0
frontend/components/trainingHint.js → frontend/components/training/trainingHint.js


+ 0 - 0
frontend/components/trainingType.js → frontend/components/training/trainingType.js


+ 25 - 31
frontend/components/signup.js → frontend/components/user/SignupForm.tsx

@@ -1,10 +1,26 @@
 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'
+import { useFormValidation } from '../../lib/forms'
+import { USER_SIGNUP, CURRENT_USER } from './graphql'
+import { signupValidation } from './validation'
+
+const initialValues = {
+  name: 'Tomi',
+  email: 'tomi@cvetic.ch',
+  password: '1234',
+  passwordAgain: '1234'
+}
+
+async function onSubmit(values, mutation) {
+  try {
+    const user = await mutation({ variables: values })
+    console.log(user)
+  } catch (error) {
+    console.log(error)
+  }
+}
 
 const SignupAdoption = adopt({
   mutation: ({ render }) => (
@@ -16,32 +32,9 @@ const SignupAdoption = adopt({
   ),
   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)
-        }
-      }}
+      initialValues={initialValues}
+      validationSchema={validationSchema}
+      onSubmit={values => onSubmit(values, mutation)}
     >
       {render}
     </Formik>
@@ -50,7 +43,7 @@ const SignupAdoption = adopt({
 
 const SignupForm = props => (
   <SignupAdoption>
-    {({ form, mutation }) => (
+    {({ form, mutation: { signup, data, error, loading } }) => (
       <Form>
         <TextInput
           label='Name'
@@ -76,7 +69,8 @@ const SignupForm = props => (
           type='password'
           placeholder='1234'
         />
-        <button type='submit'>Sign Up!</button>
+        <button type='reset' disabled={loading}>Sign Up!</button>
+        <button type='submit' disabled={loading}>Sign Up!</button>
 
         <style jsx>
           {`

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

@@ -0,0 +1,60 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { MockedProvider } from '@apollo/react-testing'
+
+import SignupForm from '../signup'
+import { USER_SIGNUP } from '../graphql'
+
+const { act } = renderer
+
+it('should render without error', () => {
+  renderer.create(
+    <MockedProvider mocks={[]} addTypename={false}>
+      <SignupForm />
+    </MockedProvider>
+  )
+})
+
+it('should render loading state initially', () => {
+  const mocks = [
+    {
+      request: {
+        query: USER_SIGNUP,
+        variables: {
+          email: 'test@email.com',
+          password: '1234',
+          name: 'Test Person'
+        }
+      },
+      result: {
+        data: {
+          user: {
+            id: '1',
+            email: 'test@email.com',
+            name: 'Test Person',
+            password: '$2a$10$wCQ1GP/U4BCLzKVtXYKxAeHOQto/nDRw2A5kDxdtQlNEglMWXFmh6',
+            createdAt: '2019-12-13T12:00:00.000Z',
+            comments: [],
+            ratings: [],
+            permissions: [],
+            interests: []
+          }
+        }
+      }
+    }
+  ]
+
+  const component = renderer.create(
+    <MockedProvider mocks={mocks} addTypename={false}>
+      <SignupForm />
+    </MockedProvider>
+  )
+
+  const form = component.root.findByType('form')
+  act(() => {
+    form.props.onSubmit()
+  })
+
+  const submit = component.root.findByType('button').toJSON
+  console.log(submit)
+})

+ 39 - 0
frontend/components/user/graphql.js

@@ -0,0 +1,39 @@
+import gql from 'graphql-tag'
+
+const USER_SIGNUP = gql`
+  mutation USER_SIGNUP($email: String!, $password: String!, $name: String!) {
+    signup(email: $email, password: $password, name: $name) {
+      id
+      email
+      name
+    }
+  }
+`
+
+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 CURRENT_USER = gql`
+  query {
+    me {
+      id
+      email
+      name
+    }
+  }
+`
+
+export { USER_SIGNUP, USER_LOGIN, USER_LOGOUT, CURRENT_USER }

+ 2 - 2
frontend/components/login.js → frontend/components/user/login.js

@@ -2,8 +2,8 @@ import { Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
 import { Formik, Form } from 'formik'
 
-import { USER_LOGIN, CURRENT_USER } from '../lib/graphql'
-import { TextInput } from '../lib/forms'
+import { USER_LOGIN, CURRENT_USER } from '../../lib/graphql'
+import { TextInput } from '../../lib/forms'
 
 const LoginAdoption = adopt({
   login: ({ render }) => (

+ 117 - 0
frontend/components/user/signup.js.bak

@@ -0,0 +1,117 @@
+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 './graphql'
+
+const initialValues = {
+  name: 'Tomi',
+  email: 'tomi@cvetic.ch',
+  password: '1234',
+  passwordAgain: '1234'
+}
+
+const 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')
+})
+
+async function onSubmit (values, mutation) {
+  try {
+    const user = await mutation({ variables: values })
+    console.log(user)
+  } catch (error) {
+    console.log(error)
+  }
+}
+
+const SignupAdoption = adopt({
+  mutation: ({ render }) => (
+    <Mutation
+      mutation={USER_SIGNUP}
+      refetchQueries={[{ query: CURRENT_USER }]}
+    >{(signup, { data, error, loading }) => render({ signup, data, error, loading, render })}
+    </Mutation>
+  ),
+  form: ({ mutation, render }) => (
+    <Formik
+      initialValues={initialValues}
+      validationSchema={validationSchema}
+      onSubmit={values => onSubmit(values, mutation)}
+    >
+      {render}
+    </Formik>
+  )
+})
+
+const SignupForm = props => (
+  <SignupAdoption>
+    {({ form, mutation: { signup, data, error, loading } }) => (
+      <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='reset' disabled={loading}>Sign Up!</button>
+        <button type='submit' disabled={loading}>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

+ 1 - 1
frontend/components/user.js → frontend/components/user/user.js

@@ -2,7 +2,7 @@ import { Query, Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
 import Link from 'next/link'
 
-import { CURRENT_USER, USER_LOGOUT } from '../lib/graphql'
+import { CURRENT_USER, USER_LOGOUT } from '../../lib/graphql'
 import LoginForm from './login'
 
 const UserAdoption = adopt({

+ 16 - 0
frontend/components/user/validation.ts

@@ -0,0 +1,16 @@
+import * as Yup from 'yup'
+
+const signupValidation = 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')
+})
+
+export { signupValidation }

+ 2 - 2
frontend/config.js

@@ -1,2 +1,2 @@
-export const endpoint = `http://localhost:4000`
-export const prodEndpoint = `http://localhost:4000`
+export const endpoint = 'http://localhost:4000'
+export const prodEndpoint = 'http://localhost:4000'

+ 39 - 0
frontend/lib/__tests__/forms.test.tsx

@@ -0,0 +1,39 @@
+import { renderHook, act } from '@testing-library/react-hooks'
+
+import useFormHandler from '../forms'
+
+describe('form hook return values', () => {
+
+  const { result } = renderHook(() => useFormHandler({ var: 'val' }, values => { return {} }))
+
+  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()
+  })
+
+  it('returns form submission properties.', () => {
+    expect(result.current.submitProps()).toMatchObject({
+      onSubmit: expect.anything()
+    })
+  })
+
+  it('sets the isSubmitting flag.', () => {
+    act(() => result.current.handleSubmit({ preventDefault: () => { } }))
+
+    expect(result.current.isSubmitting).toBe(true)
+  })
+
+
+
+})

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


+ 93 - 0
frontend/lib/forms.ts

@@ -0,0 +1,93 @@
+import { useEffect, useState } from 'react'
+
+type FormHandler = {
+  inputProps: (name: string) => {},
+  submitProps: () => {},
+  handleSubmit: (event: Event) => void,
+  handleChange: (event: Event) => void,
+  handleBlur: (event: Event) => void,
+  values: {},
+  errors: {},
+  isSubmitting: boolean
+}
+
+
+function useFormHandler(
+  initialState: {},
+  validate: (values: {}) => {}
+): FormHandler {
+
+  const [values, setValues] = useState(initialState)
+  const [errors, setErrors] = useState({})
+  const [isSubmitting, setIsSubmitting] = useState(false)
+
+  useEffect(() => {
+    if (isSubmitting) {
+      const noErrors = Object.keys(errors).length === 0
+      if (noErrors) {
+        console.log('no errors while submitting.')
+      } else {
+        console.log('errors while submitting,')
+      }
+      setIsSubmitting(false)
+    }
+  }, [errors])
+
+  function handleSubmit(
+    event: Event,
+    callback?: (event: Event, data?: {}) => void, data?: {}
+  ): void {
+    event.preventDefault()
+    const validationErrors = validate(values)
+    setErrors(validationErrors)
+    setIsSubmitting(true)
+    if (callback === null) {
+      callback(event, data)
+    }
+  }
+
+  function handleChange(event: Event): void {
+    setValues({
+      ...values,
+      [(event.target as HTMLInputElement).name]:
+        (event.target as HTMLInputElement).value
+    })
+  }
+
+  function handleBlur(event: Event): void {
+    const validationErrors = validate(values)
+    setErrors(validationErrors)
+  }
+
+  function inputProps(name: string): {} {
+    if (!initialState.hasOwnProperty(name)) {
+      throw Error(`${name} is not an existing field.`)
+    }
+    return {
+      name,
+      id: name,
+      onChange: handleChange,
+      onBlur: handleBlur,
+      value: values[name]
+    }
+  }
+
+  function submitProps(): {} {
+    return {
+      onSubmit: handleSubmit
+    }
+  }
+
+  return {
+    inputProps,
+    submitProps,
+    handleSubmit,
+    handleChange,
+    handleBlur,
+    values,
+    errors,
+    isSubmitting
+  }
+}
+
+export default useFormHandler

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1040 - 158
frontend/package-lock.json


+ 29 - 2
frontend/package.json

@@ -5,7 +5,9 @@
   "scripts": {
     "dev": "next dev",
     "build": "next build",
-    "start": "next start"
+    "start": "next start",
+    "test": "jest",
+    "test:watch": "npm run test -- --watchAll"
   },
   "dependencies": {
     "apollo-boost": "^0.4.4",
@@ -33,8 +35,33 @@
     "yup": "^0.27.0"
   },
   "devDependencies": {
+    "@apollo/react-testing": "^3.1.3",
+    "@babel/core": "^7.7.5",
+    "@babel/preset-env": "^7.7.6",
+    "@babel/preset-react": "^7.7.4",
+    "@testing-library/react": "^9.4.0",
+    "@testing-library/react-hooks": "^3.2.1",
+    "@types/react": "^16.9.16",
+    "@types/yup": "^0.26.27",
     "babel-eslint": "^10.0.3",
-    "jest": "^24.9.0"
+    "babel-jest": "^24.9.0",
+    "enzyme": "^3.11.0",
+    "enzyme-adapter-react-16": "^1.15.2",
+    "jest": "^24.9.0",
+    "jest-transform-graphql": "^2.1.0",
+    "react-test-renderer": "^16.12.0",
+    "typescript": "^3.7.3"
+  },
+  "jest": {
+    "testPathIgnorePatterns": [
+      "<rootDir>/.next/",
+      "<rootDir>/node_modules/"
+    ],
+    "transform": {
+      "\\.(gql|graphql)$": "jest-transform-graphql",
+      ".*": "babel-jest",
+      "^.+\\.js?$": "babel-jest"
+    }
   },
   "standard": {
     "parser": "babel-eslint"

+ 1 - 1
frontend/pages/signup.js

@@ -1,4 +1,4 @@
-import SignupForm from '../components/signup'
+import SignupForm from '../components/user/signup'
 
 const Signup = props => <SignupForm />
 

+ 0 - 7
frontend/pages/user.js

@@ -1,7 +0,0 @@
-import User from '../components/user'
-
-const UserPage = props => (
-  <User />
-)
-
-export default UserPage

+ 27 - 0
frontend/pages/user.tsx

@@ -0,0 +1,27 @@
+import User from '../components/user/user'
+
+interface Gogo {
+  g: string
+  h: number
+}
+
+let gg: Gogo = { g: 'hello', h: 45 }
+
+function letsGogo(anyGogo?: Gogo): void {
+  console.log(anyGogo)
+  return
+}
+
+letsGogo(gg)
+letsGogo({ g: '12', h: 12 })
+
+const myObj = { className: 'bongers' }
+
+const UserPage = (props?: Gogo) => (
+  <>
+    <h1 {...myObj}>TypeScript User</h1>
+    <User />
+  </>
+)
+
+export default UserPage

+ 5 - 0
frontend/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "compilerOptions": {
+    "target": "ESNext"
+  }
+}

+ 30 - 0
package-lock.json

@@ -0,0 +1,30 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "@types/prop-types": {
+      "version": "15.7.3",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
+      "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
+    },
+    "@types/react": {
+      "version": "16.9.16",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
+      "integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
+      "requires": {
+        "@types/prop-types": "*",
+        "csstype": "^2.2.0"
+      }
+    },
+    "csstype": {
+      "version": "2.6.8",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.8.tgz",
+      "integrity": "sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA=="
+    },
+    "typescript": {
+      "version": "3.7.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
+      "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw=="
+    }
+  }
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.