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

+
+ + + + {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 && ( + + + + + + + + + + {posts.map(({ slug, title, body }) => ( + + + + + + + ))} + +
slugtitlebody
{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');