Cleaned up Apps component. Delete App redux action. Apps edit mode with functionality do delete and pin apps
This commit is contained in:
parent
7e540587a5
commit
cb0b4b495f
10 changed files with 225 additions and 52 deletions
|
@ -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
client/src/components/Apps/AppTable/AppTable.module.css
Normal file
62
client/src/components/Apps/AppTable/AppTable.module.css
Normal file
|
@ -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
client/src/components/Apps/AppTable/AppTable.tsx
Normal file
67
client/src/components/Apps/AppTable/AppTable.tsx
Normal file
|
@ -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);
|
|
@ -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 />)
|
||||
|
|
|
@ -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,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)'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue