diff --git a/app/Api/Controllers/PostController.php b/app/Api/Controllers/PostController.php
new file mode 100644
index 0000000..9a39d1a
--- /dev/null
+++ b/app/Api/Controllers/PostController.php
@@ -0,0 +1,118 @@
+response = $response;
+ $this->repository = $repository;
+ }
+
+ /**
+ * Display a listing of the resource.
+ *
+ * @return \Illuminate\Http\Response
+ */
+ public function index()
+ {
+ /**
+ * We set skip presenter by default to work directly with the
+ * repository instead of the transformed result in the code, then
+ * when it comes to displaying an API result to the user, we apply
+ * the presenter. More on presenters and transformers here:
+ * https://github.com/andersao/l5-repository#presenters
+ */
+ try {
+ $posts = $this->repository->skipPresenter(false)->all();
+
+ return $this->response->success($posts);
+ } catch (ValidationException $e) {
+ return $this->response->validateError($e->errors());
+ }
+ }
+
+ /**
+ * Store a newly created resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @return \Illuminate\Http\Response
+ */
+ public function store(Request $request)
+ {
+ $postData = $request->only(['title', 'body']);
+
+ $createdPost = $this->repository
+ ->skipPresenter(false)
+ ->create($postData);
+
+ return $this->response->success($createdPost);
+ }
+
+ /**
+ * Display the specified resource.
+ *
+ * @param string $slug
+ * @return \Illuminate\Http\Response
+ */
+ public function show($slug)
+ {
+ $id = $this->repository->decodeSlug($slug);
+ $post = $this->repository->skipPresenter(false)->find($id);
+
+ return $this->response->success($post);
+ }
+
+ /**
+ * Update the specified resource in storage.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param string $slug
+ * @return \Illuminate\Http\Response
+ */
+ public function update(Request $request, $slug)
+ {
+ try {
+ $data = $request->only(['title', 'body']);
+ $id = $this->repository->decodeSlug($slug);
+
+ $updatedPost = $this->repository->skipPresenter(false)->update($data, $id);
+
+ return $this->response->success($updatedPost);
+ } catch (ValidationException $e) {
+ return $this->response->validateError($e->errors());
+ }
+ }
+
+ /**
+ * Remove the specified resource from storage.
+ *
+ * @param int $id
+ * @return \Illuminate\Http\Response
+ */
+ public function destroy($slug)
+ {
+ $id = $this->repository->decodeSlug($slug);
+
+ $deletedPost = $this->repository->skipPresenter(false)->delete($id);
+
+ return $this->response->success(['message' => 'post deleted']);
+ }
+}
diff --git a/app/Contracts/Repository/PostRepositoryContract.php b/app/Contracts/Repository/PostRepositoryContract.php
new file mode 100644
index 0000000..9d25301
--- /dev/null
+++ b/app/Contracts/Repository/PostRepositoryContract.php
@@ -0,0 +1,9 @@
+app->bind(
+ 'App\Contracts\Repository\PostRepositoryContract',
+ 'App\Repositories\Eloquent\PostRepository'
+ );
}
}
diff --git a/app/Repositories/Eloquent/PostRepository.php b/app/Repositories/Eloquent/PostRepository.php
new file mode 100644
index 0000000..3a7d789
--- /dev/null
+++ b/app/Repositories/Eloquent/PostRepository.php
@@ -0,0 +1,29 @@
+ $model->slug(),
+ 'title' => $model->title,
+ 'body' => $model->body
+ ];
+ }
+}
diff --git a/app/Validators/PostValidator.php b/app/Validators/PostValidator.php
new file mode 100644
index 0000000..2d20661
--- /dev/null
+++ b/app/Validators/PostValidator.php
@@ -0,0 +1,12 @@
+ 'required',
+ 'body' => 'required',
+ ];
+}
\ No newline at end of file
diff --git a/database/migrations/2019_08_18_093140_create_posts_table.php b/database/migrations/2019_08_18_093140_create_posts_table.php
new file mode 100644
index 0000000..eb28920
--- /dev/null
+++ b/database/migrations/2019_08_18_093140_create_posts_table.php
@@ -0,0 +1,33 @@
+bigIncrements('id');
+ $table->string('title');
+ $table->string('body');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('posts');
+ }
+}
diff --git a/resources/assets/js/pages/Overview/Overview.jsx b/resources/assets/js/pages/Overview/Overview.jsx
index eb5cb82..1c4a1a0 100644
--- a/resources/assets/js/pages/Overview/Overview.jsx
+++ b/resources/assets/js/pages/Overview/Overview.jsx
@@ -1,13 +1,147 @@
-import React from 'react'
+import { compose } from 'recompose'
+import { reduxForm, Field } from 'redux-form'
+import { connect } from 'react-redux'
+import React, { useEffect } from 'react'
-import { NeutralButton } from 'components'
import { ModalConsumer } from 'contexts'
+import { NeutralButton, TextInput, TextArea } from 'components'
+import { selectAllPosts } from 'store/selectors/posts'
+import {
+ getPosts as getPostsAction,
+ updatePost as updatePostAction,
+ createPost as createPostAction,
+ deletePost as deletePostAction
+} from 'store/action-creators/posts'
-const OverviewComponent = props => {
+const PostModalComponent = ({ onSubmit, handleSubmit, initialValues }) => {
+ return (
+
+
{initialValues ? 'Edit' : 'Add'} post
+
+
+ )
+}
+
+const CreatePostModal = compose(
+ connect(
+ null,
+ (dispatch, { hideModal }) => ({
+ onSubmit: values => {
+ dispatch(createPostAction(values))
+ hideModal()
+ }
+ })
+ ),
+ reduxForm({
+ form: 'add-post'
+ })
+)(PostModalComponent)
+
+const UpdatePostModal = compose(
+ connect(
+ (state, ownProps) => ({
+ initialValues: ownProps
+ }),
+ (dispatch, { hideModal }) => ({
+ onSubmit: values => {
+ dispatch(updatePostAction(values))
+ hideModal()
+ }
+ })
+ ),
+ reduxForm({
+ form: 'update-post'
+ })
+)(PostModalComponent)
+
+const OverviewComponent = ({ getPosts, deletePost, posts }) => {
const ModalExample = props => {props.message}
+
+ const populatePosts = async () => {
+ await getPosts()
+ }
+
+ useEffect(() => {
+ populatePosts()
+ }, [])
+
return (
- Put your initial dashboard page here
+ Put your initial dashboard page here. This branch contains a CRUD example
+ using a "Post" as a dummy example model. You can play around with that
+ below and read through the code on{' '}
+
+ This branch
+
+
+ {posts.length > 0 && (
+
+
+
+ slug |
+ title |
+ body |
+
+
+
+ {posts.map(({ slug, title, body }) => (
+
+ {slug} |
+ {title} |
+ {body} |
+
+ deletePost(slug)}
+ >
+ delete
+
+
+ {({ showModal }) => (
+
+ showModal(UpdatePostModal, { slug, title, body })
+ }
+ className="text-green"
+ >
+ edit
+
+ )}
+
+ |
+
+ ))}
+
+
+ )}
+
+
+ {({ showModal }) => (
+ showModal(CreatePostModal)}>
+ Create Post
+
+ )}
+
+
{({ showModal }) => (
@@ -27,4 +161,12 @@ const OverviewComponent = props => {
)
}
-export default OverviewComponent
+export default connect(
+ state => ({
+ posts: selectAllPosts(state)
+ }),
+ dispatch => ({
+ getPosts: () => dispatch(getPostsAction()),
+ deletePost: id => dispatch(deletePostAction(id))
+ })
+)(OverviewComponent)
diff --git a/resources/assets/js/store/action-creators/posts/index.js b/resources/assets/js/store/action-creators/posts/index.js
new file mode 100644
index 0000000..afe265b
--- /dev/null
+++ b/resources/assets/js/store/action-creators/posts/index.js
@@ -0,0 +1 @@
+export { getPosts, createPost, updatePost, deletePost } from './posts'
diff --git a/resources/assets/js/store/action-creators/posts/posts.js b/resources/assets/js/store/action-creators/posts/posts.js
new file mode 100644
index 0000000..a885117
--- /dev/null
+++ b/resources/assets/js/store/action-creators/posts/posts.js
@@ -0,0 +1,50 @@
+import axios from 'axios'
+
+import { postActions as actions } from 'store/actions'
+import { makeRequest } from 'store/action-creators/requests'
+
+export const getPosts = () => async dispatch => {
+ const response = await dispatch(
+ makeRequest('get-posts', () => axios.get(`/api/posts`))
+ )
+
+ dispatch({
+ type: actions.ADD_POSTS,
+ posts: response.data.data
+ })
+}
+
+export const createPost = data => async dispatch => {
+ const response = await dispatch(
+ makeRequest('create-post', () => axios.post(`/api/posts`, data))
+ )
+
+ dispatch({
+ type: actions.ADD_POST,
+ posts: response.data.data
+ })
+}
+
+export const updatePost = data => async dispatch => {
+ const response = await dispatch(
+ makeRequest(`update-post-${data.slug}`, () =>
+ axios.put(`/api/posts/${data.slug}`, data)
+ )
+ )
+
+ dispatch({
+ type: actions.UPDATE_POST,
+ posts: response.data.data
+ })
+}
+
+export const deletePost = slug => async dispatch => {
+ await dispatch(
+ makeRequest(`update-post-${slug}`, () => axios.delete(`/api/posts/${slug}`))
+ )
+
+ dispatch({
+ type: actions.DELETE_POST,
+ slug
+ })
+}
diff --git a/resources/assets/js/store/actions.js b/resources/assets/js/store/actions.js
index a415f96..2b3b3ad 100644
--- a/resources/assets/js/store/actions.js
+++ b/resources/assets/js/store/actions.js
@@ -17,3 +17,10 @@ export const userActions = {
SET_CURRENT_USER_INFO: 'USER/SET_CURRENT_USER_INFO',
SET_AVATAR: 'USER/SET_AVATAR'
}
+
+export const postActions = {
+ ADD_POST: 'ADD_POST',
+ UPDATE_POST: 'UPDATE_POST',
+ DELETE_POST: 'DELETE_POST',
+ ADD_POSTS: 'ADD_POSTS'
+}
diff --git a/resources/assets/js/store/initialState.js b/resources/assets/js/store/initialState.js
index 927a45c..fe96c98 100644
--- a/resources/assets/js/store/initialState.js
+++ b/resources/assets/js/store/initialState.js
@@ -1,6 +1,7 @@
export const initialState = {
entities: {
- users: {}
+ users: {},
+ posts: {}
},
session: {
currentUser: null
diff --git a/resources/assets/js/store/reducers/entities/entities.reducer.js b/resources/assets/js/store/reducers/entities/entities.reducer.js
index ab3b788..35d99ae 100644
--- a/resources/assets/js/store/reducers/entities/entities.reducer.js
+++ b/resources/assets/js/store/reducers/entities/entities.reducer.js
@@ -5,11 +5,13 @@ import { initialState } from 'store/initialState'
import { createReducer } from 'store/reducers/utilities'
import { usersReducer } from './users.reducer'
+import { postsReducer } from './posts.reducer'
const { entities } = initialState
const singleEntitiesReducer = combineReducers({
- users: usersReducer
+ users: usersReducer,
+ posts: postsReducer
})
const wholeEntitiesReducer = createReducer(entities, {})
diff --git a/resources/assets/js/store/reducers/entities/posts.reducer.js b/resources/assets/js/store/reducers/entities/posts.reducer.js
new file mode 100644
index 0000000..bd7c811
--- /dev/null
+++ b/resources/assets/js/store/reducers/entities/posts.reducer.js
@@ -0,0 +1,27 @@
+import { postActions } from 'store/actions'
+import { initialState } from 'store/initialState'
+import { post as postSchema } from 'store/schemas'
+import { createReducer, normalizeAndMerge } from 'store/reducers/utilities'
+
+const {
+ entities: { posts: postsState }
+} = initialState
+
+const deletePost = (state, { slug }) => {
+ const newState = { ...state }
+
+ delete newState[slug]
+
+ return newState
+}
+
+export const postsReducer = createReducer(postsState, {
+ [postActions.ADD_POST]: normalizeAndMerge('posts', postSchema, {
+ singular: true
+ }),
+ [postActions.ADD_POSTS]: normalizeAndMerge('posts', postSchema),
+ [postActions.UPDATE_POST]: normalizeAndMerge('posts', postSchema, {
+ singular: true
+ }),
+ [postActions.DELETE_POST]: deletePost
+})
diff --git a/resources/assets/js/store/schemas.js b/resources/assets/js/store/schemas.js
index b2a756c..94cae14 100644
--- a/resources/assets/js/store/schemas.js
+++ b/resources/assets/js/store/schemas.js
@@ -1,7 +1,9 @@
import { schema } from 'normalizr'
export const user = new schema.Entity('users', {}, { idAttribute: 'slug' })
+export const post = new schema.Entity('posts', {}, { idAttribute: 'slug' })
export const entities = {
- users: [user]
+ users: [user],
+ posts: [post]
}
diff --git a/resources/assets/js/store/selectors/posts.js b/resources/assets/js/store/selectors/posts.js
new file mode 100644
index 0000000..c48dbf3
--- /dev/null
+++ b/resources/assets/js/store/selectors/posts.js
@@ -0,0 +1,13 @@
+import { denormalize } from 'normalizr'
+
+import { entities as entitiesSchema } from 'store/schemas'
+
+export const selectAllPosts = state => {
+ const dnEntities = denormalize(
+ { posts: Object.keys(state.entities.posts) },
+ entitiesSchema,
+ state.entities
+ )
+
+ return dnEntities.posts
+}
diff --git a/routes/api.php b/routes/api.php
index a7618d4..f6306e7 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -9,6 +9,8 @@
Route::apiResource('/users', '\App\Api\Controllers\UserController');
Route::put('/users/{slug}/update-password', '\App\Api\Controllers\UserController@changePassword');
+ Route::apiResource('/posts', '\App\Api\Controllers\PostController');
+
Route::get('/avatars', '\App\Api\Controllers\AvatarsController@get');
Route::post('/avatars', '\App\Api\Controllers\AvatarsController@upload');
Route::put('/avatars', '\App\Api\Controllers\AvatarsController@update');