Browse Source

refactoring going on.

Tomi Cvetic 5 years ago
parent
commit
34adac4987

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

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

+ 1 - 0
frontend/Dockerfile

@@ -7,6 +7,7 @@ ENV PATH /app/node_modules/.bin:$PATH
 COPY package.json /app/package.json
 
 RUN npm install --silent
+RUN npm install typescript @types/react --silent
 RUN npm install react-scripts -g --silent
 
 CMD ["npm", "run", "dev"]

+ 1 - 3
frontend/babel.config.js

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

+ 9 - 7
frontend/components/search.js

@@ -55,7 +55,7 @@ class Search extends React.Component {
     this.setState({ query: '' })
   }
 
-  render() {
+  render () {
     return (
       <>
         <div id='searchbar'>
@@ -76,6 +76,7 @@ class Search extends React.Component {
           <button type='submit' disabled={!this.state.query}>
             <span>Search</span>
           </button>
+
           {this.state.active &&
             this.state.query &&
             (this.state.results ? (
@@ -85,21 +86,22 @@ class Search extends React.Component {
                 ))}
               </ul>
             ) : (
-                <p>Nothing found.</p>
-              ))}
+              <p>Nothing found.</p>
+            ))}
         </div>
 
         <style jsx>
           {`
             #searchbar {
               display: grid;
+              grid-template-columns: 1fr 1em 2em;
               border: 1px solid ${theme.colors.lightgrey};
-              align-self: end;
-              justify-self: right;
+              padding: 0.3em 2em;
             }
 
             input[type='text'] {
               border: none;
+              width: 60%;
             }
 
             button {
@@ -107,7 +109,6 @@ class Search extends React.Component {
               background: transparent;
               color: ${theme.colors.darkgrey};
               border: none;
-              padding: 0.3em 1.8em;
               cursor: pointer;
               box-shadow: none;
             }
@@ -121,8 +122,9 @@ class Search extends React.Component {
             }
 
             #searchresults {
-              display: float;
               position: absolute;
+              background: ${theme.colors.lightgrey};
+              top: 2em;
             }
           `}
         </style>

+ 242 - 0
frontend/components/training.js

@@ -0,0 +1,242 @@
+function calculateRating (ratings) {
+  const numberOfRatings = ratings.length
+  const sumOfRatings = ratings.reduce(
+    (accumulator, rating) => accumulator + rating.value,
+    0
+  )
+  return numberOfRatings ? sumOfRatings / numberOfRatings : '-'
+}
+
+const TrainingArchive = props => (
+  <div>
+    <h2>Training Archive</h2>
+    <ol>
+      {props.trainings.map(training => (
+        <TrainingHint key={training.id} training={training} />
+      ))}
+    </ol>
+  </div>
+)
+
+const TrainingHint = props => (
+  <div>
+    <div>{props.training.date}</div>
+    <div>{props.training.title}</div>
+  </div>
+)
+
+const Training = props => (
+  <article>
+    <h2>{props.title}</h2>
+    <aside>
+      <div id='trainingType'>
+        <span className='caption'>Type: </span>
+        <span className='data'>{props.type.name}</span>
+      </div>
+      <div id='trainingDate'>
+        <span className='caption'>Date: </span>
+        <span className='data'>
+          {new Date(props.trainingDate).toLocaleDateString()}
+        </span>
+      </div>
+      <div id='trainingLocation'>
+        <span className='caption'>Location: </span>
+        <span className='data'>{props.location}</span>
+      </div>
+      <div id='trainingRegistrations'>
+        <span className='caption'>Registrations: </span>
+        <span className='data'>
+          {props.registration.length} <a href=''>Register!</a>
+        </span>
+      </div>
+      <div id='trainingAttendance'>
+        <span className='caption'>Attendance: </span>
+        <span className='data'>{props.attendance}</span>
+      </div>
+      <div id='trainingRatings'>
+        <span className='caption'>Rating: </span>
+        <span className='data'>
+          {calculateRating(props.ratings)} [
+          <a href=''>{props.ratings.length}</a>] Rate it!
+          <a href=''>*</a>
+          <a href=''>*</a>
+          <a href=''>*</a>
+          <a href=''>*</a>
+          <a href=''>*</a>
+        </span>
+      </div>
+    </aside>
+    <section>
+      <h3>Content</h3>
+      <ol>
+        {props.content
+          .sort(block => block.sequence)
+          .map(block => (
+            <Block key={block.id} {...block} />
+          ))}
+      </ol>
+    </section>
+
+    <style jsx>
+      {`
+        article {
+          display: grid;
+          grid-template-columns: 1fr 3fr;
+
+          background-color: rgba(127, 127, 127, 0.5);
+        }
+
+        article > h2 {
+          font-weight: 900;
+          font-size: 120%;
+        }
+
+        aside {
+          padding: 1em 2em;
+          background: rgba(0, 127, 0, 0.5);
+        }
+
+        section {
+          padding: 1em 2em;
+          background: rgba(127, 0, 0, 0.5);
+        }
+      `}
+    </style>
+  </article>
+)
+
+const Youtube = props => {
+  const { link, rest } = props
+  const [crap, src] = props.link.match(/\?v=(.*)/)
+  return (
+    <iframe
+      width='285'
+      height='160'
+      src={`https://www.youtube.com/embed/${src}`}
+      frameBorder='0'
+      allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
+      allowFullScreen
+      {...rest}
+    />
+  )
+}
+
+const Spotify = props => {
+  const { link, rest } = props
+  const [crap, src] = props.link.match(/track\/(.*)/)
+  return (
+    <iframe
+      src={`https://open.spotify.com/embed/track/${src}`}
+      width='300'
+      height='80'
+      frameborder='0'
+      allowtransparency='true'
+      allow='encrypted-media'
+    />
+  )
+}
+
+const Media = props => {
+  if (props.link.includes('youtube.com')) {
+    return <Youtube {...props} />
+  } else if (props.link.includes('spotify.com')) {
+    return <Spotify {...props} />
+  } else {
+    return <p>Link not recognized.</p>
+  }
+}
+
+const Block = props => (
+  <li>
+    <h2>{props.title}</h2>
+    <p>
+      <span className='caption'>Duration: </span>
+      <span className='data'>{props.duration}</span>
+    </p>
+    <p>
+      <span className='caption'>Variation: </span>
+      <span className='data'>{props.variation}</span>
+    </p>
+    <p>
+      <span className='caption'>Description: </span>
+      <span className='data'>{props.description}</span>
+    </p>
+    <p>
+      <span className='caption'>Format: </span>
+      <span className='data'>
+        {props.format.name}{' '}
+        <sup>
+          <a title={props.format.description}>[?]</a>
+        </sup>
+      </span>
+    </p>
+    <section>
+      <h2>Tracks</h2>
+      <ol>
+        {props.tracks.map(track => (
+          <Track key={track.id} {...track} />
+        ))}
+      </ol>
+    </section>
+    <section>
+      <h2>Exercises</h2>
+      <ol>
+        {props.exercises.map(exercise => (
+          <Exercise key={exercise.id} {...exercise} />
+        ))}
+      </ol>
+    </section>
+
+    <style jsx>
+      {`
+        section {
+          display: grid;
+        }
+      `}
+    </style>
+  </li>
+)
+
+const Track = props => {
+  return (
+    <section>
+      <p>
+        Track {props.id}: {props.title} ({props.artist})
+      </p>
+      <Media link={props.link} />
+    </section>
+  )
+}
+
+const Exercise = props => {
+  return (
+    <section>
+      <p>
+        Exercise {props.id}: {props.name}
+      </p>
+    </section>
+  )
+}
+class TrainingCreateForm extends React.Component {
+  render () {
+    return (
+      <form>
+        <label htmlFor='title'>
+          Title
+          <input type='text' id='title' />
+        </label>
+        <label htmlFor='title'>
+          Title
+          <input type='text' id='title' />
+        </label>
+        <label htmlFor='title'>
+          Title
+          <input type='text' id='title' />
+        </label>
+      </form>
+    )
+  }
+}
+
+export { TrainingArchive }
+export default Training

+ 1 - 1
frontend/components/user/SignupForm.tsx

@@ -2,7 +2,7 @@ import { Formik, Form } from 'formik'
 import { Mutation } from 'react-apollo'
 import { adopt } from 'react-adopt'
 
-import { useFormValidation } from '../../lib/forms'
+import { useFormHandling } from '../../lib/forms'
 import { USER_SIGNUP, CURRENT_USER } from './graphql'
 import { signupValidation } from './validation'
 

+ 22 - 29
frontend/components/user/login.js

@@ -1,5 +1,4 @@
-import { Mutation } from 'react-apollo'
-import { adopt } from 'react-adopt'
+import { useMutation } from '@apollo/react-hooks'
 import { Formik, Form } from 'formik'
 
 import { USER_LOGIN, CURRENT_USER } from '../../lib/graphql'
@@ -7,11 +6,10 @@ import { TextInput } from '../../lib/forms'
 
 const LoginAdoption = adopt({
   login: ({ render }) => (
-    <Mutation
-      mutation={USER_LOGIN}
-      refetchQueries={[{ query: CURRENT_USER }]}
-    >
-      {(login, { data, error, loading }) => render({ login, data, error, loading })}
+    <Mutation mutation={USER_LOGIN} refetchQueries={[{ query: CURRENT_USER }]}>
+      {(login, { data, error, loading }) =>
+        render({ login, data, error, loading })
+      }
     </Mutation>
   ),
   form: ({ login: { login }, render }) => (
@@ -32,29 +30,24 @@ const LoginAdoption = adopt({
       {render}
     </Formik>
   )
-
 })
 
-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>
-)
+const LoginForm = props => {
+  const [login, {loading, error}] = useMutation(USER_LOGIN)
+  const {data, loading, error} = useQuery(CURRENT_USER)
+  if 
+  return (
+    <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>
+  )
+}
 
 export default LoginForm

+ 1 - 0
frontend/global.d.ts

@@ -0,0 +1 @@
+type Dict = { [name: string]: any }

+ 4 - 0
frontend/jest.setup.js

@@ -0,0 +1,4 @@
+import { configure } from 'enzyme'
+import Adapter from 'enzyme-adapter-react-16'
+
+configure({ adapter: new Adapter() })

+ 7 - 4
frontend/lib/__tests__/forms.test.tsx

@@ -1,10 +1,12 @@
-import { renderHook, act } from '@testing-library/react-hooks'
+import { renderHook } from '@testing-library/react-hooks'
+import { mount } from 'enzyme'
 
 import useFormHandler from '../forms'
 
 describe('form hook return values', () => {
 
-  const { result } = renderHook(() => useFormHandler({ var: 'val' }, values => { return {} }))
+  const Component = () => useFormHandler({ var: 'val' }, values => { return {} })
+  const { result } = renderHook(Component)
 
   it('returns correct initial states.', () => {
     expect(result.current.values.var).toBe('val')
@@ -29,8 +31,9 @@ describe('form hook return values', () => {
   })
 
   it('sets the isSubmitting flag.', () => {
-    act(() => result.current.handleSubmit({ preventDefault: () => { } }))
-
+    const wrapper = mount(<form {...result.current.submitProps()} />)
+    const form = wrapper.find('form')
+    form.simulate('submit')
     expect(result.current.isSubmitting).toBe(true)
   })
 

+ 20 - 8
frontend/lib/forms.ts

@@ -6,18 +6,30 @@ type FormHandler = {
   handleSubmit: (event: Event) => void,
   handleChange: (event: Event) => void,
   handleBlur: (event: Event) => void,
-  values: {},
-  errors: {},
+  values: Dict,
+  errors: Dict,
   isSubmitting: boolean
 }
 
-
+/**
+ * Provides hooks for forms.
+ * 
+ * @remarks
+ * You can use it like this
+ * ```ts
+ * const {inputParams, submitParams} = useFormHandler()
+ * ```
+ * 
+ * @param initialValues - Initial values of inputs 
+ * @param validate - validation function for the form
+ * @returns hooks to handle the form
+ */
 function useFormHandler(
-  initialState: {},
-  validate: (values: {}) => {}
+  initialValues: Dict,
+  validate: (values: Dict) => {}
 ): FormHandler {
 
-  const [values, setValues] = useState(initialState)
+  const [values, setValues] = useState(initialValues)
   const [errors, setErrors] = useState({})
   const [isSubmitting, setIsSubmitting] = useState(false)
 
@@ -41,7 +53,7 @@ function useFormHandler(
     const validationErrors = validate(values)
     setErrors(validationErrors)
     setIsSubmitting(true)
-    if (callback === null) {
+    if (callback !== undefined) {
       callback(event, data)
     }
   }
@@ -60,7 +72,7 @@ function useFormHandler(
   }
 
   function inputProps(name: string): {} {
-    if (!initialState.hasOwnProperty(name)) {
+    if (!values.hasOwnProperty(name)) {
       throw Error(`${name} is not an existing field.`)
     }
     return {

+ 34 - 0
frontend/lib/localState.js

@@ -0,0 +1,34 @@
+import gql from 'graphql-tag'
+
+export const typeDefs = gql`
+  extend type Query {
+    isLoggedIn: Boolean!
+  }
+`
+
+const LOCAL_STATE_QUERY = gql`
+  query {
+    menuOpen @client
+  }
+`
+
+const TOGGLE_MENU_MUTATION = gql`
+  mutation {
+    toggleMenu @client
+  }
+`
+
+export const resolvers = {
+  Mutation: {
+    toggleMenu(_, variables, {chache}) {
+      const {menuOpen} = cache.readQuery({
+        query: LOCAL_STATE_QUERY
+      })
+      const data = {
+        data: {menuOpen: !menuOpen}
+      }
+      cache.writeData(data)
+      return data
+    }
+  }
+}

+ 2 - 0
frontend/next-env.d.ts

@@ -0,0 +1,2 @@
+/// <reference types="next" />
+/// <reference types="next/types/global" />

+ 3 - 0
frontend/next.config.js

@@ -0,0 +1,3 @@
+const withTypescript = require('@zeit/next-typescript')
+
+module.exports = withTypescript()

+ 9 - 0
frontend/package-lock.json

@@ -2807,6 +2807,15 @@
       "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
       "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
     },
+    "@zeit/next-typescript": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@zeit/next-typescript/-/next-typescript-1.1.1.tgz",
+      "integrity": "sha512-EUcHCASftz1Bc80djkf3cKJrFgvFQyODOH1kty7ShVLLdXMaZpRLj+z7RxrCoNo1bP06w0vtXEDU0cKa0HmGgg==",
+      "dev": true,
+      "requires": {
+        "@babel/preset-typescript": "^7.0.0"
+      }
+    },
     "abab": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz",

+ 7 - 2
frontend/package.json

@@ -7,7 +7,8 @@
     "build": "next build",
     "start": "next start",
     "test": "jest",
-    "test:watch": "npm run test -- --watchAll"
+    "test:watch": "npm run test -- --watchAll",
+    "type-check": "tsc"
   },
   "dependencies": {
     "apollo-boost": "^0.4.4",
@@ -43,6 +44,7 @@
     "@testing-library/react-hooks": "^3.2.1",
     "@types/react": "^16.9.16",
     "@types/yup": "^0.26.27",
+    "@zeit/next-typescript": "^1.1.1",
     "babel-eslint": "^10.0.3",
     "babel-jest": "^24.9.0",
     "enzyme": "^3.11.0",
@@ -53,6 +55,9 @@
     "typescript": "^3.7.3"
   },
   "jest": {
+    "setupFilesAfterEnv": [
+      "<rootDir>/jest.setup.js"
+    ],
     "testPathIgnorePatterns": [
       "<rootDir>/.next/",
       "<rootDir>/node_modules/"
@@ -66,4 +71,4 @@
   "standard": {
     "parser": "babel-eslint"
   }
-}
+}

+ 9 - 8
frontend/pages/index.js

@@ -25,10 +25,10 @@ const Home = () => (
     </section>
 
     <section id='nextTraining'>
-      <Query query={TRAININGS}>{
-        ({ data, error, loading }) => {
-          if (error) return (<p>Error {error.message}</p>)
-          if (loading) return (<p>Loading...</p>)
+      <Query query={TRAININGS}>
+        {({ data, error, loading }) => {
+          if (error) return <p>Error {error.message}</p>
+          if (loading) return <p>Loading...</p>
           if (data.trainings.length) {
             console.log(data)
             return (
@@ -38,16 +38,17 @@ const Home = () => (
                     ...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: '/training' }}><a>create training...</a></Link>
+      <Link href={{ pathname: '/training' }}>
+        <a>create training...</a>
+      </Link>
     </section>
   </>
 )

+ 5 - 0
frontend/pages/test.tsx

@@ -0,0 +1,5 @@
+const IndexPage = () => (
+    <h1>Hello Next with TypeScript!</h1>
+)
+
+export default IndexPage

+ 18 - 0
frontend/pages/test2.tsx

@@ -0,0 +1,18 @@
+let a = 12
+
+type Style = 'bold' | 'italic'
+
+interface Person {
+    first: string
+    last: string
+}
+
+(x: number, y?: number): string {
+    return Math.pow(x, y).toString()
+}
+
+const About = ({ name }: { name: string }) => (
+    <h1>This page is about {name}</h1>
+)
+
+export default About

+ 27 - 2
frontend/tsconfig.json

@@ -1,5 +1,30 @@
 {
   "compilerOptions": {
-    "target": "ESNext"
-  }
+    "target": "ESNext",
+    "module": "ESNext",
+    "jsx": "preserve",
+    "moduleResolution": "Node",
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true
+  },
+  "exclude": [
+    "node_modules"
+  ],
+  "include": [
+    "next-env.d.ts",
+    "**/*.ts",
+    "**/*.tsx"
+  ]
 }

+ 1 - 28
package-lock.json

@@ -1,30 +1,3 @@
 {
-  "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=="
-    }
-  }
+  "lockfileVersion": 1
 }