Browse Source

Apps actions and reducer

unknown 4 years ago
parent
commit
78acede8ab

+ 8 - 0
client/package-lock.json

@@ -3142,6 +3142,14 @@
       "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.2.0.tgz",
       "integrity": "sha512-1uIESzroqpaTzt9uX48HO+6gfnKu3RwvWdCcWSrX4csMInJfCo1yvKPNXCwXFRpJqRW25tiASb6No0YH57PXqg=="
     },
+    "axios": {
+      "version": "0.21.1",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
+      "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
+      "requires": {
+        "follow-redirects": "^1.10.0"
+      }
+    },
     "axobject-query": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",

+ 1 - 0
client/package.json

@@ -14,6 +14,7 @@
     "@types/react-dom": "^17.0.3",
     "@types/react-redux": "^7.1.16",
     "@types/react-router-dom": "^5.1.7",
+    "axios": "^0.21.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-redux": "^7.2.4",

+ 1 - 0
client/src/App.tsx

@@ -22,6 +22,7 @@ const App = (): JSX.Element => {
         <Switch>
           <Route exact path='/' component={Home} />
           <Route path='/settings' component={Settings} />
+          <Route path='/apps' component={Apps} />
         </Switch>
       </BrowserRouter>
     </Provider>

+ 20 - 4
client/src/components/Apps/AppCard/AppCard.tsx

@@ -1,15 +1,31 @@
 import classes from './AppCard.module.css';
 import Icon from '../../UI/Icon/Icon';
 
-const AppCard = (): JSX.Element => {
+import { App } from '../../../interfaces';
+
+interface ComponentProps {
+  app: App;
+}
+
+const AppCard = (props: ComponentProps): JSX.Element => {
+  const iconParser = (mdiName: string): string => {
+    let parsedName = mdiName
+      .split('-')
+      .map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
+      .join('');
+    parsedName = `mdi${parsedName}`;
+
+    return parsedName;
+  }
+
   return (
     <div className={classes.AppCard}>
       <div className={classes.AppCardIcon}>
-        <Icon icon='mdiBookOpenBlankVariant' />
+        <Icon icon={iconParser(props.app.icon)} />
       </div>
       <div className={classes.AppCardDetails}>
-        <h5>plex</h5>
-        <a href="/">plex.example.com</a>
+        <h5>{props.app.name}</h5>
+        <a href="/">{props.app.url}</a>
       </div>
     </div>
   )

+ 48 - 21
client/src/components/Apps/Apps.tsx

@@ -1,32 +1,59 @@
-import { Fragment } from 'react';
+import { Fragment, useEffect } from 'react';
+import { Link } from 'react-router-dom';
 
+// Redux
+import { connect } from 'react-redux';
+import { getApps } from '../../store/actions';
+
+// Typescript
+import { App } from '../../interfaces';
+
+// CSS
 import classes from './Apps.module.css';
 
+// UI
 import { Container } from '../UI/Layout/Layout';
 import Headline from '../UI/Headlines/Headline/Headline';
+import Spinner from '../UI/Spinner/Spinner';
+
+// Subcomponents
 import AppCard from './AppCard/AppCard';
 
-const Apps = (): JSX.Element => {
+interface ComponentProps {
+  getApps: Function;
+  apps: App[];
+  loading: boolean;
+}
+
+const Apps = (props: ComponentProps): JSX.Element => {
+  useEffect(() => {
+    props.getApps()
+  }, [props.getApps]);
+
   return (
-    <div className={classes.Apps}>
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-      <AppCard />
-    </div>
+    <Container>
+      <Headline
+        title='Pinned Apps'
+        subtitle={<Link to='/'>Go back</Link>}
+      />
+      <Headline title='All Apps' />
+      <div className={classes.Apps}>
+        {props.loading
+          ? 'loading'
+          : props.apps.map((app: App): JSX.Element => {
+            return <AppCard key={app.id} app={app} />
+          })
+        }
+      </div>
+    </Container>
   )
 }
 
-export default Apps;
+const mapStateToProps = (state: any) => {
+  return {
+    apps: state.app.apps,
+    loading: state.app.loading
+  }
+}
+
+export default connect(mapStateToProps, { getApps })(Apps);

+ 54 - 0
client/src/components/UI/Spinner/Spinner.module.css

@@ -0,0 +1,54 @@
+.Spinner,
+.Spinner:before,
+.Spinner:after {
+  background: var(--color-primary);
+  animation: load1 1s infinite ease-in-out;
+  width: 1em;
+  height: 4em;
+}
+
+.Spinner {
+  color: var(--color-primary);
+  text-indent: -9999em;
+  margin: 88px auto;
+  position: relative;
+  font-size: 11px;
+  transform: translateZ(0);
+  animation-delay: -0.16s;
+}
+
+.Spinner:before,
+.Spinner:after {
+  position: absolute;
+  top: 0;
+  content: '';
+}
+
+.Spinner:before {
+  left: -1.5em;
+  animation-delay: -0.32s;
+}
+
+.Spinner:after {
+  left: 1.5em;
+}
+
+@keyframes load1 {
+  0%,
+  80%,
+  100% {
+    box-shadow: 0 0;
+    height: 4em;
+  }
+  40% {
+    box-shadow: 0 -2em;
+    height: 5em;
+  }
+}
+
+.SpinnerWrapper {
+  height: 150px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

+ 11 - 0
client/src/components/UI/Spinner/Spinner.tsx

@@ -0,0 +1,11 @@
+import classes from './Spinner.module.css';
+
+const Spinner = (): JSX.Element => {
+  return (
+    <div className={classes.SpinnerWrapper}>
+      <div className={classes.Spinner}>Loading...</div>
+    </div>
+  )
+}
+
+export default Spinner;

+ 2 - 1
client/src/index.css

@@ -2,7 +2,7 @@
   margin: 0;
   padding: 0;
   box-sizing: border-box;
-  transition: all 0.3s;
+  /* transition: all 0.3s; */
   user-select: none;
 }
 
@@ -12,6 +12,7 @@ body {
   --color-accent: #6677EB;
 
   background-color: var(--color-background);
+  transition: background-color 0.3s;
   /* font weights
     light 300
     regular 400

+ 8 - 0
client/src/interfaces/App.ts

@@ -0,0 +1,8 @@
+export interface App {
+  id: number;
+  name: string;
+  url: string;
+  icon: string;
+  createdAt: Date;
+  updatedAt: Date;
+}

+ 2 - 0
client/src/interfaces/index.ts

@@ -0,0 +1,2 @@
+export * from './App';
+export * from './Theme';

+ 4 - 0
client/src/store/actions/actionTypes.ts

@@ -1 +1,5 @@
 export const SET_THEME = 'SET_THEME';
+
+export const GET_APPS = 'GET_APPS';
+export const GET_APPS_SUCCESS = 'GET_APPS_SUCCESS';
+export const GET_APPS_ERROR = 'GET_APPS_ERROR';

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

@@ -0,0 +1,25 @@
+import axios from 'axios';
+import { Dispatch } from 'redux';
+import {
+  GET_APPS,
+  GET_APPS_SUCCESS,
+  GET_APPS_ERROR
+} from './actionTypes';
+
+export const getApps = () => async (dispatch: Dispatch) => {
+  dispatch({ type: GET_APPS });
+
+  try {
+    const res = await axios.get('/api/apps');
+
+    dispatch({
+      type: GET_APPS_SUCCESS,
+      payload: res.data.data
+    })
+  } catch (err) {
+    dispatch({
+      type: GET_APPS_ERROR,
+      payload: err.data.data
+    })
+  }
+}

+ 1 - 0
client/src/store/actions/index.ts

@@ -1,2 +1,3 @@
 export * from './theme';
+export * from './app';
 export * from './actionTypes';

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

@@ -0,0 +1,42 @@
+import {
+  GET_APPS,
+  GET_APPS_SUCCESS,
+  GET_APPS_ERROR
+} from '../actions/actionTypes';
+
+import { App } from '../../interfaces/App';
+
+interface State {
+  loading: boolean;
+  apps: App[];
+}
+
+const initialState: State = {
+  loading: true,
+  apps: []
+}
+
+const getApps = (state: State, action: any): State => {
+  return {
+    ...state,
+    loading: true
+  }
+}
+
+const getAppsSuccess = (state: State, action: any): State => {
+  return {
+    ...state,
+    loading: false,
+    apps: action.payload
+  }
+}
+
+const appReducer = (state = initialState, action: any) => {
+  switch (action.type) {
+    case GET_APPS: return getApps(state, action);
+    case GET_APPS_SUCCESS: return getAppsSuccess(state, action);
+    default: return state;
+  }
+}
+
+export default appReducer;

+ 3 - 1
client/src/store/reducers/index.ts

@@ -1,9 +1,11 @@
 import { combineReducers } from 'redux';
 
 import themeReducer from './theme';
+import appReducer from './app';
 
 const rootReducer = combineReducers({
-  theme: themeReducer
+  theme: themeReducer,
+  app: appReducer
 })
 
 export default rootReducer;