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

View file

@ -1,17 +1,19 @@
import classes from './AppCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon';
import { Icon } from '../../UI';
import { iconParser, urlParser } from '../../../utility';
import { App, Config, GlobalState } from '../../../interfaces';
import { connect } from 'react-redux';
import { App } from '../../../interfaces';
import { useSelector } from 'react-redux';
import { State } from '../../../store/reducers';
interface ComponentProps {
interface Props {
app: App;
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);
let iconEl: JSX.Element;
@ -42,7 +44,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
return (
<a
href={redirectUrl}
target={props.config.appsSameTab ? '' : '_blank'}
target={config.appsSameTab ? '' : '_blank'}
rel="noreferrer"
className={classes.AppCard}
>
@ -54,11 +56,3 @@ const AppCard = (props: ComponentProps): JSX.Element => {
</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 { connect } from 'react-redux';
import { addApp, updateApp } from '../../../store/actions';
import { useDispatch } from 'react-redux';
import { App, NewApp } from '../../../interfaces';
import classes from './AppForm.module.css';
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
import { ModalForm, InputGroup, Button } from '../../UI';
import { inputHandler, newAppTemplate } from '../../../utility';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps {
interface Props {
modalHandler: () => void;
addApp: (formData: NewApp | FormData) => any;
updateApp: (id: number, formData: NewApp | FormData) => any;
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 [customIcon, setCustomIcon] = useState<File | null>(null);
const [formData, setFormData] = useState<NewApp>(newAppTemplate);
@ -68,16 +68,16 @@ const AppForm = (props: ComponentProps): JSX.Element => {
if (!props.app) {
if (customIcon) {
const data = createFormData();
props.addApp(data);
addApp(data);
} else {
props.addApp(formData);
addApp(formData);
}
} else {
if (customIcon) {
const data = createFormData();
props.updateApp(props.app.id, data);
updateApp(props.app.id, data);
} else {
props.updateApp(props.app.id, formData);
updateApp(props.app.id, formData);
props.modalHandler();
}
}
@ -192,5 +192,3 @@ const AppForm = (props: ComponentProps): JSX.Element => {
</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 { App } from '../../../interfaces/App';
import AppCard from '../AppCard/AppCard';
import { AppCard } from '../AppCard/AppCard';
interface ComponentProps {
interface Props {
apps: App[];
totalApps?: number;
searching: boolean;
}
const AppGrid = (props: ComponentProps): JSX.Element => {
export const AppGrid = (props: Props): JSX.Element => {
let apps: JSX.Element;
if (props.apps.length > 0) {
@ -49,5 +49,3 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
return apps;
};
export default AppGrid;

View file

@ -8,48 +8,45 @@ import {
import { Link } from 'react-router-dom';
// Redux
import { connect } from 'react-redux';
import {
pinApp,
deleteApp,
reorderApps,
updateConfig,
createNotification,
} from '../../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
// Typescript
import { App, Config, GlobalState, NewNotification } from '../../../interfaces';
import { App } from '../../../interfaces';
// CSS
import classes from './AppTable.module.css';
// UI
import Icon from '../../UI/Icons/Icon/Icon';
import Table from '../../UI/Table/Table';
import { Icon, Table } from '../../UI';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps {
apps: App[];
config: Config;
pinApp: (app: App) => void;
deleteApp: (id: number) => void;
interface Props {
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 [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
// Copy apps array
useEffect(() => {
setLocalApps([...props.apps]);
}, [props.apps]);
setLocalApps([...apps]);
}, [apps]);
// Check ordering
useEffect(() => {
const order = props.config.useOrdering;
const order = config.useOrdering;
if (order === 'orderId') {
setIsCustomOrder(true);
@ -62,7 +59,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
);
if (proceed) {
props.deleteApp(app.id);
deleteApp(app.id);
}
};
@ -79,7 +76,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
const dragEndHanlder = (result: DropResult): void => {
if (!isCustomOrder) {
props.createNotification({
createNotification({
title: 'Error',
message: 'Custom order is disabled',
});
@ -95,7 +92,7 @@ const AppTable = (props: ComponentProps): JSX.Element => {
tmpApps.splice(result.destination.index, 0, movedApp);
setLocalApps(tmpApps);
props.reorderApps(tmpApps);
reorderApps(tmpApps);
};
return (
@ -178,9 +175,9 @@ const AppTable = (props: ComponentProps): JSX.Element => {
</div>
<div
className={classes.TableAction}
onClick={() => props.pinApp(app)}
onClick={() => pinApp(app)}
onKeyDown={(e) =>
keyboardActionHandler(e, app, props.pinApp)
keyboardActionHandler(e, app, pinApp)
}
tabIndex={0}
>
@ -208,20 +205,3 @@ const AppTable = (props: ComponentProps): JSX.Element => {
</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';
// Redux
import { connect } from 'react-redux';
import { getApps } from '../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
// Typescript
import { App, GlobalState } from '../../interfaces';
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';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
import Modal from '../UI/Modal/Modal';
import { Headline, Spinner, ActionButton, Modal, Container } from '../UI';
// Subcomponents
import AppGrid from './AppGrid/AppGrid';
import AppForm from './AppForm/AppForm';
import AppTable from './AppTable/AppTable';
import { AppGrid } from './AppGrid/AppGrid';
import { AppForm } from './AppForm/AppForm';
import { AppTable } from './AppTable/AppTable';
// Utils
import { appTemplate } from '../../utility';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
interface ComponentProps {
getApps: Function;
apps: App[];
loading: boolean;
interface Props {
searching: boolean;
}
const Apps = (props: ComponentProps): JSX.Element => {
const { getApps, apps, loading, searching = false } = props;
export const Apps = (props: Props): JSX.Element => {
const { apps, loading } = useSelector((state: State) => state.apps);
const dispatch = useDispatch();
const { getApps } = bindActionCreators(actionCreators, dispatch);
const [modalIsOpen, setModalIsOpen] = useState(false);
const [isInEdit, setIsInEdit] = useState(false);
@ -95,12 +93,3 @@ const Apps = (props: ComponentProps): JSX.Element => {
</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 Icon from '../../UI/Icons/Icon/Icon';
import { iconParser, urlParser } from '../../../utility';
import { Fragment } from 'react';
import { connect } from 'react-redux';
import { Icon } from '../../UI';
interface ComponentProps {
import { iconParser, urlParser } from '../../../utility';
interface Props {
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 (
<div className={classes.BookmarkCard}>
<h3>{props.category.name}</h3>
@ -56,7 +62,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
return (
<a
href={redirectUrl}
target={props.config.bookmarksSameTab ? '' : '_blank'}
target={config.bookmarksSameTab ? '' : '_blank'}
rel="noreferrer"
key={`bookmark-${bookmark.id}`}
>
@ -69,11 +75,3 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
</div>
);
};
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(BookmarkCard);

View file

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

View file

@ -8,45 +8,39 @@ import {
import { Link } from 'react-router-dom';
// Redux
import { connect } from 'react-redux';
import {
pinCategory,
deleteCategory,
deleteBookmark,
createNotification,
reorderCategories,
} from '../../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
// Typescript
import {
Bookmark,
Category,
Config,
GlobalState,
NewNotification,
} from '../../../interfaces';
import { Bookmark, Category } from '../../../interfaces';
import { ContentType } from '../Bookmarks';
// CSS
import classes from './BookmarkTable.module.css';
// UI
import Table from '../../UI/Table/Table';
import Icon from '../../UI/Icons/Icon/Icon';
import { Table, Icon } from '../../UI';
interface ComponentProps {
interface Props {
contentType: ContentType;
categories: Category[];
config: Config;
pinCategory: (category: Category) => void;
deleteCategory: (id: number) => 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 [isCustomOrder, setIsCustomOrder] = useState<boolean>(false);
@ -57,7 +51,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
// Check ordering
useEffect(() => {
const order = props.config.useOrdering;
const order = config.useOrdering;
if (order === 'orderId') {
setIsCustomOrder(true);
@ -70,7 +64,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
);
if (proceed) {
props.deleteCategory(category.id);
deleteCategory(category.id);
}
};
@ -80,7 +74,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
);
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 => {
if (!isCustomOrder) {
props.createNotification({
createNotification({
title: 'Error',
message: 'Custom order is disabled',
});
@ -112,7 +106,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
tmpCategories.splice(result.destination.index, 0, movedApp);
setLocalCategories(tmpCategories);
props.reorderCategories(tmpCategories);
reorderCategories(tmpCategories);
};
if (props.contentType === ContentType.category) {
@ -186,12 +180,12 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
</div>
<div
className={classes.TableAction}
onClick={() => props.pinCategory(category)}
onClick={() => pinCategory(category)}
onKeyDown={(e) =>
keyboardActionHandler(
e,
category,
props.pinCategory
pinCategory
)
}
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 { 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 { Container } from '../UI/Layout/Layout';
import Headline from '../UI/Headlines/Headline/Headline';
import ActionButton from '../UI/Buttons/ActionButton/ActionButton';
// UI
import { Container, Headline, ActionButton, Spinner, Modal } from '../UI';
import BookmarkGrid from './BookmarkGrid/BookmarkGrid';
import { Category, GlobalState, Bookmark } from '../../interfaces';
import Spinner from '../UI/Spinner/Spinner';
import Modal from '../UI/Modal/Modal';
import BookmarkForm from './BookmarkForm/BookmarkForm';
import BookmarkTable from './BookmarkTable/BookmarkTable';
// Components
import { BookmarkGrid } from './BookmarkGrid/BookmarkGrid';
import { BookmarkForm } from './BookmarkForm/BookmarkForm';
import { BookmarkTable } from './BookmarkTable/BookmarkTable';
interface ComponentProps {
loading: boolean;
categories: Category[];
getCategories: () => void;
// Utils
import { bookmarkTemplate, categoryTemplate } from '../../utility';
interface Props {
searching: boolean;
}
@ -28,8 +33,15 @@ export enum ContentType {
bookmark,
}
const Bookmarks = (props: ComponentProps): JSX.Element => {
const { getCategories, categories, loading, searching = false } = props;
export const Bookmarks = (props: Props): JSX.Element => {
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 [formContentType, setFormContentType] = useState(ContentType.category);
@ -38,24 +50,10 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
ContentType.category
);
const [isInUpdate, setIsInUpdate] = useState(false);
const [categoryInUpdate, setCategoryInUpdate] = useState<Category>({
name: '',
id: -1,
isPinned: false,
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(),
});
const [categoryInUpdate, setCategoryInUpdate] =
useState<Category>(categoryTemplate);
const [bookmarkInUpdate, setBookmarkInUpdate] =
useState<Bookmark>(bookmarkTemplate);
useEffect(() => {
if (categories.length === 0) {
@ -161,12 +159,3 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
</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 { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Config, GlobalState } from '../../../interfaces';
import WeatherWidget from '../../Widgets/WeatherWidget/WeatherWidget';
import { getDateTime } from './functions/getDateTime';
import { greeter } from './functions/greeter';
// CSS
import classes from './Header.module.css';
interface Props {
config: Config;
}
// Components
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 [greeting, setGreeting] = useState<string>(greeter());
@ -39,11 +39,3 @@ const Header = (props: Props): JSX.Element => {
</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';
// Redux
import { connect } from 'react-redux';
import { getApps, getCategories } from '../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
// Typescript
import { GlobalState } from '../../interfaces/GlobalState';
import { App, Category, Config } from '../../interfaces';
import { App, Category } from '../../interfaces';
// UI
import Icon from '../UI/Icons/Icon/Icon';
import { Container } from '../UI/Layout/Layout';
import SectionHeadline from '../UI/Headlines/SectionHeadline/SectionHeadline';
import Spinner from '../UI/Spinner/Spinner';
import { Icon, Container, SectionHeadline, Spinner } from '../UI';
// CSS
import classes from './Home.module.css';
// Components
import AppGrid from '../Apps/AppGrid/AppGrid';
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
import SearchBar from '../SearchBar/SearchBar';
import Header from './Header/Header';
import { AppGrid } from '../Apps/AppGrid/AppGrid';
import { BookmarkGrid } from '../Bookmarks/BookmarkGrid/BookmarkGrid';
import { SearchBar } from '../SearchBar/SearchBar';
import { Header } from './Header/Header';
interface ComponentProps {
getApps: Function;
getCategories: Function;
appsLoading: boolean;
apps: App[];
categoriesLoading: boolean;
categories: Category[];
config: Config;
}
const Home = (props: ComponentProps): JSX.Element => {
export const Home = (): JSX.Element => {
const {
getApps,
apps,
appsLoading,
getCategories,
categories,
categoriesLoading,
} = props;
apps: { apps, loading: appsLoading },
bookmarks: { categories, loading: bookmarksLoading },
config: { config },
} = useSelector((state: State) => state);
const dispatch = useDispatch();
const { getApps, getCategories } = bindActionCreators(
actionCreators,
dispatch
);
// Local search query
const [localSearch, setLocalSearch] = useState<null | string>(null);
@ -90,7 +81,7 @@ const Home = (props: ComponentProps): JSX.Element => {
return (
<Container>
{!props.config.hideSearch ? (
{!config.hideSearch ? (
<SearchBar
setLocalSearch={setLocalSearch}
appSearchResult={appSearchResult}
@ -100,9 +91,9 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div>
)}
{!props.config.hideHeader ? <Header /> : <div></div>}
{!config.hideHeader ? <Header /> : <div></div>}
{!props.config.hideApps ? (
{!config.hideApps ? (
<Fragment>
<SectionHeadline title="Applications" link="/applications" />
{appsLoading ? (
@ -124,10 +115,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div>
)}
{!props.config.hideCategories ? (
{!config.hideCategories ? (
<Fragment>
<SectionHeadline title="Bookmarks" link="/bookmarks" />
{categoriesLoading ? (
{bookmarksLoading ? (
<Spinner />
) : (
<BookmarkGrid
@ -151,15 +142,3 @@ const Home = (props: ComponentProps): JSX.Element => {
</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 { GlobalState, Notification as _Notification } from '../../interfaces';
import { useSelector } from 'react-redux';
import { Notification as NotificationInterface } from '../../interfaces';
import classes from './NotificationCenter.module.css';
import Notification from '../UI/Notification/Notification';
import { Notification } from '../UI';
import { State } from '../../store/reducers';
interface ComponentProps {
notifications: _Notification[];
}
export const NotificationCenter = (): JSX.Element => {
const { notifications } = useSelector((state: State) => state.notification);
const NotificationCenter = (props: ComponentProps): JSX.Element => {
return (
<div
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 (
<Notification
title={notification.title}
@ -29,11 +28,3 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => {
</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';
// Redux
import { connect } from 'react-redux';
import { createNotification } from '../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
// Typescript
import {
App,
Category,
Config,
GlobalState,
NewNotification,
} from '../../interfaces';
import { App, Category } from '../../interfaces';
// CSS
import classes from './SearchBar.module.css';
// Utils
import { searchParser, urlParser, redirectUrl } from '../../utility';
import { State } from '../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../store';
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
interface Props {
setLocalSearch: (query: string) => void;
appSearchResult: App[] | null;
bookmarkSearchResult: Category[] | null;
config: Config;
loading: boolean;
}
const SearchBar = (props: ComponentProps): JSX.Element => {
const {
setLocalSearch,
createNotification,
config,
loading,
appSearchResult,
bookmarkSearchResult,
} = props;
export const SearchBar = (props: Props): JSX.Element => {
const { config, loading } = useSelector((state: State) => state.config);
const dispatch = useDispatch();
const { createNotification } = bindActionCreators(actionCreators, dispatch);
const { setLocalSearch, appSearchResult, bookmarkSearchResult } = props;
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
@ -126,12 +117,3 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
</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 classes from './AppDetails.module.css';
import Button from '../../UI/Buttons/Button/Button';
import { Button } from '../../UI';
import { checkVersion } from '../../../utility';
const AppDetails = (): JSX.Element => {
export const AppDetails = (): JSX.Element => {
return (
<Fragment>
<p className={classes.AppVersion}>
<a
href='https://github.com/pawelmalak/flame'
target='_blank'
rel='noreferrer'>
href="https://github.com/pawelmalak/flame"
target="_blank"
rel="noreferrer"
>
Flame
</a>
{' '}
</a>{' '}
version {process.env.REACT_APP_VERSION}
</p>
<p className={classes.AppVersion}>
See changelog {' '}
See changelog{' '}
<a
href='https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md'
target='_blank'
rel='noreferrer'>
href="https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md"
target="_blank"
rel="noreferrer"
>
here
</a>
</p>
<Button click={() => checkVersion(true)}>Check for updates</Button>
</Fragment>
)
}
export default AppDetails;
);
};

View file

@ -1,41 +1,28 @@
import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
// Redux
import { connect } from 'react-redux';
import {
createNotification,
updateConfig,
sortApps,
sortCategories,
} from '../../../store/actions';
import { useDispatch, useSelector } from 'react-redux';
// Typescript
import {
Config,
GlobalState,
NewNotification,
OtherSettingsForm,
} from '../../../interfaces';
import { OtherSettingsForm } from '../../../interfaces';
// UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
import { InputGroup, Button, SettingsHeadline } from '../../UI';
// Utils
import { otherSettingsTemplate, inputHandler } from '../../../utility';
import { State } from '../../../store/reducers';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../store';
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
updateConfig: (formData: OtherSettingsForm) => void;
sortApps: () => void;
sortCategories: () => void;
loading: boolean;
config: Config;
}
export const OtherSettings = (): JSX.Element => {
const { loading, config } = useSelector((state: State) => state.config);
const OtherSettings = (props: ComponentProps): JSX.Element => {
const { config } = props;
const dispatch = useDispatch();
const { updateConfig, sortApps, sortCategories } = bindActionCreators(
actionCreators,
dispatch
);
// Initial state
const [formData, setFormData] = useState<OtherSettingsForm>(
@ -47,21 +34,21 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
setFormData({
...config,
});
}, [props.loading]);
}, [loading]);
// Form handler
const formSubmitHandler = async (e: FormEvent) => {
e.preventDefault();
// Save settings
await props.updateConfig(formData);
await updateConfig(formData);
// Update local page title
document.title = formData.customTitle;
// Sort apps and categories with new settings
props.sortApps();
props.sortCategories();
sortApps();
sortCategories();
};
// Input handler
@ -338,19 +325,3 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</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 { 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 Modal from '../../../UI/Modal/Modal';
import Icon from '../../../UI/Icons/Icon/Icon';
import {
Config,
GlobalState,
NewNotification,
Query,
} from '../../../../interfaces';
import QueriesForm from './QueriesForm';
import { deleteQuery, createNotification } from '../../../../store/actions';
import Button from '../../../UI/Buttons/Button/Button';
// UI
import { Modal, Icon, Button } from '../../../UI';
interface Props {
customQueries: Query[];
deleteQuery: (prefix: string) => {};
createNotification: (notification: NewNotification) => void;
config: Config;
}
// Components
import { QueriesForm } from './QueriesForm';
const CustomQueries = (props: Props): JSX.Element => {
const { customQueries, deleteQuery, createNotification } = props;
export const CustomQueries = (): JSX.Element => {
const { customQueries, config } = useSelector((state: State) => state.config);
const dispatch = useDispatch();
const { deleteQuery, createNotification } = bindActionCreators(
actionCreators,
dispatch
);
const [modalIsOpen, setModalIsOpen] = useState(false);
const [editableQuery, setEditableQuery] = useState<Query | null>(null);
@ -34,7 +36,7 @@ const CustomQueries = (props: Props): JSX.Element => {
};
const deleteHandler = (query: Query) => {
const currentProvider = props.config.defaultSearchProvider;
const currentProvider = config.defaultSearchProvider;
const isCurrent = currentProvider === query.prefix;
if (isCurrent) {
@ -105,14 +107,3 @@ const CustomQueries = (props: Props): JSX.Element => {
</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 { useDispatch } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actionCreators } from '../../../../store';
import { Query } from '../../../../interfaces';
import Button from '../../../UI/Buttons/Button/Button';
import InputGroup from '../../../UI/Forms/InputGroup/InputGroup';
import ModalForm from '../../../UI/Forms/ModalForm/ModalForm';
import { connect } from 'react-redux';
import { addQuery, updateQuery } from '../../../../store/actions';
import { Button, InputGroup, ModalForm } from '../../../UI';
interface Props {
modalHandler: () => void;
addQuery: (query: Query) => {};
updateQuery: (query: Query, Oldprefix: string) => {};
query?: Query;
}
const QueriesForm = (props: Props): JSX.Element => {
const { modalHandler, addQuery, updateQuery, query } = props;
export const QueriesForm = (props: Props): JSX.Element => {
const dispatch = useDispatch();
const { addQuery, updateQuery } = bindActionCreators(
actionCreators,
dispatch
);
const { modalHandler, query } = props;
const [formData, setFormData] = useState<Query>({
name: '',
@ -77,6 +83,7 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="name">Prefix</label>
<input
@ -89,6 +96,7 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor="name">Query Template</label>
<input
@ -101,9 +109,8 @@ const QueriesForm = (props: Props): JSX.Element => {
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
{query ? <Button>Update provider</Button> : <Button>Add provider</Button>}
</ModalForm>
);
};
export default connect(null, { addQuery, updateQuery })(QueriesForm);

View file

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

View file

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

View file

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

View file

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

View file

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