Skip to content

Commit e17e27a

Browse files
committed
Initial commit
0 parents  commit e17e27a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+11303
-0
lines changed

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
An example react-redux-saga CRUD application
3+
4+
# react-redux-crud
5+
6+
An example CRUD application for managing blog posts. Built with [React](https://reactjs.org/), [Redux](https://redux.js.org/) and [Redux Saga](https://redux-saga.js.org/) on the client side. The REST API server was generated with [loopback.io](http://loopback.io/) and uses an in-memory database at runtime (persisting the models in a flat db.json file).
7+
8+
## How to install
9+
10+
```bash
11+
git clone [email protected]:maprihoda/react-redux-crud.git
12+
cd react-redux-crud
13+
```
14+
15+
In terminal 1:
16+
17+
```bash
18+
cd server
19+
yarn install
20+
```
21+
22+
In terminal 2:
23+
24+
```bash
25+
cd client
26+
yarn install
27+
```
28+
29+
## How to run
30+
31+
In terminal 1:
32+
33+
```bash
34+
yarn start
35+
```
36+
37+
This launches a backend API server listening at http://localhost:3001. You can browse the REST API at http://localhost:3001/explorer. To browse the API with Postman, import the server/react-redux-crud.postman_collection.json file.
38+
39+
In terminal 2:
40+
41+
```bash
42+
yarn start
43+
```
44+
45+
You can now view the client SPA in the browser.
46+
47+
Local: http://localhost:3000/
48+
On Your Network: http://10.0.0.32:3000/
49+
50+
51+
## License
52+
53+
The MIT License (MIT)

client/.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# See https://help.github.com/ignore-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
6+
# testing
7+
/coverage
8+
9+
# production
10+
/build
11+
12+
# misc
13+
.DS_Store
14+
.env.local
15+
.env.development.local
16+
.env.test.local
17+
.env.production.local
18+
19+
npm-debug.log*
20+
yarn-debug.log*
21+
yarn-error.log*
22+
23+
src/**/*.css

client/package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "client",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"axios": "^0.17.1",
7+
"font-awesome": "^4.7.0",
8+
"node-sass-chokidar": "^0.0.3",
9+
"normalize.css": "^7.0.0",
10+
"npm-run-all": "^4.1.2",
11+
"react": "^16.1.1",
12+
"react-dom": "^16.1.1",
13+
"react-redux": "^5.0.6",
14+
"react-router-dom": "^4.2.2",
15+
"react-scripts": "1.0.17",
16+
"redux": "^3.7.2",
17+
"redux-saga": "^0.16.0"
18+
},
19+
"scripts": {
20+
"build-css": "node-sass-chokidar src/ -o src/",
21+
"watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive",
22+
"start-js": "react-scripts start",
23+
"start": "npm-run-all -p watch-css start-js",
24+
"build-js": "react-scripts build",
25+
"build": "npm-run-all build-css build-js",
26+
"test": "react-scripts test --env=jsdom",
27+
"eject": "react-scripts eject"
28+
},
29+
"devDependencies": {
30+
"redux-devtools": "^3.4.1",
31+
"redux-devtools-dock-monitor": "^1.1.2",
32+
"redux-devtools-log-monitor": "^1.4.0"
33+
}
34+
}

client/public/favicon.ico

3.78 KB
Binary file not shown.

client/public/index.html

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<meta name="theme-color" content="#000000">
7+
<!--
8+
manifest.json provides metadata used when your web app is added to the
9+
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
10+
-->
11+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
12+
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
13+
<!--
14+
Notice the use of %PUBLIC_URL% in the tags above.
15+
It will be replaced with the URL of the `public` folder during the build.
16+
Only files inside the `public` folder can be referenced from the HTML.
17+
18+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
19+
work correctly both with client-side routing and a non-root public URL.
20+
Learn how to configure a non-root public URL by running `npm run build`.
21+
-->
22+
<title>My blog</title>
23+
</head>
24+
<body>
25+
<noscript>
26+
You need to enable JavaScript to run this app.
27+
</noscript>
28+
<div id="root"></div>
29+
<!--
30+
This HTML file is a template.
31+
If you open it directly in the browser, you will see an empty page.
32+
33+
You can add webfonts, meta tags, or analytics to this file.
34+
The build step will place the bundled scripts into the <body> tag.
35+
36+
To begin the development, run `npm start` or `yarn start`.
37+
To create a production bundle, use `npm run build` or `yarn build`.
38+
-->
39+
</body>
40+
</html>

client/public/manifest.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"short_name": "React App",
3+
"name": "Create React App Sample",
4+
"icons": [
5+
{
6+
"src": "favicon.ico",
7+
"sizes": "64x64 32x32 24x24 16x16",
8+
"type": "image/x-icon"
9+
}
10+
],
11+
"start_url": "./index.html",
12+
"display": "standalone",
13+
"theme_color": "#000000",
14+
"background_color": "#ffffff"
15+
}

client/src/App.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from 'react'
2+
import { Router, Route, Switch, Redirect } from 'react-router-dom'
3+
import history from './services/history'
4+
import AdminPage from './components/AdminPage'
5+
import ErrorPage from './components/ErrorPage'
6+
7+
export default function App() {
8+
return (
9+
<Router history={history}>
10+
<div>
11+
<Switch>
12+
<Route exact path="/" render={() => (
13+
<Redirect to="/admin"/>
14+
)}/>
15+
<Route path="/admin" component={AdminPage}/>
16+
<Route path="/error" component={ErrorPage}/>
17+
<Redirect to="/error"/>
18+
</Switch>
19+
</div>
20+
</Router>
21+
)
22+
}

client/src/App.test.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React from 'react'
2+
import ReactDOM from 'react-dom'
3+
import App from './App'
4+
5+
it('renders without crashing', () => {
6+
const div = document.createElement('div')
7+
ReactDOM.render(<App />, div)
8+
});

client/src/actionTypes.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const FETCH_POSTS = 'FETCH_POSTS'
2+
export const FETCH_POSTS_IF_NEEDED = 'FETCH_POSTS_IF_NEEDED'
3+
export const FETCH_POSTS_PENDING = 'FETCH_POSTS_PENDING'
4+
export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'
5+
export const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE'
6+
7+
export const DELETE_POST = 'DELETE_POST'
8+
export const DELETE_POST_PENDING = 'DELETE_POST_PENDING'
9+
export const DELETE_POST_SUCCESS = 'DELETE_POST_SUCCESS'
10+
export const DELETE_POST_FAILURE = 'DELETE_POST_FAILURE'
11+
12+
export const CREATE_POST = 'CREATE_POST'
13+
export const CREATE_POST_PENDING = 'CREATE_POST_PENDING'
14+
export const CREATE_POST_SUCCESS = 'CREATE_POST_SUCCESS'
15+
export const CREATE_POST_FAILURE = 'CREATE_POST_FAILURE'
16+
17+
export const UPDATE_POST = 'UPDATE_POST'
18+
export const UPDATE_POST_PENDING = 'UPDATE_POST_PENDING'
19+
export const UPDATE_POST_SUCCESS = 'UPDATE_POST_SUCCESS'
20+
export const UPDATE_POST_FAILURE = 'UPDATE_POST_FAILURE'

client/src/components/AdminPage.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react'
2+
import { Route, Switch, Redirect, Link } from 'react-router-dom'
3+
import PostsPage from './posts/PostsPage'
4+
import NewPostPage from './posts/NewPostPage'
5+
import EditPostPage from './posts/EditPostPage'
6+
import PostPage from './posts/PostPage'
7+
8+
export default function AdminPage({ match: { url } }) {
9+
return (
10+
<div>
11+
<div className="header">
12+
<div className="container">
13+
<Link to="/admin" className="header__brand">Admin</Link>
14+
</div>
15+
</div>
16+
17+
<div className="container">
18+
<Switch>
19+
<Route exact path={`${url}`} render={() => (
20+
<Redirect to={`${url}/posts`}/>
21+
)}/>
22+
<Route exact path={`${url}/posts`} component={PostsPage}/>
23+
<Route exact path={`${url}/posts/new`} component={NewPostPage} />
24+
<Route exact path={`${url}/posts/:id`} component={PostPage} />
25+
<Route exact path={`${url}/posts/:id/edit`} component={EditPostPage} />
26+
<Redirect to="/error"/>
27+
</Switch>
28+
</div>
29+
</div>
30+
)
31+
}

client/src/components/DevTools.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react'
2+
import { createDevTools } from 'redux-devtools'
3+
import LogMonitor from 'redux-devtools-log-monitor'
4+
import DockMonitor from 'redux-devtools-dock-monitor'
5+
6+
const DevTools = createDevTools(
7+
<DockMonitor
8+
toggleVisibilityKey="ctrl-h"
9+
changePositionKey="ctrl-q"
10+
defaultIsVisible={true}
11+
>
12+
<LogMonitor theme="tomorrow" />
13+
</DockMonitor>
14+
)
15+
16+
export default DevTools

client/src/components/ErrorPage.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react'
2+
import { Link } from 'react-router-dom'
3+
4+
export default function ErrorPage() {
5+
return (
6+
<div className="container">
7+
<div className="error-page">
8+
<h1>Oops, something went wrong</h1>
9+
<div>Start at <Link to="/">the beginning</Link></div>
10+
</div>
11+
</div>
12+
)
13+
}

client/src/components/PostsList.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { Link } from 'react-router-dom'
3+
4+
export default function PostsList(props) {
5+
const { loading, posts, url, onEditPost, onDeletePost } = props
6+
7+
if (loading) return <p>Loading...</p>
8+
if (posts.length === 0) return <div>No posts.</div>
9+
10+
return (
11+
<ul className="posts">
12+
{
13+
posts.map(post => (
14+
<li className="posts__item" key={post.id}>
15+
<Link
16+
className="posts__title"
17+
to={`${url}/${post.id}`}
18+
>
19+
{post.title}
20+
</Link>
21+
<button
22+
className="btn posts__btn"
23+
onClick={() => onEditPost(post.id)}
24+
title="Edit"
25+
>
26+
<i className="fa fa-pencil-square-o"></i>
27+
</button>
28+
<button
29+
className="btn posts__btn"
30+
onClick={() => onDeletePost(post.id)}
31+
title="Delete"
32+
>
33+
<i className="fa fa-trash-o"></i>
34+
</button>
35+
</li>
36+
))
37+
}
38+
</ul>
39+
)
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'react-redux'
3+
import navigateTo from '../../services/navigation'
4+
import { UPDATE_POST } from '../../actionTypes'
5+
import { selectCurrentPost } from '../../selectors/posts'
6+
import PostForm from './PostForm'
7+
8+
class EditPostPage extends Component {
9+
handleSubmit = (payload) => {
10+
const id = this.props.match.params.id
11+
payload = { ...payload, id }
12+
this.props.dispatch({ type: UPDATE_POST, payload })
13+
navigateTo('/admin/posts')
14+
}
15+
16+
render() {
17+
const { post } = this.props
18+
19+
return (
20+
<div>
21+
<h2>Edit post</h2>
22+
{ post && (
23+
<PostForm post={post} onSubmit={this.handleSubmit}/>
24+
)
25+
}
26+
</div>
27+
)
28+
}
29+
}
30+
31+
export default connect(
32+
(state, ownProps) => {
33+
const post = selectCurrentPost(state, ownProps.match.params.id)
34+
return { post }
35+
}
36+
)(EditPostPage)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'react-redux'
3+
import navigateTo from '../../services/navigation'
4+
import { CREATE_POST } from '../../actionTypes'
5+
import PostForm from './PostForm'
6+
7+
class NewPostPage extends Component {
8+
handleSubmit = (payload) => {
9+
this.props.dispatch({ type: CREATE_POST, payload })
10+
navigateTo('/admin/posts')
11+
}
12+
13+
render() {
14+
return (
15+
<div>
16+
<h2>Create new post</h2>
17+
<PostForm onSubmit={this.handleSubmit}/>
18+
</div>
19+
)
20+
}
21+
}
22+
23+
export default connect()(NewPostPage)

0 commit comments

Comments
 (0)