From 0f2125e72085a0de03841ade65b3823077799786 Mon Sep 17 00:00:00 2001
From: unknown <pawel999@icloud.com>
Date: Tue, 25 May 2021 14:05:53 +0200
Subject: [PATCH] Pin/Delete category

---
 .../Bookmarks/BookmarkTable/BookmarkTable.tsx | 29 ++++++--
 .../components/Bookmarks/Bookmarks.module.css |  9 +++
 client/src/components/Bookmarks/Bookmarks.tsx |  4 +-
 client/src/store/actions/actionTypes.ts       |  6 ++
 client/src/store/actions/bookmark.ts          | 70 +++++++++++++++++++
 client/src/store/reducers/bookmark.ts         | 25 +++++++
 controllers/category.js                       | 18 +++++
 models/associateModels.js                     |  2 +-
 8 files changed, 156 insertions(+), 7 deletions(-)

diff --git a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
index 388b4d8..4387afc 100644
--- a/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
+++ b/client/src/components/Bookmarks/BookmarkTable/BookmarkTable.tsx
@@ -1,5 +1,8 @@
 import { ContentType } from '../Bookmarks';
 import classes from './BookmarkTable.module.css';
+import { connect } from 'react-redux';
+import { pinCategory, deleteCategory } from '../../../store/actions';
+import { KeyboardEvent } from 'react';
 
 import Table from '../../UI/Table/Table';
 import { Bookmark, Category } from '../../../interfaces';
@@ -8,9 +11,25 @@ import Icon from '../../UI/Icons/Icon/Icon';
 interface ComponentProps {
   contentType: ContentType;
   categories: Category[];
+  pinCategory: (category: Category) => void;
+  deleteCategory: (id: number) => void;
 }
 
 const BookmarkTable = (props: ComponentProps): JSX.Element => {
+  const deleteCategoryHandler = (category: Category): void => {
+    const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`);
+
+    if (proceed) {
+      props.deleteCategory(category.id);
+    }
+  }
+
+  const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => {
+    if (e.key === 'Enter') {
+      handler(category);
+    }
+  }
+
   if (props.contentType === ContentType.category) {
     return (
       <Table headers={[
@@ -24,8 +43,8 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
               <td className={classes.TableActions}>
                 <div
                   className={classes.TableAction}
-                  // onClick={() => deleteAppHandler(app)}
-                  // onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
+                  onClick={() => deleteCategoryHandler(category)}
+                  onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
                   tabIndex={0}>
                   <Icon icon='mdiDelete' />
                 </div>
@@ -38,8 +57,8 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
                 </div>
                 <div
                   className={classes.TableAction}
-                  // onClick={() => props.pinApp(app)}
-                  // onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
+                  onClick={() => props.pinCategory(category)}
+                  onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
                   tabIndex={0}>
                   {category.isPinned
                     ? <Icon icon='mdiPinOff' color='var(--color-accent)' />
@@ -100,4 +119,4 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
   }
 }
 
-export default BookmarkTable;
\ No newline at end of file
+export default connect(null, { pinCategory, deleteCategory })(BookmarkTable);
\ No newline at end of file
diff --git a/client/src/components/Bookmarks/Bookmarks.module.css b/client/src/components/Bookmarks/Bookmarks.module.css
index 09810e1..4c17197 100644
--- a/client/src/components/Bookmarks/Bookmarks.module.css
+++ b/client/src/components/Bookmarks/Bookmarks.module.css
@@ -1,4 +1,13 @@
 .ActionsContainer {
   display: flex;
   align-items: center;
+}
+
+.BookmarksMessage {
+  color: var(--color-primary);
+}
+
+.BookmarksMessage a {
+  color: var(--color-accent);
+  font-weight: 600;
 }
\ No newline at end of file
diff --git a/client/src/components/Bookmarks/Bookmarks.tsx b/client/src/components/Bookmarks/Bookmarks.tsx
index ba14b5e..6e39cc3 100644
--- a/client/src/components/Bookmarks/Bookmarks.tsx
+++ b/client/src/components/Bookmarks/Bookmarks.tsx
@@ -99,7 +99,9 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
       {props.loading
         ? <Spinner />
         : (!isInEdit
-          ? <BookmarkGrid categories={props.categories} />
+          ? props.categories.length > 0
+            ? <BookmarkGrid categories={props.categories} />
+            : <p className={classes.BookmarksMessage}>You don't have any bookmarks. You can a new one from <Link to='/bookmarks'>/bookmarks</Link> menu</p>
           : <BookmarkTable contentType={tableContentType} categories={props.categories} />)
       }
     </Container>
diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts
index 153e3fa..db5f344 100644
--- a/client/src/store/actions/actionTypes.ts
+++ b/client/src/store/actions/actionTypes.ts
@@ -11,6 +11,8 @@ import {
   GetCategoriesAction,
   AddCategoryAction,
   AddBookmarkAction,
+  PinCategoryAction,
+  DeleteCategoryAction,
   // Notifications
   CreateNotificationAction,
   ClearNotificationAction
@@ -34,6 +36,8 @@ export enum ActionTypes {
   getCategoriesError = 'GET_CATEGORIES_ERROR',
   addCategory = 'ADD_CATEGORY',
   addBookmark = 'ADD_BOOKMARK',
+  pinCategory = 'PIN_CATEGORY',
+  deleteCategory = 'DELETE_CATEGORY',
   // Notifications
   createNotification = 'CREATE_NOTIFICATION',
   clearNotification = 'CLEAR_NOTIFICATION'
@@ -52,6 +56,8 @@ export type Action =
   GetCategoriesAction<any> |
   AddCategoryAction |
   AddBookmarkAction |
+  PinCategoryAction |
+  DeleteCategoryAction |
   // Notifications
   CreateNotificationAction |
   ClearNotificationAction;
\ No newline at end of file
diff --git a/client/src/store/actions/bookmark.ts b/client/src/store/actions/bookmark.ts
index 7c121dc..933be1b 100644
--- a/client/src/store/actions/bookmark.ts
+++ b/client/src/store/actions/bookmark.ts
@@ -4,6 +4,9 @@ import { ActionTypes } from './actionTypes';
 import { Category, ApiResponse, NewCategory, Bookmark, NewBookmark } from '../../interfaces';
 import { CreateNotificationAction } from './notification';
 
+/**
+ * GET CATEGORIES
+ */
 export interface GetCategoriesAction<T> {
   type: ActionTypes.getCategories | ActionTypes.getCategoriesSuccess | ActionTypes.getCategoriesError;
   payload: T;
@@ -27,6 +30,9 @@ export const getCategories = () => async (dispatch: Dispatch) => {
   }
 }
 
+/**
+ * ADD CATEGORY
+ */
 export interface AddCategoryAction {
   type: ActionTypes.addCategory,
   payload: Category
@@ -53,6 +59,9 @@ export const addCategory = (formData: NewCategory) => async (dispatch: Dispatch)
   }
 }
 
+/**
+ * ADD BOOKMARK
+ */
 export interface AddBookmarkAction {
   type: ActionTypes.addBookmark,
   payload: Bookmark
@@ -77,4 +86,65 @@ export const addBookmark = (formData: NewBookmark) => async (dispatch: Dispatch)
   } catch (err) {
     console.log(err);
   }
+}
+
+/**
+ * PIN CATEGORY
+ */
+export interface PinCategoryAction {
+  type: ActionTypes.pinCategory,
+  payload: Category
+}
+
+export const pinCategory = (category: Category) => async (dispatch: Dispatch) => {
+  try {
+    const { id, isPinned, name } = category;
+    const res = await axios.put<ApiResponse<Category>>(`/api/categories/${id}`, { isPinned: !isPinned });
+
+    const status = isPinned ? 'unpinned from Homescreen' : 'pinned to Homescreen';
+
+    dispatch<CreateNotificationAction>({
+      type: ActionTypes.createNotification,
+      payload: {
+        title: 'Success',
+        message: `Category ${name} ${status}`
+      }
+    })
+
+    dispatch<PinCategoryAction>({
+      type: ActionTypes.pinCategory,
+      payload: res.data.data
+    })
+  } catch (err) {
+    console.log(err);
+  }
+}
+
+/**
+ * DELETE CATEGORY
+ */
+export interface DeleteCategoryAction {
+  type: ActionTypes.deleteCategory,
+  payload: number
+}
+
+export const deleteCategory = (id: number) => async (dispatch: Dispatch) => {
+  try {
+    const res = await axios.delete<ApiResponse<{}>>(`/api/categories/${id}`);
+
+    dispatch<CreateNotificationAction>({
+      type: ActionTypes.createNotification,
+      payload: {
+        title: 'Success',
+        message: `Category deleted`
+      }
+    })
+
+    dispatch<DeleteCategoryAction>({
+      type: ActionTypes.deleteCategory,
+      payload: id
+    })
+  } catch (err) {
+    console.log(err);
+  }
 }
\ No newline at end of file
diff --git a/client/src/store/reducers/bookmark.ts b/client/src/store/reducers/bookmark.ts
index 6c4990f..997d880 100644
--- a/client/src/store/reducers/bookmark.ts
+++ b/client/src/store/reducers/bookmark.ts
@@ -55,12 +55,37 @@ const addBookmark = (state: State, action: Action): State => {
   }
 }
 
+const pinCategory = (state: State, action: Action): State => {
+  const tmpCategories = [...state.categories];
+  const changedCategory = tmpCategories.find((category: Category) => category.id === action.payload.id);
+
+  if (changedCategory) {
+    changedCategory.isPinned = action.payload.isPinned;
+  }
+
+  return {
+    ...state,
+    categories: tmpCategories
+  }
+}
+
+const deleteCategory = (state: State, action: Action): State => {
+  const tmpCategories = [...state.categories].filter((category: Category) => category.id !== action.payload);
+
+  return {
+    ...state,
+    categories: tmpCategories
+  }
+}
+
 const bookmarkReducer = (state = initialState, action: Action) => {
   switch (action.type) {
     case ActionTypes.getCategories: return getCategories(state, action);
     case ActionTypes.getCategoriesSuccess: return getCategoriesSuccess(state, action);
     case ActionTypes.addCategory: return addCategory(state, action);
     case ActionTypes.addBookmark: return addBookmark(state, action);
+    case ActionTypes.pinCategory: return pinCategory(state, action);
+    case ActionTypes.deleteCategory: return deleteCategory(state, action);
     default: return state;
   }
 }
diff --git a/controllers/category.js b/controllers/category.js
index 5ab26df..60e7305 100644
--- a/controllers/category.js
+++ b/controllers/category.js
@@ -79,6 +79,24 @@ exports.updateCategory = asyncWrapper(async (req, res, next) => {
 // @route     DELETE /api/categories/:id
 // @access    Public
 exports.deleteCategory = asyncWrapper(async (req, res, next) => {
+  const category = await Category.findOne({
+    where: { id: req.params.id },
+    include: [{
+      model: Bookmark,
+      as: 'bookmarks'
+    }]
+  });
+
+  if (!category) {
+    return next(new ErrorResponse(`Category with id of ${req.params.id} was not found`, 404))
+  }
+
+  category.bookmarks.forEach(async (bookmark) => {
+    await Bookmark.destroy({
+      where: { id: bookmark.id }
+    })
+  })
+
   await Category.destroy({
     where: { id: req.params.id }
   })
diff --git a/models/associateModels.js b/models/associateModels.js
index 4ca1fab..2457092 100644
--- a/models/associateModels.js
+++ b/models/associateModels.js
@@ -3,11 +3,11 @@ const Bookmark = require('./Bookmark');
 
 const associateModels = () => {
   // Category <> Bookmark
-  Bookmark.belongsTo(Category, { foreignKey: 'categoryId' });
   Category.hasMany(Bookmark, {
     as: 'bookmarks',
     foreignKey: 'categoryId'
   });
+  Bookmark.belongsTo(Category, { foreignKey: 'categoryId' });
 }
 
 module.exports = associateModels;
\ No newline at end of file