Components: refactored rest of the components to use new state. Minor changes to exports, imports and props

This commit is contained in:
Paweł Malak 2021-11-09 14:33:51 +01:00
parent 89d935e27f
commit 969bdb7d24
29 changed files with 462 additions and 733 deletions

View File

@ -1,38 +1,41 @@
import { BrowserRouter, Route, Switch } from 'react-router-dom'; import { BrowserRouter, Route, Switch } from 'react-router-dom';
import { fetchQueries, getConfig, setTheme } from './store/actions'; import { actionCreators } from './store';
import 'external-svg-loader'; import 'external-svg-loader';
// Redux
import { store } from './store/store';
import { Provider } from 'react-redux';
// Utils // Utils
import { checkVersion } from './utility'; import { checkVersion } from './utility';
// Routes // Routes
import Home from './components/Home/Home'; import { Home } from './components/Home/Home';
import Apps from './components/Apps/Apps'; import { Apps } from './components/Apps/Apps';
import Settings from './components/Settings/Settings'; import { Settings } from './components/Settings/Settings';
import Bookmarks from './components/Bookmarks/Bookmarks'; import { Bookmarks } from './components/Bookmarks/Bookmarks';
import NotificationCenter from './components/NotificationCenter/NotificationCenter'; import { NotificationCenter } from './components/NotificationCenter/NotificationCenter';
import { useDispatch } from 'react-redux';
// Load config import { bindActionCreators } from 'redux';
store.dispatch<any>(getConfig()); import { useEffect } from 'react';
// Set theme
if (localStorage.theme) {
store.dispatch<any>(setTheme(localStorage.theme));
}
// Check for updates
checkVersion();
// fetch queries
store.dispatch<any>(fetchQueries());
const App = (): JSX.Element => { const App = (): JSX.Element => {
const dispath = useDispatch();
const { fetchQueries, getConfig, setTheme } = bindActionCreators(
actionCreators,
dispath
);
useEffect(() => {
getConfig();
if (localStorage.theme) {
setTheme(localStorage.theme);
}
checkVersion();
fetchQueries();
}, []);
return ( return (
<Provider store={store}> <>
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
<Route exact path="/" component={Home} /> <Route exact path="/" component={Home} />
@ -42,7 +45,7 @@ const App = (): JSX.Element => {
</Switch> </Switch>
</BrowserRouter> </BrowserRouter>
<NotificationCenter /> <NotificationCenter />
</Provider> </>
); );
}; };

View File

@ -1,17 +1,19 @@
import classes from './AppCard.module.css'; import classes from './AppCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import { Icon } from '../../UI';
import { iconParser, urlParser } from '../../../utility'; import { iconParser, urlParser } from '../../../utility';
import { App, Config, GlobalState } from '../../../interfaces'; import { App } from '../../../interfaces';
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { State } from '../../../store/reducers';
interface ComponentProps { interface Props {
app: App; app: App;
pinHandler?: Function; pinHandler?: Function;
config: Config;
} }
const AppCard = (props: ComponentProps): JSX.Element => { export const AppCard = (props: Props): JSX.Element => {
const { config } = useSelector((state: State) => state.config);
const [displayUrl, redirectUrl] = urlParser(props.app.url); const [displayUrl, redirectUrl] = urlParser(props.app.url);
let iconEl: JSX.Element; let iconEl: JSX.Element;
@ -42,7 +44,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
return ( return (
<a <a
href={redirectUrl} href={redirectUrl}
target={props.config.appsSameTab ? '' : '_blank'} target={config.appsSameTab ? '' : '_blank'}
rel="noreferrer" rel="noreferrer"
className={classes.AppCard} className={classes.AppCard}
> >
@ -54,11 +56,3 @@ const AppCard = (props: ComponentProps): JSX.Element => {
</a> </a>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(AppCard);

View File

@ -1,23 +1,23 @@
import { useState, useEffect, ChangeEvent, SyntheticEvent } from 'react'; import { useState, useEffect, ChangeEvent, SyntheticEvent } from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { addApp, updateApp } from '../../../store/actions';
import { App, NewApp } from '../../../interfaces'; import { App, NewApp } from '../../../interfaces';
import classes from './AppForm.module.css'; import classes from './AppForm.module.css';
import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import { ModalForm, InputGroup, Button } from '../../UI';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
import { inputHandler, newAppTemplate } from '../../../utility'; import { inputHandler, newAppTemplate } from '../../../utility';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps { interface Props {
modalHandler: () => void; modalHandler: () => void;
addApp: (formData: NewApp | FormData) => any;
updateApp: (id: number, formData: NewApp | FormData) => any;
app?: App; app?: App;
} }
const AppForm = (props: ComponentProps): JSX.Element => { export const AppForm = (props: Props): JSX.Element => {
const dispatch = useDispatch();
const { addApp, updateApp } = bindActionCreators(actionCreators, dispatch);
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false); const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
const [customIcon, setCustomIcon] = useState<File | null>(null); const [customIcon, setCustomIcon] = useState<File | null>(null);
const [formData, setFormData] = useState<NewApp>(newAppTemplate); const [formData, setFormData] = useState<NewApp>(newAppTemplate);
@ -68,16 +68,16 @@ const AppForm = (props: ComponentProps): JSX.Element => {
if (!props.app) { if (!props.app) {
if (customIcon) { if (customIcon) {
const data = createFormData(); const data = createFormData();
props.addApp(data); addApp(data);
} else { } else {
props.addApp(formData); addApp(formData);
} }
} else { } else {
if (customIcon) { if (customIcon) {
const data = createFormData(); const data = createFormData();
props.updateApp(props.app.id, data); updateApp(props.app.id, data);
} else { } else {
props.updateApp(props.app.id, formData); updateApp(props.app.id, formData);
props.modalHandler(); props.modalHandler();
} }
} }
@ -192,5 +192,3 @@ const AppForm = (props: ComponentProps): JSX.Element => {
</ModalForm> </ModalForm>
); );
}; };
export default connect(null, { addApp, updateApp })(AppForm);

View File

@ -2,15 +2,15 @@ import classes from './AppGrid.module.css';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { App } from '../../../interfaces/App'; import { App } from '../../../interfaces/App';
import AppCard from '../AppCard/AppCard'; import { AppCard } from '../AppCard/AppCard';
interface ComponentProps { interface Props {
apps: App[]; apps: App[];
totalApps?: number; totalApps?: number;
searching: boolean; searching: boolean;
} }
const AppGrid = (props: ComponentProps): JSX.Element => { export const AppGrid = (props: Props): JSX.Element => {
let apps: JSX.Element; let apps: JSX.Element;
if (props.apps.length > 0) { if (props.apps.length > 0) {
@ -49,5 +49,3 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
return apps; return apps;
}; };
export default AppGrid;

View File

@ -8,48 +8,45 @@ import {
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import {
pinApp,
deleteApp,
reorderApps,
updateConfig,
createNotification,
} from '../../../store/actions';
// Typescript // Typescript
import { App, Config, GlobalState, NewNotification } from '../../../interfaces'; import { App } from '../../../interfaces';
// CSS // CSS
import classes from './AppTable.module.css'; import classes from './AppTable.module.css';
// UI // UI
import Icon from '../../UI/Icons/Icon/Icon'; import { Icon, Table } from '../../UI';
import Table from '../../UI/Table/Table'; import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps { interface Props {
apps: App[];
config: Config;
pinApp: (app: App) => void;
deleteApp: (id: number) => void;
updateAppHandler: (app: App) => void; updateAppHandler: (app: App) => void;
reorderApps: (apps: App[]) => void;
updateConfig: (formData: any) => void;
createNotification: (notification: NewNotification) => void;
} }
const AppTable = (props: ComponentProps): JSX.Element => { export const AppTable = (props: Props): JSX.Element => {
const {
apps: { apps },
config: { config },
} = useSelector((state: State) => state);
const dispatch = useDispatch();
const { pinApp, deleteApp, reorderApps, updateConfig, createNotification } =
bindActionCreators(actionCreators, dispatch);
const [localApps, setLocalApps] = useState<App[]>([]); const [localApps, setLocalApps] = useState<App[]>([]);
const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false); const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
// Copy apps array // Copy apps array
useEffect(() => { useEffect(() => {
setLocalApps([...props.apps]); setLocalApps([...apps]);
}, [props.apps]); }, [apps]);
// Check ordering // Check ordering
useEffect(() => { useEffect(() => {
const order = props.config.useOrdering; const order = config.useOrdering;
if (order === 'orderId') { if (order === 'orderId') {
setIsCustomOrder(true); setIsCustomOrder(true);
@ -62,7 +59,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
); );
if (proceed) { if (proceed) {
props.deleteApp(app.id); deleteApp(app.id);
} }
}; };
@ -79,7 +76,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
const dragEndHanlder = (result: DropResult): void => { const dragEndHanlder = (result: DropResult): void => {
if (!isCustomOrder) { if (!isCustomOrder) {
props.createNotification({ createNotification({
title: 'Error', title: 'Error',
message: 'Custom order is disabled', message: 'Custom order is disabled',
}); });
@ -95,7 +92,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
tmpApps.splice(result.destination.index, 0, movedApp); tmpApps.splice(result.destination.index, 0, movedApp);
setLocalApps(tmpApps); setLocalApps(tmpApps);
props.reorderApps(tmpApps); reorderApps(tmpApps);
}; };
return ( return (
@ -178,9 +175,9 @@ const AppTable = (props: ComponentProps): JSX.Element => {
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.pinApp(app)} onClick={() => pinApp(app)}
onKeyDown={(e) => onKeyDown={(e) =>
keyboardActionHandler(e, app, props.pinApp) keyboardActionHandler(e, app, pinApp)
} }
tabIndex={0} tabIndex={0}
> >
@ -208,20 +205,3 @@ const AppTable = (props: ComponentProps): JSX.Element => {
</Fragment> </Fragment>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
apps: state.app.apps,
config: state.config.config,
};
};
const actions = {
pinApp,
deleteApp,
reorderApps,
updateConfig,
createNotification,
};
export default connect(mapStateToProps, actions)(AppTable);

View File

@ -2,39 +2,37 @@ import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getApps } from '../../store/actions';
// Typescript // Typescript
import { App, GlobalState } from '../../interfaces'; import { App } from '../../interfaces';
// CSS // CSS
import classes from './Apps.module.css'; import classes from './Apps.module.css';
// UI // UI
import { Container } from '../UI/Layout/Layout'; import { Headline, Spinner, ActionButton, Modal, Container } from '../UI';
import Headline from '../UI/Headlines/Headline/Headline';
import Spinner from '../UI/Spinner/Spinner';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
import Modal from '../UI/Modal/Modal';
// Subcomponents // Subcomponents
import AppGrid from './AppGrid/AppGrid'; import { AppGrid } from './AppGrid/AppGrid';
import AppForm from './AppForm/AppForm'; import { AppForm } from './AppForm/AppForm';
import AppTable from './AppTable/AppTable'; import { AppTable } from './AppTable/AppTable';
// Utils // Utils
import { appTemplate } from '../../utility'; import { appTemplate } from '../../utility';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
interface ComponentProps { interface Props {
getApps: Function;
apps: App[];
loading: boolean;
searching: boolean; searching: boolean;
} }
const Apps = (props: ComponentProps): JSX.Element => { export const Apps = (props: Props): JSX.Element => {
const { getApps, apps, loading, searching = false } = props; const { apps, loading } = useSelector((state: State) => state.apps);
const dispatch = useDispatch();
const { getApps } = bindActionCreators(actionCreators, dispatch);
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false);
const [isInEdit, setIsInEdit] = useState(false); const [isInEdit, setIsInEdit] = useState(false);
@ -95,12 +93,3 @@ const Apps = (props: ComponentProps): JSX.Element => {
</Container> </Container>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
apps: state.app.apps,
loading: state.app.loading,
};
};
export default connect(mapStateToProps, { getApps })(Apps);

View File

@ -1,17 +1,23 @@
import { Bookmark, Category, Config, GlobalState } from '../../../interfaces'; import { Fragment } from 'react';
import { useSelector } from 'react-redux';
import { State } from '../../../store/reducers';
import { Bookmark, Category } from '../../../interfaces';
import classes from './BookmarkCard.module.css'; import classes from './BookmarkCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import { Icon } from '../../UI';
import { iconParser, urlParser } from '../../../utility';
import { Fragment } from 'react';
import { connect } from 'react-redux';
interface ComponentProps { import { iconParser, urlParser } from '../../../utility';
interface Props {
category: Category; category: Category;
config: Config;
} }
const BookmarkCard = (props: ComponentProps): JSX.Element => { export const BookmarkCard = (props: Props): JSX.Element => {
const { config } = useSelector((state: State) => state.config);
return ( return (
<div className={classes.BookmarkCard}> <div className={classes.BookmarkCard}>
<h3>{props.category.name}</h3> <h3>{props.category.name}</h3>
@ -56,7 +62,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
return ( return (
<a <a
href={redirectUrl} href={redirectUrl}
target={props.config.bookmarksSameTab ? '' : '_blank'} target={config.bookmarksSameTab ? '' : '_blank'}
rel="noreferrer" rel="noreferrer"
key={`bookmark-${bookmark.id}`} key={`bookmark-${bookmark.id}`}
> >
@ -69,11 +75,3 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
</div> </div>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(BookmarkCard);

View File

@ -8,94 +8,68 @@ import {
} from 'react'; } from 'react';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import {
getCategories,
addCategory,
addBookmark,
updateCategory,
updateBookmark,
createNotification,
} from '../../../store/actions';
// Typescript // Typescript
import { import {
Bookmark, Bookmark,
Category, Category,
GlobalState,
NewBookmark, NewBookmark,
NewCategory, NewCategory,
NewNotification,
} from '../../../interfaces'; } from '../../../interfaces';
import { ContentType } from '../Bookmarks'; import { ContentType } from '../Bookmarks';
// UI // UI
import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import { ModalForm, InputGroup, Button } from '../../UI';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
// CSS // CSS
import classes from './BookmarkForm.module.css'; import classes from './BookmarkForm.module.css';
import { newBookmarkTemplate, newCategoryTemplate } from '../../../utility';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps { interface Props {
modalHandler: () => void; modalHandler: () => void;
contentType: ContentType; contentType: ContentType;
categories: Category[];
category?: Category; category?: Category;
bookmark?: Bookmark; bookmark?: Bookmark;
addCategory: (formData: NewCategory) => void;
addBookmark: (formData: NewBookmark | FormData) => void;
updateCategory: (id: number, formData: NewCategory) => void;
updateBookmark: (
id: number,
formData: NewBookmark | FormData,
category: {
prev: number;
curr: number;
}
) => void;
createNotification: (notification: NewNotification) => void;
} }
const BookmarkForm = (props: ComponentProps): JSX.Element => { export const BookmarkForm = (props: Props): JSX.Element => {
const { categories } = useSelector((state: State) => state.bookmarks);
const dispatch = useDispatch();
const {
addCategory,
addBookmark,
updateCategory,
updateBookmark,
createNotification,
} = bindActionCreators(actionCreators, dispatch);
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false); const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
const [customIcon, setCustomIcon] = useState<File | null>(null); const [customIcon, setCustomIcon] = useState<File | null>(null);
const [categoryName, setCategoryName] = useState<NewCategory>({ const [categoryName, setCategoryName] =
name: '', useState<NewCategory>(newCategoryTemplate);
});
const [formData, setFormData] = useState<NewBookmark>({ const [formData, setFormData] = useState<NewBookmark>(newBookmarkTemplate);
name: '',
url: '',
categoryId: -1,
icon: '',
});
// Load category data if provided for editing // Load category data if provided for editing
useEffect(() => { useEffect(() => {
if (props.category) { if (props.category) {
setCategoryName({ name: props.category.name }); setCategoryName({ ...props.category });
} else { } else {
setCategoryName({ name: '' }); setCategoryName(newCategoryTemplate);
} }
}, [props.category]); }, [props.category]);
// Load bookmark data if provided for editing // Load bookmark data if provided for editing
useEffect(() => { useEffect(() => {
if (props.bookmark) { if (props.bookmark) {
setFormData({ setFormData({ ...props.bookmark });
name: props.bookmark.name,
url: props.bookmark.url,
categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon,
});
} else { } else {
setFormData({ setFormData(newBookmarkTemplate);
name: '',
url: '',
categoryId: -1,
icon: '',
});
} }
}, [props.bookmark]); }, [props.bookmark]);
@ -118,12 +92,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Add new // Add new
if (props.contentType === ContentType.category) { if (props.contentType === ContentType.category) {
// Add category // Add category
props.addCategory(categoryName); addCategory(categoryName);
setCategoryName({ name: '' }); setCategoryName(newCategoryTemplate);
} else if (props.contentType === ContentType.bookmark) { } else if (props.contentType === ContentType.bookmark) {
// Add bookmark // Add bookmark
if (formData.categoryId === -1) { if (formData.categoryId === -1) {
props.createNotification({ createNotification({
title: 'Error', title: 'Error',
message: 'Please select category', message: 'Please select category',
}); });
@ -132,16 +106,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
if (customIcon) { if (customIcon) {
const data = createFormData(); const data = createFormData();
props.addBookmark(data); addBookmark(data);
} else { } else {
props.addBookmark(formData); addBookmark(formData);
} }
setFormData({ setFormData({
name: '', ...newBookmarkTemplate,
url: '',
categoryId: formData.categoryId, categoryId: formData.categoryId,
icon: '',
}); });
// setCustomIcon(null); // setCustomIcon(null);
@ -150,29 +122,24 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
// Update // Update
if (props.contentType === ContentType.category && props.category) { if (props.contentType === ContentType.category && props.category) {
// Update category // Update category
props.updateCategory(props.category.id, categoryName); updateCategory(props.category.id, categoryName);
setCategoryName({ name: '' }); setCategoryName(newCategoryTemplate);
} else if (props.contentType === ContentType.bookmark && props.bookmark) { } else if (props.contentType === ContentType.bookmark && props.bookmark) {
// Update bookmark // Update bookmark
if (customIcon) { if (customIcon) {
const data = createFormData(); const data = createFormData();
props.updateBookmark(props.bookmark.id, data, { updateBookmark(props.bookmark.id, data, {
prev: props.bookmark.categoryId, prev: props.bookmark.categoryId,
curr: formData.categoryId, curr: formData.categoryId,
}); });
} else { } else {
props.updateBookmark(props.bookmark.id, formData, { updateBookmark(props.bookmark.id, formData, {
prev: props.bookmark.categoryId, prev: props.bookmark.categoryId,
curr: formData.categoryId, curr: formData.categoryId,
}); });
} }
setFormData({ setFormData(newBookmarkTemplate);
name: '',
url: '',
categoryId: -1,
icon: '',
});
setCustomIcon(null); setCustomIcon(null);
} }
@ -231,7 +198,9 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
placeholder="Social Media" placeholder="Social Media"
required required
value={categoryName.name} value={categoryName.name}
onChange={(e) => setCategoryName({ name: e.target.value })} onChange={(e) =>
setCategoryName({ name: e.target.value, isPublic: !!!!!false })
}
/> />
</InputGroup> </InputGroup>
</Fragment> </Fragment>
@ -249,6 +218,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="url">Bookmark URL</label> <label htmlFor="url">Bookmark URL</label>
<input <input
@ -271,6 +241,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
</a> </a>
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="categoryId">Bookmark Category</label> <label htmlFor="categoryId">Bookmark Category</label>
<select <select
@ -281,7 +252,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
value={formData.categoryId} value={formData.categoryId}
> >
<option value={-1}>Select category</option> <option value={-1}>Select category</option>
{props.categories.map((category: Category): JSX.Element => { {categories.map((category: Category): JSX.Element => {
return ( return (
<option key={category.id} value={category.id}> <option key={category.id} value={category.id}>
{category.name} {category.name}
@ -290,6 +261,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
})} })}
</select> </select>
</InputGroup> </InputGroup>
{!useCustomIcon ? ( {!useCustomIcon ? (
// mdi // mdi
<InputGroup> <InputGroup>
@ -344,20 +316,3 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
</ModalForm> </ModalForm>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
categories: state.bookmark.categories,
};
};
const dispatchMap = {
getCategories,
addCategory,
addBookmark,
updateCategory,
updateBookmark,
createNotification,
};
export default connect(mapStateToProps, dispatchMap)(BookmarkForm);

View File

@ -4,15 +4,15 @@ import classes from './BookmarkGrid.module.css';
import { Category } from '../../../interfaces'; import { Category } from '../../../interfaces';
import BookmarkCard from '../BookmarkCard/BookmarkCard'; import { BookmarkCard } from '../BookmarkCard/BookmarkCard';
interface ComponentProps { interface Props {
categories: Category[]; categories: Category[];
totalCategories?: number; totalCategories?: number;
searching: boolean; searching: boolean;
} }
const BookmarkGrid = (props: ComponentProps): JSX.Element => { export const BookmarkGrid = (props: Props): JSX.Element => {
let bookmarks: JSX.Element; let bookmarks: JSX.Element;
if (props.categories.length > 0) { if (props.categories.length > 0) {
@ -53,5 +53,3 @@ const BookmarkGrid = (props: ComponentProps): JSX.Element => {
return bookmarks; return bookmarks;
}; };
export default BookmarkGrid;

View File

@ -8,45 +8,39 @@ import {
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { import { State } from '../../../store/reducers';
pinCategory, import { bindActionCreators } from 'redux';
deleteCategory, import { actionCreators } from '../../../store';
deleteBookmark,
createNotification,
reorderCategories,
} from '../../../store/actions';
// Typescript // Typescript
import { import { Bookmark, Category } from '../../../interfaces';
Bookmark,
Category,
Config,
GlobalState,
NewNotification,
} from '../../../interfaces';
import { ContentType } from '../Bookmarks'; import { ContentType } from '../Bookmarks';
// CSS // CSS
import classes from './BookmarkTable.module.css'; import classes from './BookmarkTable.module.css';
// UI // UI
import Table from '../../UI/Table/Table'; import { Table, Icon } from '../../UI';
import Icon from '../../UI/Icons/Icon/Icon';
interface ComponentProps { interface Props {
contentType: ContentType; contentType: ContentType;
categories: Category[]; categories: Category[];
config: Config;
pinCategory: (category: Category) => void;
deleteCategory: (id: number) => void;
updateHandler: (data: Category | Bookmark) => void; updateHandler: (data: Category | Bookmark) => void;
deleteBookmark: (bookmarkId: number, categoryId: number) => void;
createNotification: (notification: NewNotification) => void;
reorderCategories: (categories: Category[]) => void;
} }
const BookmarkTable = (props: ComponentProps): JSX.Element => { export const BookmarkTable = (props: Props): JSX.Element => {
const { config } = useSelector((state: State) => state.config);
const dispatch = useDispatch();
const {
pinCategory,
deleteCategory,
deleteBookmark,
createNotification,
reorderCategories,
} = bindActionCreators(actionCreators, dispatch);
const [localCategories, setLocalCategories] = useState<Category[]>([]); const [localCategories, setLocalCategories] = useState<Category[]>([]);
const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false); const [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
@ -57,7 +51,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
// Check ordering // Check ordering
useEffect(() => { useEffect(() => {
const order = props.config.useOrdering; const order = config.useOrdering;
if (order === 'orderId') { if (order === 'orderId') {
setIsCustomOrder(true); setIsCustomOrder(true);
@ -70,7 +64,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
); );
if (proceed) { if (proceed) {
props.deleteCategory(category.id); deleteCategory(category.id);
} }
}; };
@ -80,7 +74,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
); );
if (proceed) { if (proceed) {
props.deleteBookmark(bookmark.id, bookmark.categoryId); deleteBookmark(bookmark.id, bookmark.categoryId);
} }
}; };
@ -96,7 +90,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
const dragEndHanlder = (result: DropResult): void => { const dragEndHanlder = (result: DropResult): void => {
if (!isCustomOrder) { if (!isCustomOrder) {
props.createNotification({ createNotification({
title: 'Error', title: 'Error',
message: 'Custom order is disabled', message: 'Custom order is disabled',
}); });
@ -112,7 +106,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
tmpCategories.splice(result.destination.index, 0, movedApp); tmpCategories.splice(result.destination.index, 0, movedApp);
setLocalCategories(tmpCategories); setLocalCategories(tmpCategories);
props.reorderCategories(tmpCategories); reorderCategories(tmpCategories);
}; };
if (props.contentType === ContentType.category) { if (props.contentType === ContentType.category) {
@ -186,12 +180,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
</div> </div>
<div <div
className={classes.TableAction} className={classes.TableAction}
onClick={() => props.pinCategory(category)} onClick={() => pinCategory(category)}
onKeyDown={(e) => onKeyDown={(e) =>
keyboardActionHandler( keyboardActionHandler(
e, e,
category, category,
props.pinCategory pinCategory
) )
} }
tabIndex={0} tabIndex={0}
@ -265,19 +259,3 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
); );
} }
}; };
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
const actions = {
pinCategory,
deleteCategory,
deleteBookmark,
createNotification,
reorderCategories,
};
export default connect(mapStateToProps, actions)(BookmarkTable);

View File

@ -1,25 +1,30 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { getCategories } from '../../store/actions';
// Redux
import { useDispatch, useSelector } from 'react-redux';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
// Typescript
import { Category, Bookmark } from '../../interfaces';
// CSS
import classes from './Bookmarks.module.css'; import classes from './Bookmarks.module.css';
import { Container } from '../UI/Layout/Layout'; // UI
import Headline from '../UI/Headlines/Headline/Headline'; import { Container, Headline, ActionButton, Spinner, Modal } from '../UI';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
import BookmarkGrid from './BookmarkGrid/BookmarkGrid'; // Components
import { Category, GlobalState, Bookmark } from '../../interfaces'; import { BookmarkGrid } from './BookmarkGrid/BookmarkGrid';
import Spinner from '../UI/Spinner/Spinner'; import { BookmarkForm } from './BookmarkForm/BookmarkForm';
import Modal from '../UI/Modal/Modal'; import { BookmarkTable } from './BookmarkTable/BookmarkTable';
import BookmarkForm from './BookmarkForm/BookmarkForm';
import BookmarkTable from './BookmarkTable/BookmarkTable';
interface ComponentProps { // Utils
loading: boolean; import { bookmarkTemplate, categoryTemplate } from '../../utility';
categories: Category[];
getCategories: () => void; interface Props {
searching: boolean; searching: boolean;
} }
@ -28,8 +33,15 @@ export enum ContentType {
bookmark, bookmark,
} }
const Bookmarks = (props: ComponentProps): JSX.Element => { export const Bookmarks = (props: Props): JSX.Element => {
const { getCategories, categories, loading, searching = false } = props; const { loading, categories } = useSelector(
(state: State) => state.bookmarks
);
const dispatch = useDispatch();
const { getCategories } = bindActionCreators(actionCreators, dispatch);
const { searching = false } = props;
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false);
const [formContentType, setFormContentType] = useState(ContentType.category); const [formContentType, setFormContentType] = useState(ContentType.category);
@ -38,24 +50,10 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
ContentType.category ContentType.category
); );
const [isInUpdate, setIsInUpdate] = useState(false); const [isInUpdate, setIsInUpdate] = useState(false);
const [categoryInUpdate, setCategoryInUpdate] = useState<Category>({ const [categoryInUpdate, setCategoryInUpdate] =
name: '', useState<Category>(categoryTemplate);
id: -1, const [bookmarkInUpdate, setBookmarkInUpdate] =
isPinned: false, useState<Bookmark>(bookmarkTemplate);
orderId: 0,
bookmarks: [],
createdAt: new Date(),
updatedAt: new Date(),
});
const [bookmarkInUpdate, setBookmarkInUpdate] = useState<Bookmark>({
name: '',
url: '',
categoryId: -1,
icon: '',
id: -1,
createdAt: new Date(),
updatedAt: new Date(),
});
useEffect(() => { useEffect(() => {
if (categories.length === 0) { if (categories.length === 0) {
@ -161,12 +159,3 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
</Container> </Container>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
loading: state.bookmark.loading,
categories: state.bookmark.categories,
};
};
export default connect(mapStateToProps, { getCategories })(Bookmarks);

View File

@ -1,17 +1,17 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Config, GlobalState } from '../../../interfaces';
import WeatherWidget from '../../Widgets/WeatherWidget/WeatherWidget'; // CSS
import { getDateTime } from './functions/getDateTime';
import { greeter } from './functions/greeter';
import classes from './Header.module.css'; import classes from './Header.module.css';
interface Props { // Components
config: Config; import { WeatherWidget } from '../../Widgets/WeatherWidget/WeatherWidget';
}
const Header = (props: Props): JSX.Element => { // Utils
import { getDateTime } from './functions/getDateTime';
import { greeter } from './functions/greeter';
export const Header = (): JSX.Element => {
const [dateTime, setDateTime] = useState<string>(getDateTime()); const [dateTime, setDateTime] = useState<string>(getDateTime());
const [greeting, setGreeting] = useState<string>(greeter()); const [greeting, setGreeting] = useState<string>(greeter());
@ -39,11 +39,3 @@ const Header = (props: Props): JSX.Element => {
</header> </header>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(Header);

View File

@ -2,47 +2,38 @@ import { useState, useEffect, Fragment } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { getApps, getCategories } from '../../store/actions'; import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
// Typescript // Typescript
import { GlobalState } from '../../interfaces/GlobalState'; import { App, Category } from '../../interfaces';
import { App, Category, Config } from '../../interfaces';
// UI // UI
import Icon from '../UI/Icons/Icon/Icon'; import { Icon, Container, SectionHeadline, Spinner } from '../UI';
import { Container } from '../UI/Layout/Layout';
import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline';
import Spinner from '../UI/Spinner/Spinner';
// CSS // CSS
import classes from './Home.module.css'; import classes from './Home.module.css';
// Components // Components
import AppGrid from '../Apps/AppGrid/AppGrid'; import { AppGrid } from '../Apps/AppGrid/AppGrid';
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; import { BookmarkGrid } from '../Bookmarks/BookmarkGrid/BookmarkGrid';
import SearchBar from '../SearchBar/SearchBar'; import { SearchBar } from '../SearchBar/SearchBar';
import Header from './Header/Header'; import { Header } from './Header/Header';
interface ComponentProps { export const Home = (): JSX.Element => {
getApps: Function;
getCategories: Function;
appsLoading: boolean;
apps: App[];
categoriesLoading: boolean;
categories: Category[];
config: Config;
}
const Home = (props: ComponentProps): JSX.Element => {
const { const {
getApps, apps: { apps, loading: appsLoading },
apps, bookmarks: { categories, loading: bookmarksLoading },
appsLoading, config: { config },
getCategories, } = useSelector((state: State) => state);
categories,
categoriesLoading, const dispatch = useDispatch();
} = props; const { getApps, getCategories } = bindActionCreators(
actionCreators,
dispatch
);
// Local search query // Local search query
const [localSearch, setLocalSearch] = useState<null | string>(null); const [localSearch, setLocalSearch] = useState<null | string>(null);
@ -90,7 +81,7 @@ const Home = (props: ComponentProps): JSX.Element => {
return ( return (
<Container> <Container>
{!props.config.hideSearch ? ( {!config.hideSearch ? (
<SearchBar <SearchBar
setLocalSearch={setLocalSearch} setLocalSearch={setLocalSearch}
appSearchResult={appSearchResult} appSearchResult={appSearchResult}
@ -100,9 +91,9 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div> <div></div>
)} )}
{!props.config.hideHeader ? <Header /> : <div></div>} {!config.hideHeader ? <Header /> : <div></div>}
{!props.config.hideApps ? ( {!config.hideApps ? (
<Fragment> <Fragment>
<SectionHeadline title="Applications" link="/applications" /> <SectionHeadline title="Applications" link="/applications" />
{appsLoading ? ( {appsLoading ? (
@ -124,10 +115,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div> <div></div>
)} )}
{!props.config.hideCategories ? ( {!config.hideCategories ? (
<Fragment> <Fragment>
<SectionHeadline title="Bookmarks" link="/bookmarks" /> <SectionHeadline title="Bookmarks" link="/bookmarks" />
{categoriesLoading ? ( {bookmarksLoading ? (
<Spinner /> <Spinner />
) : ( ) : (
<BookmarkGrid <BookmarkGrid
@ -151,15 +142,3 @@ const Home = (props: ComponentProps): JSX.Element => {
</Container> </Container>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
appsLoading: state.app.loading,
apps: state.app.apps,
categoriesLoading: state.bookmark.loading,
categories: state.bookmark.categories,
config: state.config.config,
};
};
export default connect(mapStateToProps, { getApps, getCategories })(Home);

View File

@ -1,21 +1,20 @@
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
import { GlobalState, Notification as _Notification } from '../../interfaces'; import { Notification as NotificationInterface } from '../../interfaces';
import classes from './NotificationCenter.module.css'; import classes from './NotificationCenter.module.css';
import Notification from '../UI/Notification/Notification'; import { Notification } from '../UI';
import { State } from '../../store/reducers';
interface ComponentProps { export const NotificationCenter = (): JSX.Element => {
notifications: _Notification[]; const { notifications } = useSelector((state: State) => state.notification);
}
const NotificationCenter = (props: ComponentProps): JSX.Element => {
return ( return (
<div <div
className={classes.NotificationCenter} className={classes.NotificationCenter}
style={{ height: `${props.notifications.length * 75}px` }} style={{ height: `${notifications.length * 75}px` }}
> >
{props.notifications.map((notification: _Notification) => { {notifications.map((notification: NotificationInterface) => {
return ( return (
<Notification <Notification
title={notification.title} title={notification.title}
@ -29,11 +28,3 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => {
</div> </div>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
notifications: state.notification.notifications,
};
};
export default connect(mapStateToProps)(NotificationCenter);

View File

@ -1,42 +1,33 @@
import { useRef, useEffect, KeyboardEvent } from 'react'; import { useRef, useEffect, KeyboardEvent } from 'react';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createNotification } from '../../store/actions';
// Typescript // Typescript
import { import { App, Category } from '../../interfaces';
App,
Category,
Config,
GlobalState,
NewNotification,
} from '../../interfaces';
// CSS // CSS
import classes from './SearchBar.module.css'; import classes from './SearchBar.module.css';
// Utils // Utils
import { searchParser, urlParser, redirectUrl } from '../../utility'; import { searchParser, urlParser, redirectUrl } from '../../utility';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
interface ComponentProps { interface Props {
createNotification: (notification: NewNotification) => void;
setLocalSearch: (query: string) => void; setLocalSearch: (query: string) => void;
appSearchResult: App[] | null; appSearchResult: App[] | null;
bookmarkSearchResult: Category[] | null; bookmarkSearchResult: Category[] | null;
config: Config;
loading: boolean;
} }
const SearchBar = (props: ComponentProps): JSX.Element => { export const SearchBar = (props: Props): JSX.Element => {
const { const { config, loading } = useSelector((state: State) => state.config);
setLocalSearch,
createNotification, const dispatch = useDispatch();
config, const { createNotification } = bindActionCreators(actionCreators, dispatch);
loading,
appSearchResult, const { setLocalSearch, appSearchResult, bookmarkSearchResult } = props;
bookmarkSearchResult,
} = props;
const inputRef = useRef<HTMLInputElement>(document.createElement('input')); const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
@ -126,12 +117,3 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
</div> </div>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
loading: state.config.loading,
};
};
export default connect(mapStateToProps, { createNotification })(SearchBar);

View File

@ -1,34 +1,33 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import classes from './AppDetails.module.css'; import classes from './AppDetails.module.css';
import Button from '../../UI/Buttons/Button/Button'; import { Button } from '../../UI';
import { checkVersion } from '../../../utility'; import { checkVersion } from '../../../utility';
const AppDetails = (): JSX.Element => { export const AppDetails = (): JSX.Element => {
return ( return (
<Fragment> <Fragment>
<p className={classes.AppVersion}> <p className={classes.AppVersion}>
<a <a
href='https://github.com/pawelmalak/flame' href="https://github.com/pawelmalak/flame"
target='_blank' target="_blank"
rel='noreferrer'> rel="noreferrer"
>
Flame Flame
</a> </a>{' '}
{' '}
version {process.env.REACT_APP_VERSION} version {process.env.REACT_APP_VERSION}
</p> </p>
<p className={classes.AppVersion}> <p className={classes.AppVersion}>
See changelog {' '} See changelog{' '}
<a <a
href='https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md' href="https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md"
target='_blank' target="_blank"
rel='noreferrer'> rel="noreferrer"
>
here here
</a> </a>
</p> </p>
<Button click={() => checkVersion(true)}>Check for updates</Button> <Button click={() => checkVersion(true)}>Check for updates</Button>
</Fragment> </Fragment>
) );
} };
export default AppDetails;

View File

@ -1,41 +1,28 @@
import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import {
createNotification,
updateConfig,
sortApps,
sortCategories,
} from '../../../store/actions';
// Typescript // Typescript
import { import { OtherSettingsForm } from '../../../interfaces';
Config,
GlobalState,
NewNotification,
OtherSettingsForm,
} from '../../../interfaces';
// UI // UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import { InputGroup, Button, SettingsHeadline } from '../../UI';
import Button from '../../UI/Buttons/Button/Button';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
// Utils // Utils
import { otherSettingsTemplate, inputHandler } from '../../../utility'; import { otherSettingsTemplate, inputHandler } from '../../../utility';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps { export const OtherSettings = (): JSX.Element => {
createNotification: (notification: NewNotification) => void; const { loading, config } = useSelector((state: State) => state.config);
updateConfig: (formData: OtherSettingsForm) => void;
sortApps: () => void;
sortCategories: () => void;
loading: boolean;
config: Config;
}
const OtherSettings = (props: ComponentProps): JSX.Element => { const dispatch = useDispatch();
const { config } = props; const { updateConfig, sortApps, sortCategories } = bindActionCreators(
actionCreators,
dispatch
);
// Initial state // Initial state
const [formData, setFormData] = useState<OtherSettingsForm>( const [formData, setFormData] = useState<OtherSettingsForm>(
@ -47,21 +34,21 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
setFormData({ setFormData({
...config, ...config,
}); });
}, [props.loading]); }, [loading]);
// Form handler // Form handler
const formSubmitHandler = async (e: FormEvent) => { const formSubmitHandler = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
// Save settings // Save settings
await props.updateConfig(formData); await updateConfig(formData);
// Update local page title // Update local page title
document.title = formData.customTitle; document.title = formData.customTitle;
// Sort apps and categories with new settings // Sort apps and categories with new settings
props.sortApps(); sortApps();
props.sortCategories(); sortCategories();
}; };
// Input handler // Input handler
@ -338,19 +325,3 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</form> </form>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
loading: state.config.loading,
config: state.config.config,
};
};
const actions = {
createNotification,
updateConfig,
sortApps,
sortCategories,
};
export default connect(mapStateToProps, actions)(OtherSettings);

View File

@ -1,29 +1,31 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { connect } from 'react-redux';
// Redux
import { useDispatch, useSelector } from 'react-redux';
import { State } from '../../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
// Typescript
import { Query } from '../../../../interfaces';
// CSS
import classes from './CustomQueries.module.css'; import classes from './CustomQueries.module.css';
import Modal from '../../../UI/Modal/Modal'; // UI
import Icon from '../../../UI/Icons/Icon/Icon'; import { Modal, Icon, Button } from '../../../UI';
import {
Config,
GlobalState,
NewNotification,
Query,
} from '../../../../interfaces';
import QueriesForm from './QueriesForm';
import { deleteQuery, createNotification } from '../../../../store/actions';
import Button from '../../../UI/Buttons/Button/Button';
interface Props { // Components
customQueries: Query[]; import { QueriesForm } from './QueriesForm';
deleteQuery: (prefix: string) => {};
createNotification: (notification: NewNotification) => void;
config: Config;
}
const CustomQueries = (props: Props): JSX.Element => { export const CustomQueries = (): JSX.Element => {
const { customQueries, deleteQuery, createNotification } = props; const { customQueries, config } = useSelector((state: State) => state.config);
const dispatch = useDispatch();
const { deleteQuery, createNotification } = bindActionCreators(
actionCreators,
dispatch
);
const [modalIsOpen, setModalIsOpen] = useState(false); const [modalIsOpen, setModalIsOpen] = useState(false);
const [editableQuery, setEditableQuery] = useState<Query | null>(null); const [editableQuery, setEditableQuery] = useState<Query | null>(null);
@ -34,7 +36,7 @@ const CustomQueries = (props: Props): JSX.Element => {
}; };
const deleteHandler = (query: Query) => { const deleteHandler = (query: Query) => {
const currentProvider = props.config.defaultSearchProvider; const currentProvider = config.defaultSearchProvider;
const isCurrent = currentProvider === query.prefix; const isCurrent = currentProvider === query.prefix;
if (isCurrent) { if (isCurrent) {
@ -105,14 +107,3 @@ const CustomQueries = (props: Props): JSX.Element => {
</Fragment> </Fragment>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
customQueries: state.config.customQueries,
config: state.config.config,
};
};
export default connect(mapStateToProps, { deleteQuery, createNotification })(
CustomQueries
);

View File

@ -1,20 +1,26 @@
import { ChangeEvent, FormEvent, useState, useEffect } from 'react'; import { ChangeEvent, FormEvent, useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
import { Query } from '../../../../interfaces'; import { Query } from '../../../../interfaces';
import Button from '../../../UI/Buttons/Button/Button';
import InputGroup from '../../../UI/Forms/InputGroup/InputGroup'; import { Button, InputGroup, ModalForm } from '../../../UI';
import ModalForm from '../../../UI/Forms/ModalForm/ModalForm';
import { connect } from 'react-redux';
import { addQuery, updateQuery } from '../../../../store/actions';
interface Props { interface Props {
modalHandler: () => void; modalHandler: () => void;
addQuery: (query: Query) => {};
updateQuery: (query: Query, Oldprefix: string) => {};
query?: Query; query?: Query;
} }
const QueriesForm = (props: Props): JSX.Element => { export const QueriesForm = (props: Props): JSX.Element => {
const { modalHandler, addQuery, updateQuery, query } = props; const dispatch = useDispatch();
const { addQuery, updateQuery } = bindActionCreators(
actionCreators,
dispatch
);
const { modalHandler, query } = props;
const [formData, setFormData] = useState<Query>({ const [formData, setFormData] = useState<Query>({
name: '', name: '',
@ -77,6 +83,7 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="name">Prefix</label> <label htmlFor="name">Prefix</label>
<input <input
@ -89,6 +96,7 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="name">Query Template</label> <label htmlFor="name">Query Template</label>
<input <input
@ -101,9 +109,8 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
</InputGroup> </InputGroup>
{query ? <Button>Update provider</Button> : <Button>Add provider</Button>} {query ? <Button>Update provider</Button> : <Button>Add provider</Button>}
</ModalForm> </ModalForm>
); );
}; };
export default connect(null, { addQuery, updateQuery })(QueriesForm);

View File

@ -1,58 +1,49 @@
// React // React
import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react'; import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react';
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
// State
import { createNotification, updateConfig } from '../../../store/actions';
// Typescript // Typescript
import { import { Query, SearchForm } from '../../../interfaces';
Config,
GlobalState,
NewNotification,
Query,
SearchForm,
} from '../../../interfaces';
// Components // Components
import CustomQueries from './CustomQueries/CustomQueries'; import { CustomQueries } from './CustomQueries/CustomQueries';
// UI // UI
import Button from '../../UI/Buttons/Button/Button'; import { Button, SettingsHeadline, InputGroup } from '../../UI';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
// Utils // Utils
import { inputHandler, searchSettingsTemplate } from '../../../utility'; import { inputHandler, searchSettingsTemplate } from '../../../utility';
// Data // Data
import { queries } from '../../../utility/searchQueries.json'; import { queries } from '../../../utility/searchQueries.json';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface Props { export const SearchSettings = (): JSX.Element => {
createNotification: (notification: NewNotification) => void; const { loading, customQueries, config } = useSelector(
updateConfig: (formData: SearchForm) => void; (state: State) => state.config
loading: boolean; );
customQueries: Query[];
config: Config; const dispatch = useDispatch();
} const { updateConfig } = bindActionCreators(actionCreators, dispatch);
const SearchSettings = (props: Props): JSX.Element => {
// Initial state // Initial state
const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate); const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
// Get config // Get config
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
...props.config, ...config,
}); });
}, [props.loading]); }, [loading]);
// Form handler // Form handler
const formSubmitHandler = async (e: FormEvent) => { const formSubmitHandler = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
// Save settings // Save settings
await props.updateConfig(formData); await updateConfig(formData);
}; };
// Input handler // Input handler
@ -84,7 +75,7 @@ const SearchSettings = (props: Props): JSX.Element => {
value={formData.defaultSearchProvider} value={formData.defaultSearchProvider}
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
> >
{[...queries, ...props.customQueries].map((query: Query, idx) => { {[...queries, ...customQueries].map((query: Query, idx) => {
const isCustom = idx >= queries.length; const isCustom = idx >= queries.length;
return ( return (
@ -95,6 +86,7 @@ const SearchSettings = (props: Props): JSX.Element => {
})} })}
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="searchSameTab"> <label htmlFor="searchSameTab">
Open search results in the same tab Open search results in the same tab
@ -109,6 +101,7 @@ const SearchSettings = (props: Props): JSX.Element => {
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="hideSearch">Hide search bar</label> <label htmlFor="hideSearch">Hide search bar</label>
<select <select
@ -121,6 +114,7 @@ const SearchSettings = (props: Props): JSX.Element => {
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="disableAutofocus">Disable search bar autofocus</label> <label htmlFor="disableAutofocus">Disable search bar autofocus</label>
<select <select
@ -133,6 +127,7 @@ const SearchSettings = (props: Props): JSX.Element => {
<option value={0}>False</option> <option value={0}>False</option>
</select> </select>
</InputGroup> </InputGroup>
<Button>Save changes</Button> <Button>Save changes</Button>
</form> </form>
@ -142,18 +137,3 @@ const SearchSettings = (props: Props): JSX.Element => {
</Fragment> </Fragment>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
loading: state.config.loading,
customQueries: state.config.customQueries,
config: state.config.config,
};
};
const actions = {
createNotification,
updateConfig,
};
export default connect(mapStateToProps, actions)(SearchSettings);

View File

@ -1,4 +1,3 @@
//
import { NavLink, Link, Switch, Route } from 'react-router-dom'; import { NavLink, Link, Switch, Route } from 'react-router-dom';
// Typescript // Typescript
@ -8,21 +7,20 @@ import { Route as SettingsRoute } from '../../interfaces';
import classes from './Settings.module.css'; import classes from './Settings.module.css';
// Components // Components
import Themer from '../Themer/Themer'; import { Themer } from '../Themer/Themer';
import WeatherSettings from './WeatherSettings/WeatherSettings'; import { WeatherSettings } from './WeatherSettings/WeatherSettings';
import OtherSettings from './OtherSettings/OtherSettings'; import { OtherSettings } from './OtherSettings/OtherSettings';
import AppDetails from './AppDetails/AppDetails'; import { AppDetails } from './AppDetails/AppDetails';
import StyleSettings from './StyleSettings/StyleSettings'; import { StyleSettings } from './StyleSettings/StyleSettings';
import SearchSettings from './SearchSettings/SearchSettings'; import { SearchSettings } from './SearchSettings/SearchSettings';
// UI // UI
import { Container } from '../UI/Layout/Layout'; import { Container, Headline } from '../UI';
import Headline from '../UI/Headlines/Headline/Headline';
// Data // Data
import { routes } from './settings.json'; import { routes } from './settings.json';
const Settings = (): JSX.Element => { export const Settings = (): JSX.Element => {
return ( return (
<Container> <Container>
<Headline title="Settings" subtitle={<Link to="/">Go back</Link>} /> <Headline title="Settings" subtitle={<Link to="/">Go back</Link>} />
@ -57,5 +55,3 @@ const Settings = (): JSX.Element => {
</Container> </Container>
); );
}; };
export default Settings;

View File

@ -2,54 +2,55 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
import axios from 'axios'; import axios from 'axios';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { createNotification } from '../../../store/actions'; import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
// Typescript // Typescript
import { ApiResponse, NewNotification } from '../../../interfaces'; import { ApiResponse } from '../../../interfaces';
// UI // UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import { InputGroup, Button } from '../../UI';
import Button from '../../UI/Buttons/Button/Button';
interface ComponentProps { export const StyleSettings = (): JSX.Element => {
createNotification: (notification: NewNotification) => void; const dispatch = useDispatch();
} const { createNotification } = bindActionCreators(actionCreators, dispatch);
const StyleSettings = (props: ComponentProps): JSX.Element => {
const [customStyles, setCustomStyles] = useState<string>(''); const [customStyles, setCustomStyles] = useState<string>('');
useEffect(() => { useEffect(() => {
axios.get<ApiResponse<string>>('/api/config/0/css') axios
.then(data => setCustomStyles(data.data.data)) .get<ApiResponse<string>>('/api/config/0/css')
.catch(err => console.log(err.response)); .then((data) => setCustomStyles(data.data.data))
}, []) .catch((err) => console.log(err.response));
}, []);
const inputChangeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => { const inputChangeHandler = (e: ChangeEvent<HTMLTextAreaElement>) => {
e.preventDefault(); e.preventDefault();
setCustomStyles(e.target.value); setCustomStyles(e.target.value);
} };
const formSubmitHandler = (e: FormEvent) => { const formSubmitHandler = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
axios.put<ApiResponse<{}>>('/api/config/0/css', { styles: customStyles }) axios
.put<ApiResponse<{}>>('/api/config/0/css', { styles: customStyles })
.then(() => { .then(() => {
props.createNotification({ createNotification({
title: 'Success', title: 'Success',
message: 'CSS saved. Reload page to see changes' message: 'CSS saved. Reload page to see changes',
});
}) })
}) .catch((err) => console.log(err.response));
.catch(err => console.log(err.response)); };
}
return ( return (
<form onSubmit={(e) => formSubmitHandler(e)}> <form onSubmit={(e) => formSubmitHandler(e)}>
<InputGroup> <InputGroup>
<label htmlFor='customStyles'>Custom CSS</label> <label htmlFor="customStyles">Custom CSS</label>
<textarea <textarea
id='customStyles' id="customStyles"
name='customStyles' name="customStyles"
value={customStyles} value={customStyles}
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
spellCheck={false} spellCheck={false}
@ -57,7 +58,5 @@ const StyleSettings = (props: ComponentProps): JSX.Element => {
</InputGroup> </InputGroup>
<Button>Save CSS</Button> <Button>Save CSS</Button>
</form> </form>
) );
} };
export default connect(null, { createNotification })(StyleSettings);

View File

@ -2,34 +2,29 @@ import { useState, ChangeEvent, useEffect, FormEvent } from 'react';
import axios from 'axios'; import axios from 'axios';
// Redux // Redux
import { connect } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createNotification, updateConfig } from '../../../store/actions'; import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
// Typescript // Typescript
import { import { ApiResponse, Weather, WeatherForm } from '../../../interfaces';
ApiResponse,
Config,
GlobalState,
NewNotification,
Weather,
WeatherForm,
} from '../../../interfaces';
// UI // UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import { InputGroup, Button } from '../../UI';
import Button from '../../UI/Buttons/Button/Button';
// Utils // Utils
import { inputHandler, weatherSettingsTemplate } from '../../../utility'; import { inputHandler, weatherSettingsTemplate } from '../../../utility';
import { State } from '../../../store/reducers';
interface ComponentProps { export const WeatherSettings = (): JSX.Element => {
createNotification: (notification: NewNotification) => void; const { loading, config } = useSelector((state: State) => state.config);
updateConfig: (formData: WeatherForm) => void;
loading: boolean; const dispatch = useDispatch();
config: Config; const { createNotification, updateConfig } = bindActionCreators(
} actionCreators,
dispatch
);
const WeatherSettings = (props: ComponentProps): JSX.Element => {
// Initial state // Initial state
const [formData, setFormData] = useState<WeatherForm>( const [formData, setFormData] = useState<WeatherForm>(
weatherSettingsTemplate weatherSettingsTemplate
@ -38,9 +33,9 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
// Get config // Get config
useEffect(() => { useEffect(() => {
setFormData({ setFormData({
...props.config, ...config,
}); });
}, [props.loading]); }, [loading]);
// Form handler // Form handler
const formSubmitHandler = async (e: FormEvent) => { const formSubmitHandler = async (e: FormEvent) => {
@ -48,26 +43,26 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
// Check for api key input // Check for api key input
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) { if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
props.createNotification({ createNotification({
title: 'Warning', title: 'Warning',
message: 'API key is missing. Weather Module will NOT work', message: 'API key is missing. Weather Module will NOT work',
}); });
} }
// Save settings // Save settings
await props.updateConfig(formData); await updateConfig(formData);
// Update weather // Update weather
axios axios
.get<ApiResponse<Weather>>('/api/weather/update') .get<ApiResponse<Weather>>('/api/weather/update')
.then(() => { .then(() => {
props.createNotification({ createNotification({
title: 'Success', title: 'Success',
message: 'Weather updated', message: 'Weather updated',
}); });
}) })
.catch((err) => { .catch((err) => {
props.createNotification({ createNotification({
title: 'Error', title: 'Error',
message: err.response.data.error, message: err.response.data.error,
}); });
@ -108,6 +103,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
. Key is required for weather module to work. . Key is required for weather module to work.
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="lat">Location latitude</label> <label htmlFor="lat">Location latitude</label>
<input <input
@ -131,6 +127,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
</a> </a>
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="long">Location longitude</label> <label htmlFor="long">Location longitude</label>
<input <input
@ -144,6 +141,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
lang="en-150" lang="en-150"
/> />
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor="isCelsius">Temperature unit</label> <label htmlFor="isCelsius">Temperature unit</label>
<select <select
@ -156,18 +154,8 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>Fahrenheit</option> <option value={0}>Fahrenheit</option>
</select> </select>
</InputGroup> </InputGroup>
<Button>Save changes</Button> <Button>Save changes</Button>
</form> </form>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
loading: state.config.loading,
config: state.config.config,
};
};
export default connect(mapStateToProps, { createNotification, updateConfig })(
WeatherSettings
);

View File

@ -1,14 +1,17 @@
import { Theme } from '../../interfaces/Theme'; import { Theme } from '../../interfaces/Theme';
import classes from './ThemePreview.module.css'; import classes from './ThemePreview.module.css';
interface ComponentProps { interface Props {
theme: Theme; theme: Theme;
applyTheme: Function; applyTheme: Function;
} }
const ThemePreview = (props: ComponentProps): JSX.Element => { export const ThemePreview = (props: Props): JSX.Element => {
return ( return (
<div className={classes.ThemePreview} onClick={() => props.applyTheme(props.theme.name)}> <div
className={classes.ThemePreview}
onClick={() => props.applyTheme(props.theme.name)}
>
<div className={classes.ColorsPreview}> <div className={classes.ColorsPreview}>
<div <div
className={classes.ColorPreview} className={classes.ColorPreview}
@ -25,7 +28,5 @@ const ThemePreview = (props: ComponentProps): JSX.Element => {
</div> </div>
<p>{props.theme.name}</p> <p>{props.theme.name}</p>
</div> </div>
) );
} };
export default ThemePreview;

View File

@ -1,37 +1,29 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import { connect } from 'react-redux'; import { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
import classes from './Themer.module.css'; import classes from './Themer.module.css';
import { themes } from './themes.json'; import { themes } from './themes.json';
import { Theme } from '../../interfaces/Theme'; import { Theme } from '../../interfaces/Theme';
import ThemePreview from './ThemePreview'; import { ThemePreview } from './ThemePreview';
import { setTheme } from '../../store/actions'; export const Themer = (): JSX.Element => {
const dispatch = useDispatch();
interface ComponentProps { const { setTheme } = bindActionCreators(actionCreators, dispatch);
setTheme: Function;
}
const Themer = (props: ComponentProps): JSX.Element => {
return ( return (
<Fragment> <Fragment>
<div> <div>
<div className={classes.ThemerGrid}> <div className={classes.ThemerGrid}>
{themes.map((theme: Theme, idx: number): JSX.Element => ( {themes.map(
<ThemePreview (theme: Theme, idx: number): JSX.Element => (
key={idx} <ThemePreview key={idx} theme={theme} applyTheme={setTheme} />
theme={theme} )
applyTheme={props.setTheme} )}
/>
))}
</div> </div>
</div> </div>
</Fragment> </Fragment>
);
) };
}
export default connect(null, { setTheme })(Themer);

View File

@ -2,23 +2,21 @@ import { useState, useEffect, Fragment } from 'react';
import axios from 'axios'; import axios from 'axios';
// Redux // Redux
import { connect } from 'react-redux'; import { useSelector } from 'react-redux';
// Typescript // Typescript
import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces'; import { Weather, ApiResponse, Config } from '../../../interfaces';
// CSS // CSS
import classes from './WeatherWidget.module.css'; import classes from './WeatherWidget.module.css';
// UI // UI
import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon'; import { WeatherIcon } from '../../UI';
import { State } from '../../../store/reducers';
interface ComponentProps { export const WeatherWidget = (): JSX.Element => {
configLoading: boolean; const { loading, config } = useSelector((state: State) => state.config);
config: Config;
}
const WeatherWidget = (props: ComponentProps): JSX.Element => {
const [weather, setWeather] = useState<Weather>({ const [weather, setWeather] = useState<Weather>({
externalLastUpdate: '', externalLastUpdate: '',
tempC: 0, tempC: 0,
@ -68,8 +66,8 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
return ( return (
<div className={classes.WeatherWidget}> <div className={classes.WeatherWidget}>
{isLoading || {isLoading ||
props.configLoading || loading ||
(props.config.WEATHER_API_KEY && weather.id > 0 && ( (config.WEATHER_API_KEY && weather.id > 0 && (
<Fragment> <Fragment>
<div className={classes.WeatherIcon}> <div className={classes.WeatherIcon}>
<WeatherIcon <WeatherIcon
@ -78,7 +76,7 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
/> />
</div> </div>
<div className={classes.WeatherDetails}> <div className={classes.WeatherDetails}>
{props.config.isCelsius ? ( {config.isCelsius ? (
<span>{weather.tempC}°C</span> <span>{weather.tempC}°C</span>
) : ( ) : (
<span>{weather.tempF}°F</span> <span>{weather.tempF}°F</span>
@ -90,12 +88,3 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
</div> </div>
); );
}; };
const mapStateToProps = (state: GlobalState) => {
return {
configLoading: state.config.loading,
config: state.config.config,
};
};
export default connect(mapStateToProps)(WeatherWidget);

View File

@ -1,11 +1,17 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import './index.css'; import './index.css';
import { Provider } from 'react-redux';
import { store } from './store/store';
import App from './App'; import App from './App';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<Provider store={store}>
<App /> <App />
</Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );

View File

@ -1,13 +0,0 @@
import { State as AppState } from '../store/reducers/app';
import { State as ThemeState } from '../store/reducers/theme';
import { State as BookmarkState } from '../store/reducers/bookmark';
import { State as NotificationState } from '../store/reducers/notification';
import { State as ConfigState } from '../store/reducers/config';
export interface GlobalState {
theme: ThemeState;
app: AppState;
bookmark: BookmarkState;
notification: NotificationState;
config: ConfigState;
}

View File

@ -1,6 +1,5 @@
export * from './App'; export * from './App';
export * from './Theme'; export * from './Theme';
export * from './GlobalState';
export * from './Api'; export * from './Api';
export * from './Weather'; export * from './Weather';
export * from './Bookmark'; export * from './Bookmark';