소스 검색

Worked on project. Started Redux helper.

Tomislav Cvetic 8 년 전
부모
커밋
b6b2449976
14개의 변경된 파일876개의 추가작업 그리고 119개의 파일을 삭제
  1. 5 8
      server/basicSchema.js
  2. 15 16
      server/index.js
  3. 6 4
      server/project/model.js
  4. 116 0
      server/project/route.js
  5. 67 0
      server/router.js
  6. 5 8
      server/routes/movies.js
  7. 26 0
      src/auth/AuthService.js
  8. 39 0
      src/auth/Login.js
  9. 10 0
      src/auth/LoginAction.js
  10. 461 0
      src/css/normalize.css
  11. 0 41
      src/css/spectre.css
  12. 95 12
      src/helpers.js
  13. 1 0
      src/project/constants.js
  14. 30 30
      src/project/state.js

+ 5 - 8
server/basicSchema.js

@@ -7,15 +7,14 @@
   * copy current version
   *
 */
+import { Schema } from 'mongoose'
 
-const mongoose = require('mongoose')
-
-const historySchema = new mongoose.Schema({
-  author: [mongoose.Schema.Types.ObjectId],
+const historySchema = new Schema({
+  author: [Schema.Types.ObjectId],
   created: Date,
   version: Number,
   tag: String,
-  reference: [mongoose.Schema.Types.ObjectId]
+  reference: [Schema.Types.ObjectId]
 })
 
 const basicSchema = {
@@ -32,9 +31,7 @@ const basicSchema = {
     maxlength: 200 }
 }
 
-const collection = {
+export const collection = {
   ...basicSchema,
   __history: [historySchema]
 }
-
-module.exports = { basicSchema, collection }

+ 15 - 16
server/index.js

@@ -3,10 +3,9 @@ const bodyParser = require('body-parser')
 const sockjs = require('sockjs')
 const http = require('http')
 const mongoose = require('mongoose')
-const router = express.Router()
 
 /** Load the submodules */
-import ProjectModel from './project/model'
+import project from './project/route'
 
 /** Create the express app */
 const app = express()
@@ -27,25 +26,25 @@ sockjsServer.on('connection', function (conn) {
 /** Bind the http server to express */
 const server = http.createServer(app)
 
-function f1 (req, res, next) {
-  console.log('f1', req)
-  next()
-}
-function f2 (req, res, next) {
-  console.log('f2')
-  next()
-}
-function f3 (req, res, next) {
-  console.log('f3')
-  res.json({message: 'Welcome to the api!'})
-}
-
 sockjsServer.installHandlers(server, {prefix: '/echo'})
 mongoose.connect(connectionString)
 
+function welcomeRouter (req, res) {
+  res.json({ message: 'Welcome to the AutoMate db API!' })
+}
+function errorRouter (req, res) {
+  res.status(404)
+  res.send({ message: 'Unknown route.' })
+}
+
 app.use(bodyParser.json())
 app.use(bodyParser.urlencoded({ extended: true }))
-app.use(/^\/db(\/\w+)?(\/\w+)?\/?$/, [f1, f2, f3])
+app.get(/^\/db\/?$/, welcomeRouter)
+app.get(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.get])
+app.post(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.post])
+app.put(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.put])
+app.delete(/^\/db(\/\w+)?(\/\w+)?\/?$/, [project.delete])
+app.use(/.*/, errorRouter)
 
 /* app.get('/', function (req, res) {
   res.sendFile(`${__dirname}/index.html`)

+ 6 - 4
server/project/model.js

@@ -1,5 +1,5 @@
-const mongoose = require('mongoose')
-const basicSchema = require('../basicSchema')
+import mongoose from 'mongoose'
+import { collection } from '../basicSchema'
 
 const metaSchema = new mongoose.Schema({
   type: String,
@@ -10,8 +10,10 @@ const metaSchema = new mongoose.Schema({
 
 /** Metas are embedded. */
 const projectSchema = new mongoose.Schema({
-  ...basicSchema.collection,
+  ...collection,
   meta: [metaSchema]
 })
 
-module.exports = mongoose.model('Project', projectSchema)
+const Project = mongoose.model('Project', projectSchema)
+
+export default Project

+ 116 - 0
server/project/route.js

@@ -0,0 +1,116 @@
+import Project from './model'
+import mongoose from 'mongoose'
+
+const routing = {
+  /** GET handler (request one or more projects) */
+  get: (req, res, next) => {
+    const path = req.params['0']
+    let id = req.params['1']
+    console.log(req.params)
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    /** check whether an id was specified. */
+    if (typeof id !== 'undefined') {
+      try {
+        id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+      } catch (err) {
+        res.status(422)
+        res.send({error: err.message})
+        return
+      }
+      console.log(id)
+      /** if yes, return the one project */
+      Project.findOne({ _id: id }, function (err, project) {
+        if (err) {
+          res.status(404)
+          res.send(err)
+          return
+        }
+        res.json(project)
+      })
+    } else {
+      /** if not, return all projects */
+      /** @todo: add some pagination here */
+      /** @todo: add some filtering here */
+      Project.find(function (err, projects) {
+        if (err) {
+          res.status(404)
+          res.send(err)
+          return
+        }
+        res.json(projects)
+      })
+    }
+  },
+
+  /** POST handler (insert new projects into database) */
+  post: (req, res, next) => {
+    const path = req.params['0']
+    // const id = req.params['1']
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    const project = new Project(req.body)
+
+    project.save(function (err) {
+      if (err) {
+        res.status(422)
+        res.send(err)
+      }
+      res.send({ success: 'Project added.' })
+    })
+  },
+
+  /** PUT handler (update existing project) */
+  put: (req, res, next) => {
+    const path = req.params['0']
+    const id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    Project.findOne({ _id: id }, function (err, project) {
+      if (err) {
+        res.status(404)
+        res.send(err)
+      }
+
+      for (let prop in req.body) {
+        project[prop] = req.body[prop]
+      }
+
+      project.save(function (err) {
+        if (err) {
+          res.status(422)
+          res.send(err)
+        }
+        res.json({ message: 'Movie updated.' })
+      })
+    })
+  },
+
+  /** DELETE handler (delete project) */
+  delete: (req, res, next) => {
+    const path = req.params['0']
+    const id = mongoose.Types.ObjectId(req.params['1'].substring(1))
+    /** If the path doesn't match, call the next router */
+    if (path !== '/project') {
+      next()
+    }
+
+    Project.remove({ _id: id }, function (err, project) {
+      if (err) {
+        res.send(err)
+      }
+      res.json({ message: 'Movie deleted.' })
+    })
+  }
+}
+
+export default routing

+ 67 - 0
server/router.js

@@ -0,0 +1,67 @@
+const Project = require('./project')
+const express = require('express')
+const router = express.Router()
+
+// GET all movies
+router.route(/^\/(\w+)\/(\w+)$/).get(function (req, res, next) {
+  Project.find(function (err, movies) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json(movies)
+  })
+})
+/*
+// POST new movie
+router.route('/movies').post(function (req, res) {
+  const movie = new Project(req.body)
+
+  movie.save(function (err) {
+    if (err) {
+      return res.send(err)
+    }
+    res.send({ message: 'Project added.' })
+  })
+})
+
+// PUT update movie
+router.route('/movies/:id').put(function (req, res) {
+  Project.findOne({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+
+    for (prop in req.body) {
+      movie[prop] = req.body[prop]
+    }
+
+    movie.save(function (req, res) {
+      if (err) {
+        return res.send(err)
+      }
+      res.json({ message: 'Project updated.' })
+    })
+  })
+})
+
+// GET one movie
+router.route('/movies/:id').get(function (req, res) {
+  Project.findOne({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json(movie)
+  })
+})
+
+// DELETE one
+router.route('/movies/:id').delete(function (req, res) {
+  Project.remove({ _id: req.params.id }, function (err, movie) {
+    if (err) {
+      return res.send(err)
+    }
+    res.json({ message: 'Project deleted.' })
+  })
+}) */
+
+module.exports = router

+ 5 - 8
server/routes/movies.js

@@ -2,7 +2,7 @@ const Movie = require('../models/movie.js')
 const express = require('express')
 const router = express.Router()
 
-// GET
+// GET all movies
 router.route('/movies').get(function (req, res) {
   Movie.find(function (err, movies) {
     if (err) {
@@ -12,12 +12,10 @@ router.route('/movies').get(function (req, res) {
   })
 })
 
-// POST
+// POST new movie
 router.route('/movies').post(function (req, res) {
-  console.log('Body:', req.body)
   const movie = new Movie(req.body)
 
-  console.log('Movie:', movie)
   movie.save(function (err) {
     if (err) {
       return res.send(err)
@@ -26,7 +24,7 @@ router.route('/movies').post(function (req, res) {
   })
 })
 
-// PUT
+// PUT update movie
 router.route('/movies/:id').put(function (req, res) {
   Movie.findOne({ _id: req.params.id }, function (err, movie) {
     if (err) {
@@ -37,7 +35,6 @@ router.route('/movies/:id').put(function (req, res) {
       movie[prop] = req.body[prop]
     }
 
-    console.log('Movie:', movie)
     movie.save(function (req, res) {
       if (err) {
         return res.send(err)
@@ -47,7 +44,7 @@ router.route('/movies/:id').put(function (req, res) {
   })
 })
 
-// GET :id
+// GET one movie
 router.route('/movies/:id').get(function (req, res) {
   Movie.findOne({ _id: req.params.id }, function (err, movie) {
     if (err) {
@@ -57,7 +54,7 @@ router.route('/movies/:id').get(function (req, res) {
   })
 })
 
-// GET :id
+// DELETE one
 router.route('/movies/:id').delete(function (req, res) {
   Movie.remove({ _id: req.params.id }, function (err, movie) {
     if (err) {

+ 26 - 0
src/auth/AuthService.js

@@ -0,0 +1,26 @@
+/** @todo Need to implement authentication!
+  * https://auth0.com/blog/adding-authentication-to-your-react-flux-app/
+  * https://jwt.io/introduction/
+  */
+
+class AuthService {
+  login (username, password) {
+    return when(req({
+      url: 'http://localhost:3000/sessions/create',
+      method: 'POST',
+      crossOrigin: true,
+      type: 'json',
+      data: {
+        username,
+        password
+      }
+    }))
+    .then((res) => {
+      let jwt = res.id_token
+      LoginActions.loginUser(jwt)
+      return true
+    })
+  }
+}
+
+export default new AuthService()

+ 39 - 0
src/auth/Login.js

@@ -0,0 +1,39 @@
+import React from 'react'
+
+class Login extends React.Component {
+  constructor () {
+    super()
+    this.state = {
+      user: '',
+      password: ''
+    }
+    this.login = this.login.bind(this)
+    this.changeState = this.changeState.bind(this)
+  }
+
+  login (event) {
+    event.preventDefault()
+    Auth.login(this.state.user, this.state.password)
+      .catch((error) => {
+        console.log('Login error:', error)
+      })
+  }
+
+  changeState (event) {
+    this.state[event.target.name] = event.target.value
+  }
+
+  render () {
+    return (
+      <form action='' role='form'>
+        <div className='form-group'>
+          <input type='text' name='user' value={this.state.user} onChange={this.changeState} placeholder='Username' />
+          <input type='password' name='password' value={this.state.password} onChange={this.changeState} placeholder='Password' />
+          <button type='submit' onClick={this.login}>Submit</button>
+        </div>
+      </form>
+    )
+  }
+}
+
+export default Login

+ 10 - 0
src/auth/LoginAction.js

@@ -0,0 +1,10 @@
+export default {
+  loginUser: (jwt) => {
+    RouterContainer.get().transitionTo('/')
+    localStorage.setItem('jwt', jwt)
+    AppDispatcher.dispatch({
+      actionType: 'LOGIN_USER',
+      jwt
+    })
+  }
+}

+ 461 - 0
src/css/normalize.css

@@ -0,0 +1,461 @@
+/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Change the default font family in all browsers (opinionated).
+ * 2. Correct the line height in all browsers.
+ * 3. Prevent adjustments of font size after orientation changes in
+ *    IE on Windows Phone and in iOS.
+ */
+
+/* Document
+   ========================================================================== */
+
+html {
+  font-family: sans-serif; /* 1 */
+  line-height: 1.15; /* 2 */
+  -ms-text-size-adjust: 100%; /* 3 */
+  -webkit-text-size-adjust: 100%; /* 3 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers (opinionated).
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+article,
+aside,
+footer,
+header,
+nav,
+section {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in IE.
+ */
+
+figcaption,
+figure,
+main { /* 1 */
+  display: block;
+}
+
+/**
+ * Add the correct margin in IE 8.
+ */
+
+figure {
+  margin: 1em 40px;
+}
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * 1. Remove the gray background on active links in IE 10.
+ * 2. Remove gaps in links underline in iOS 8+ and Safari 8+.
+ */
+
+a {
+  background-color: transparent; /* 1 */
+  -webkit-text-decoration-skip: objects; /* 2 */
+}
+
+/**
+ * Remove the outline on focused links when they are also active or hovered
+ * in all browsers (opinionated).
+ */
+
+a:active,
+a:hover {
+  outline-width: 0;
+}
+
+/**
+ * 1. Remove the bottom border in Firefox 39-.
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Prevent the duplicate application of `bolder` by the next rule in Safari 6.
+ */
+
+b,
+strong {
+  font-weight: inherit;
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font style in Android 4.3-.
+ */
+
+dfn {
+  font-style: italic;
+}
+
+/**
+ * Add the correct background and color in IE 9-.
+ */
+
+mark {
+  background-color: #ff0;
+  color: #000;
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+audio,
+video {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in iOS 4-7.
+ */
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+/**
+ * Remove the border on images inside links in IE 10-.
+ */
+
+img {
+  border-style: none;
+}
+
+/**
+ * Hide the overflow in IE.
+ */
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers (opinionated).
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: sans-serif; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
+ *    controls in Android 4.
+ * 2. Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+html [type="button"], /* 1 */
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button; /* 2 */
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Change the border, margin, and padding in all browsers (opinionated).
+ */
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * 1. Add the correct display in IE 9-.
+ * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  display: inline-block; /* 1 */
+  vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Remove the default vertical scrollbar in IE.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10-.
+ * 2. Remove the padding in IE 10-.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding and cancel buttons in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-cancel-button,
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in IE 9-.
+ * 1. Add the correct display in Edge, IE, and Firefox.
+ */
+
+details, /* 1 */
+menu {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Scripting
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 9-.
+ */
+
+canvas {
+  display: inline-block;
+}
+
+/**
+ * Add the correct display in IE.
+ */
+
+template {
+  display: none;
+}
+
+/* Hidden
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10-.
+ */
+
+[hidden] {
+  display: none;
+}

+ 0 - 41
src/css/spectre.css

@@ -535,47 +535,6 @@ abbr[title] {
   cursor: help;
   text-decoration: none;
 }
-:lang(zh),
-:lang(ja),
-:lang(ko),
-.cjk {
-  font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Hiragino Kaku Gothic Pro", Meiryo, "Malgun Gothic", "Helvetica Neue", sans-serif;
-}
-:lang(zh) ins,
-:lang(ja) ins,
-.cjk ins,
-:lang(zh) u,
-:lang(ja) u,
-.cjk u {
-  border-bottom: .1rem solid;
-  text-decoration: none;
-}
-:lang(zh) del + del,
-:lang(ja) del + del,
-.cjk del + del,
-:lang(zh) del + s,
-:lang(ja) del + s,
-.cjk del + s,
-:lang(zh) ins + ins,
-:lang(ja) ins + ins,
-.cjk ins + ins,
-:lang(zh) ins + u,
-:lang(ja) ins + u,
-.cjk ins + u,
-:lang(zh) s + del,
-:lang(ja) s + del,
-.cjk s + del,
-:lang(zh) s + s,
-:lang(ja) s + s,
-.cjk s + s,
-:lang(zh) u + ins,
-:lang(ja) u + ins,
-.cjk u + ins,
-:lang(zh) u + u,
-:lang(ja) u + u,
-.cjk u + u {
-  margin-left: .125em;
-}
 .table {
   border-collapse: collapse;
   border-spacing: 0;

+ 95 - 12
src/helpers.js

@@ -1,15 +1,4 @@
-export function lockRange (value, nrOfBits) {
-  const lowerValue = 0
-  const upperValue = Math.pow(2, nrOfBits) - 1
-  if (value < lowerValue) {
-    return lowerValue
-  } else if (value > upperValue) {
-    return lowerValue
-  } else {
-    return value
-  }
-}
-
+/** Parse and print engineering notation */
 const prefixMap = {
   'y': 1e-24,
   'z': 1e-21,
@@ -54,3 +43,97 @@ export function parseEng (string) {
   }
   return sign * num * Math.pow(10, exp)
 }
+
+/** Redux helpers */
+import { call, put, takeEvery } from 'redux-sagas/effects'
+
+export function genStuff (name, actionsList = ['create', 'update', 'remove'], sync = true, api = null) {
+  const actionTypes = {}
+  const actions = {}
+
+  let actionList
+  if (!sync) {
+    let tmpActionList = []
+    actionList.forEach(action => {
+      tmpActionList.push(`${action}_request`)
+      tmpActionList.push(`${action}_success`)
+      tmpActionList.push(`${action}_failure`)
+    })
+    actionList = tmpActionList
+  }
+
+  /** Populate the actionTypes and actions */
+  actionList.forEach(action => {
+    const actionType = `${action.toUpperCase()}_${name.toUpperCase()}`
+    const actionName = `${action}${name[0].toUpperCase()}${name.substring(1)}`
+    actionTypes[actionType] = `${name}/${actionType}`
+    actions[actionName] = (id, data) => { return { type: `${name}/${actionType}`, id, data } }
+  })
+
+  const state = []
+
+  function * worker (action) {
+    try {
+      const data = yield call(api, payload)
+      yield put({ type: '??_SUCCESS', data })
+    } catch (error) {
+      yield put({ type: '??_FAILURE', error })
+    }
+  }
+
+  function * watcher (action) {
+    actionList.forEach(action => {
+      yield takeEvery(`${action.toUpperCase()}_REQUEST`, worker)
+    })
+  }
+
+  const reducer = (sync) ? (state = [], action) => {
+    let nextState
+    switch (action.type) {
+      case actionTypes[`CREATE_${name.toUpperCase()}`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      default:
+        return state
+    }
+  } : (state = [], action) => {
+    let nextState
+    switch (action.type) {
+      case actionTypes[`CREATE_REQUEST_${name.toUpperCase()}`]:
+        worker(action)
+        return nextState
+      case actionTypes[`CREATE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state, action.data ]
+        return nextState
+      case actionTypes[`UPDATE_REQUEST_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`UPDATE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), { ...state[action.id], ...action.data }, ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_REQUEST_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      case actionTypes[`REMOVE_SUCCESS_${name.toUpperCase()}`]:
+        nextState = [ ...state.slice(0, action.id), ...state.slice(action.id + 1) ]
+        return nextState
+      default:
+        return state
+    }
+  }
+
+  return {
+    actionTypes,
+    actions,
+    state,
+    reducer,
+    worker: (sync) ? null : worker,
+    watcher: (sync) ? null : watcher
+  }
+}

+ 1 - 0
src/project/constants.js

@@ -6,3 +6,4 @@
  **/
 
 export const NAME = 'project'
+export const DATA = ['project']

+ 30 - 30
src/project/state.js

@@ -8,46 +8,46 @@
  * - actions
  **/
 
-import { NAME } from './constants'
+import { NAME, DATA } from './constants'
 // import { call, put, takeEvery } from 'redux-saga/effects'
 
 /** actionTypes define what actions are handeled by the reducer. */
-export const actionTypes = {
-  CREATE_REQ: `${NAME}/CREATE_REQ`,
-  UPDATE_REQ: `${NAME}/UPDATE_REQ`,
-  DELETE_REQ: `${NAME}/DELETE_REQ`
-}
-
-/** actions is an object with references to all action creators */
-function createProject (project) {
-  return {
-    type: actionTypes.CREATE_REQ,
-    project
-  }
-}
+const actionTypes = {}
+export const actionCreators = {}
 
-function updateProject (projectId, project) {
-  return {
-    type: actionTypes.UPDATE_REQ,
-    projectId,
-    project
-  }
-}
+// Generate default actionsTypes CREATE, UPDATE, REMOVE
+DATA.forEach((dataItem, idx) => {
+  ['create_request', 'create_success', 'create_fail',
+    'update_request', 'update_success', 'update_fail',
+    'remove_request', 'remove_success', 'remove_fail'].forEach(action => {
+    // The Redux convention is to name action types e.g. demo_module/UPDATE
+    // For action creators, we define here the name e.g. removePrimary(id, data)
+    // where id is the element id and data is the element itself.
+      const actionType = `${action.toUpperCase()}_${dataItem.toUpperCase()}`
+      const actionName = `${action}${dataItem[0].toUpperCase()}${dataItem.substring(1)}`
+      if (idx === 0) {
+        actionCreators[actionName] = (id, data) => { return { type: `${NAME}/${actionType}`, id, data } }
+      } else {
+        actionCreators[actionName] = (primaryId, id, data) => { return { type: `${NAME}/${actionType}`, primaryId, id, data } }
+      }
+      actionTypes[actionType] = `${NAME}/${actionType}`
+    })
+})
 
-function removeProject (projectId) {
-  return {
-    type: actionTypes.REMOVE_REQ,
-    projectId
-  }
-}
-export const actions = { createProject, updateProject, removeProject }
+// Add specific action creators here:
+// actionCreators['loadSamples'] = () => { return { type: `${NAME}/LOAD_SAMPLES` } }
+// actionTypes['LOAD_SAMPLES'] = `${NAME}/LOAD_SAMPLES`
 
 /** state definition */
+/** It is generally easier to not have another object here. */
 export const state = []
+console.log('State state', state)
 
 /** reducer is called by the redux dispatcher and handles all component actions */
 export function reducer (state = {}, action) {
   switch (action.type) {
+    case actionTypes.CREATE_REQUEST:
+
     case actionTypes.CREATE:
       return {
         ...state,
@@ -68,7 +68,7 @@ export function reducer (state = {}, action) {
   }
 }
 
-/** sagas are asynchronous workers (JS generators) to handle the state.
+/** sagas are asynchronous workers (JS generators) to handle the state. */
 // Worker
 export function * incrementAsync () {
   try {
@@ -88,5 +88,5 @@ export function * watchIncrementAsync () {
 function * sagas () {
   yield takeEvery('INCREMENT_REQUEST')
   yield takeEvery('DECREMENT_REQUEST')
-} */
+}
 export const sagas = null