Bläddra i källkod

Cleaned up Apps component. Delete App redux action. Apps edit mode with functionality do delete and pin apps

unknown 4 år sedan
förälder
incheckning
cb0b4b495f

+ 2 - 2
client/src/App.tsx

@@ -12,7 +12,7 @@ import Apps from './components/Apps/Apps';
 import Settings from './components/Settings/Settings';
 
 if (localStorage.theme) {
-  store.dispatch(setTheme(localStorage.theme));
+  store.dispatch<any>(setTheme(localStorage.theme));
 }
 
 const App = (): JSX.Element => {
@@ -29,4 +29,4 @@ const App = (): JSX.Element => {
   );
 }
 
-export default App;
+export default App;

+ 62 - 0
client/src/components/Apps/AppTable/AppTable.module.css

@@ -0,0 +1,62 @@
+.TableContainer {
+  width: 100%;
+}
+
+.Table {
+  border-collapse: collapse;
+  width: 100%;
+  text-align: left;
+  font-size: 16px;
+  color: var(--color-primary);
+}
+
+.Table th,
+.Table td {
+  /* border: 1px solid orange; */
+  padding: 10px;
+}
+
+/* Head */
+
+.Table th {
+  --header-radius: 4px;
+  background-color: var(--color-primary);
+  color: var(--color-background);
+}
+
+.Table th:first-child {
+  border-top-left-radius: var(--header-radius);
+  border-bottom-left-radius: var(--header-radius);
+}
+
+.Table th:last-child {
+  border-top-right-radius: var(--header-radius);
+  border-bottom-right-radius: var(--header-radius);
+}
+
+/* Body */
+
+.Table td {
+  /* opacity: 0.5; */
+  transition: all 0.2s;
+}
+
+/* .Table td:hover {
+  opacity: 1;
+} */
+
+/* Actions */
+
+.TableActions {
+  display: flex;
+  align-items: center;
+}
+
+
+.TableAction {
+  width: 22px;
+}
+
+.TableAction:hover {
+  cursor: pointer;
+}

+ 67 - 0
client/src/components/Apps/AppTable/AppTable.tsx

@@ -0,0 +1,67 @@
+import { connect } from 'react-redux';
+import { App, GlobalState } from '../../../interfaces';
+import { pinApp, deleteApp } from '../../../store/actions';
+
+import classes from './AppTable.module.css';
+import Icon from '../../UI/Icon/Icon';
+
+interface ComponentProps {
+  apps: App[];
+  pinApp: (id: number, isPinned: boolean) => void;
+  deleteApp: (id: number) => void;
+}
+
+const AppTable = (props: ComponentProps): JSX.Element => {
+  const deleteAppHandler = (app: App): void => {
+    const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
+
+    if (proceed) {
+      props.deleteApp(app.id);
+    }
+  }
+
+  return (
+    <div className={classes.TableContainer}>
+      <table className={classes.Table}>
+        <thead className={classes.TableHead}>
+          <tr>
+            <th>Name</th>
+            <th>Url</th>
+            <th>Icon</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+        <tbody className={classes.TableBody}>
+          {props.apps.map((app: App): JSX.Element => {
+            return (
+              <tr key={app.id}>
+                <td>{app.name}</td>
+                <td>{app.url}</td>
+                <td>{app.icon}</td>
+                <td className={classes.TableActions}>
+                  <div
+                    className={classes.TableAction}
+                    onClick={() => deleteAppHandler(app)}>
+                    <Icon icon='mdiDelete' />
+                  </div>
+                  <div className={classes.TableAction}><Icon icon='mdiPencil' /></div>
+                  <div className={classes.TableAction} onClick={() => props.pinApp(app.id, app.isPinned)}>
+                    {app.isPinned? <Icon icon='mdiPinOff' color='var(--color-accent)' /> : <Icon icon='mdiPin' />}
+                  </div>
+                </td>
+              </tr>
+            )
+          })}
+        </tbody>
+      </table>
+    </div>
+  )
+}
+
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    apps: state.app.apps
+  }
+}
+
+export default connect(mapStateToProps, { pinApp, deleteApp })(AppTable);

+ 4 - 14
client/src/components/Apps/Apps.tsx

@@ -1,4 +1,4 @@
-import { Fragment, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
 import { Link } from 'react-router-dom';
 
 // Redux
@@ -22,12 +22,11 @@ import Modal from '../UI/Modal/Modal';
 import AppGrid from './AppGrid/AppGrid';
 import AppForm from './AppForm/AppForm';
 import AppTable from './AppTable/AppTable';
-import Test from '../Test';
 
 interface ComponentProps {
   getApps: Function;
-  pinApp: (id: number, isPinned: boolean) => any;
-  addApp: (formData: NewApp) => any;
+  pinApp: (id: number, isPinned: boolean) => void;
+  addApp: (formData: NewApp) => void;
   apps: App[];
   loading: boolean;
 }
@@ -38,17 +37,8 @@ const Apps = (props: ComponentProps): JSX.Element => {
 
   useEffect(() => {
     props.getApps();
-    // props.addApp({
-    //   name: 'Plex',
-    //   url: '192.168.0.128',
-    //   icon: 'cat'
-    // })
   }, [props.getApps]);
 
-  const pinAppHandler = (id: number, state: boolean): void => {
-    props.pinApp(id, state);
-  }
-
   const toggleModal = (): void => {
     setModalIsOpen(!modalIsOpen);
   }
@@ -83,7 +73,7 @@ const Apps = (props: ComponentProps): JSX.Element => {
 
       <div className={classes.Apps}>
         {props.loading
-          ? 'loading'
+          ? <Spinner />
           : (!isInEdit
               ? <AppGrid apps={props.apps} />
               : <AppTable />)

+ 50 - 31
client/src/components/Home/Home.tsx

@@ -1,55 +1,64 @@
+import { useEffect } from 'react';
 import { Link } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { GlobalState } from '../../interfaces/GlobalState';
+import { getApps } from '../../store/actions';
+
 import Icon from '../UI/Icon/Icon';
+import Modal from '../UI/Modal/Modal';
 
 import classes from './Home.module.css';
 import { Container } from '../UI/Layout/Layout';
 import Headline from '../UI/Headlines/Headline/Headline';
 import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline';
 import Apps from '../Apps/Apps';
+import AppGrid from '../Apps/AppGrid/AppGrid';
+import { App } from '../../interfaces';
+import Spinner from '../UI/Spinner/Spinner';
+
+interface ComponentProps {
+  getApps: Function;
+  loading: boolean;
+  apps: App[];
+}
+
+const Home = (props: ComponentProps): JSX.Element => {
+  useEffect(() => {
+    props.getApps();
+  }, [props.getApps]);
 
-const Home = (): JSX.Element => {
   const dateAndTime = (): string => {
     const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
     const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
 
     const now = new Date();
 
-    return `${days[now.getDay()]}, ${now.getDate()} of ${months[now.getMonth()]} ${now.getFullYear()}`;
+    const ordinal = (day: number): string => {
+      if (day > 3 && day < 21) return 'th';
+      switch (day % 10) {
+        case 1:  return "st";
+        case 2:  return "nd";
+        case 3:  return "rd";
+        default: return "th";
+      }
+    }
+
+    return `${days[now.getDay()]}, ${now.getDate()}${ordinal(now.getDate())} ${months[now.getMonth()]} ${now.getFullYear()}`;
   }
 
   const greeter = (): string => {
     const now = new Date().getHours();
     let msg: string;
 
-    if (now > 18) {
-      msg = 'Good evening!';
-    } else if (now > 12) {
-      msg = 'Good afternoon!';
-    } else if (now > 6) {
-      msg = 'Good morning!';
-    } else if (now > 0) {
-      msg = 'Good night!';
-    } else {
-      msg = 'Hello!';
-    }
+    if (now > 18) msg = 'Good evening!';
+    else if (now > 12) msg = 'Good afternoon!';
+    else if (now > 6) msg = 'Good morning!';
+    else if (now > 0) msg = 'Good night!';
+    else msg = 'Hello!';
 
     return msg;
   }
 
-  (() => {
-    const mdiName = 'book-open-blank-variant';
-    const expected = 'mdiBookOpenBlankVariant';
-
-    let parsedName = mdiName
-      .split('-')
-      .map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
-      .join('');
-    parsedName = `mdi${parsedName}`;
-    
-    console.log(parsedName);
-    console.log(parsedName === expected);
-  })();
-
   return (
     <Container>
       <header className={classes.Header}>
@@ -57,10 +66,13 @@ const Home = (): JSX.Element => {
         <h1>{greeter()}</h1>
       </header>
 
-      <SectionHeadline title='Apps' />
-      <Apps />
+      <SectionHeadline title='Apps' link='/apps' />
+      {props.loading
+        ? <Spinner />
+        : <AppGrid apps={props.apps.filter((app: App) => app.isPinned)} />
+      }
 
-      <SectionHeadline title='Bookmarks' />
+      <SectionHeadline title='Bookmarks' link='/bookmarks' />
 
       <Link to='/settings' className={classes.SettingsButton}>
         <Icon icon='mdiCog' />
@@ -69,4 +81,11 @@ const Home = (): JSX.Element => {
   )
 }
 
-export default Home;
+const mapStateToProps = (state: GlobalState) => {
+  return {
+    loading: state.app.loading,
+    apps: state.app.apps
+  }
+}
+
+export default connect(mapStateToProps, { getApps })(Home);

+ 4 - 1
client/src/components/UI/Icon/Icon.tsx

@@ -4,6 +4,7 @@ import { Icon as MDIcon } from '@mdi/react';
 
 interface ComponentProps {
   icon: string;
+  color?: string;
 }
 
 const Icon = (props: ComponentProps): JSX.Element => {
@@ -16,8 +17,10 @@ const Icon = (props: ComponentProps): JSX.Element => {
   }
 
   return (
-    <MDIcon className={classes.Icon}
+    <MDIcon
+      className={classes.Icon}
       path={iconPath}
+      color={props.color ? props.color : 'var(--color-primary)'}
     />
   )
 }

+ 5 - 3
client/src/store/actions/actionTypes.ts

@@ -2,7 +2,8 @@ import {
   GetAppsAction,
   SetTheme,
   PinAppAction,
-  AddAppAction
+  AddAppAction,
+  DeleteAppAction
 } from './';
 
 export enum ActionTypes {
@@ -12,7 +13,8 @@ export enum ActionTypes {
   getAppsError = 'GET_APPS_ERROR',
   pinApp = 'PIN_APP',
   addApp = 'ADD_APP',
-  addAppSuccess = 'ADD_APP_SUCCESS'
+  addAppSuccess = 'ADD_APP_SUCCESS',
+  deleteApp = 'DELETE_APP'
 }
 
-export type Action = GetAppsAction<any> | SetTheme | PinAppAction | AddAppAction;
+export type Action = GetAppsAction<any> | SetTheme | PinAppAction | AddAppAction | DeleteAppAction;

+ 18 - 0
client/src/store/actions/app.ts

@@ -63,4 +63,22 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => {
   } catch (err) {
     console.log(err);
   }
+}
+
+export interface DeleteAppAction {
+  type: ActionTypes.deleteApp,
+  payload: number
+}
+
+export const deleteApp = (id: number) => async (dispatch: Dispatch) => {
+  try {
+    const res = await axios.delete<AppResponse<{}>>(`/api/apps/${id}`);
+
+    dispatch<DeleteAppAction>({
+      type: ActionTypes.deleteApp,
+      payload: id
+    })
+  } catch (err) {
+    console.log(err);
+  }
 }

+ 10 - 0
client/src/store/reducers/app.ts

@@ -60,6 +60,15 @@ const addAppSuccess = (state: State, action: Action): State => {
   }
 }
 
+const deleteApp = (state: State, action: Action): State => {
+  const tmpApps = [...state.apps].filter((app: App) => app.id !== action.payload);
+
+  return {
+    ...state,
+    apps: tmpApps
+  }
+}
+
 const appReducer = (state = initialState, action: Action) => {
   switch (action.type) {
     case ActionTypes.getApps: return getApps(state, action);
@@ -67,6 +76,7 @@ const appReducer = (state = initialState, action: Action) => {
     case ActionTypes.getAppsError: return getAppsError(state, action);
     case ActionTypes.pinApp: return pinApp(state, action);
     case ActionTypes.addAppSuccess: return addAppSuccess(state, action);
+    case ActionTypes.deleteApp: return deleteApp(state, action);
     default: return state;
   }
 }

+ 3 - 1
controllers/apps.js

@@ -18,7 +18,9 @@ exports.createApp = asyncWrapper(async (req, res, next) => {
 // @route     GET /api/apps
 // @access    Public
 exports.getApps = asyncWrapper(async (req, res, next) => {
-  const apps = await App.findAll();
+  const apps = await App.findAll({
+    order: [['name', 'ASC']]
+  });
 
   res.status(200).json({
     success: true,