mirror of
https://github.com/pawelmalak/flame.git
synced 2024-12-24 18:52:37 +03:00
Client: Implemented new config system
This commit is contained in:
parent
34279c8b8c
commit
76e50624e7
client/src
components
Apps
Bookmarks
Home
Settings
OtherSettings
SearchSettings
WeatherSettings
Widgets/WeatherWidget
interfaces
utility
@ -2,12 +2,13 @@ import classes from './AppCard.module.css';
|
||||
import Icon from '../../UI/Icons/Icon/Icon';
|
||||
import { iconParser, urlParser } from '../../../utility';
|
||||
|
||||
import { App } from '../../../interfaces';
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { App, Config, GlobalState } from '../../../interfaces';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface ComponentProps {
|
||||
app: App;
|
||||
pinHandler?: Function;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const AppCard = (props: ComponentProps): JSX.Element => {
|
||||
@ -29,7 +30,7 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||
<div className={classes.CustomIcon}>
|
||||
<svg
|
||||
data-src={`/uploads/${icon}`}
|
||||
fill='var(--color-primary)'
|
||||
fill="var(--color-primary)"
|
||||
className={classes.CustomIcon}
|
||||
></svg>
|
||||
</div>
|
||||
@ -41,8 +42,8 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
href={redirectUrl}
|
||||
target={searchConfig('appsSameTab', false) ? '' : '_blank'}
|
||||
rel='noreferrer'
|
||||
target={props.config.appsSameTab ? '' : '_blank'}
|
||||
rel="noreferrer"
|
||||
className={classes.AppCard}
|
||||
>
|
||||
<div className={classes.AppCardIcon}>{iconEl}</div>
|
||||
@ -54,4 +55,10 @@ const AppCard = (props: ComponentProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppCard;
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(AppCard);
|
||||
|
@ -1,13 +1,24 @@
|
||||
import { Fragment, KeyboardEvent, useState, useEffect } from 'react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
Draggable,
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// Redux
|
||||
import { connect } from 'react-redux';
|
||||
import { pinApp, deleteApp, reorderApps, updateConfig, createNotification } from '../../../store/actions';
|
||||
import {
|
||||
pinApp,
|
||||
deleteApp,
|
||||
reorderApps,
|
||||
updateConfig,
|
||||
createNotification,
|
||||
} from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { App, GlobalState, NewNotification } from '../../../interfaces';
|
||||
import { App, Config, GlobalState, NewNotification } from '../../../interfaces';
|
||||
|
||||
// CSS
|
||||
import classes from './AppTable.module.css';
|
||||
@ -16,11 +27,9 @@ import classes from './AppTable.module.css';
|
||||
import Icon from '../../UI/Icons/Icon/Icon';
|
||||
import Table from '../../UI/Table/Table';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
apps: App[];
|
||||
config: Config;
|
||||
pinApp: (app: App) => void;
|
||||
deleteApp: (id: number) => void;
|
||||
updateAppHandler: (app: App) => void;
|
||||
@ -36,38 +45,44 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||
// Copy apps array
|
||||
useEffect(() => {
|
||||
setLocalApps([...props.apps]);
|
||||
}, [props.apps])
|
||||
}, [props.apps]);
|
||||
|
||||
// Check ordering
|
||||
useEffect(() => {
|
||||
const order = searchConfig('useOrdering', '');
|
||||
const order = props.config.useOrdering;
|
||||
|
||||
if (order === 'orderId') {
|
||||
setIsCustomOrder(true);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const deleteAppHandler = (app: App): void => {
|
||||
const proceed = window.confirm(`Are you sure you want to delete ${app.name} at ${app.url} ?`);
|
||||
const proceed = window.confirm(
|
||||
`Are you sure you want to delete ${app.name} at ${app.url} ?`
|
||||
);
|
||||
|
||||
if (proceed) {
|
||||
props.deleteApp(app.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Support keyboard navigation for actions
|
||||
const keyboardActionHandler = (e: KeyboardEvent, app: App, handler: Function) => {
|
||||
const keyboardActionHandler = (
|
||||
e: KeyboardEvent,
|
||||
app: App,
|
||||
handler: Function
|
||||
) => {
|
||||
if (e.key === 'Enter') {
|
||||
handler(app);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dragEndHanlder = (result: DropResult): void => {
|
||||
if (!isCustomOrder) {
|
||||
props.createNotification({
|
||||
title: 'Error',
|
||||
message: 'Custom order is disabled'
|
||||
})
|
||||
message: 'Custom order is disabled',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -81,32 +96,39 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||
|
||||
setLocalApps(tmpApps);
|
||||
props.reorderApps(tmpApps);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.Message}>
|
||||
{isCustomOrder
|
||||
? <p>You can drag and drop single rows to reorder application</p>
|
||||
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
|
||||
}
|
||||
{isCustomOrder ? (
|
||||
<p>You can drag and drop single rows to reorder application</p>
|
||||
) : (
|
||||
<p>
|
||||
Custom order is disabled. You can change it in{' '}
|
||||
<Link to="/settings/other">settings</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DragDropContext onDragEnd={dragEndHanlder}>
|
||||
<Droppable droppableId='apps'>
|
||||
<Droppable droppableId="apps">
|
||||
{(provided) => (
|
||||
<Table headers={[
|
||||
'Name',
|
||||
'URL',
|
||||
'Icon',
|
||||
'Actions'
|
||||
]}
|
||||
innerRef={provided.innerRef}>
|
||||
<Table
|
||||
headers={['Name', 'URL', 'Icon', 'Actions']}
|
||||
innerRef={provided.innerRef}
|
||||
>
|
||||
{localApps.map((app: App, index): JSX.Element => {
|
||||
return (
|
||||
<Draggable key={app.id} draggableId={app.id.toString()} index={index}>
|
||||
<Draggable
|
||||
key={app.id}
|
||||
draggableId={app.id.toString()}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => {
|
||||
const style = {
|
||||
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
|
||||
border: snapshot.isDragging
|
||||
? '1px solid var(--color-accent)'
|
||||
: 'none',
|
||||
borderRadius: '4px',
|
||||
...provided.draggableProps.style,
|
||||
};
|
||||
@ -118,63 +140,85 @@ const AppTable = (props: ComponentProps): JSX.Element => {
|
||||
ref={provided.innerRef}
|
||||
style={style}
|
||||
>
|
||||
<td style={{ width:'200px' }}>{app.name}</td>
|
||||
<td style={{ width:'200px' }}>{app.url}</td>
|
||||
<td style={{ width:'200px' }}>{app.icon}</td>
|
||||
<td style={{ width: '200px' }}>{app.name}</td>
|
||||
<td style={{ width: '200px' }}>{app.url}</td>
|
||||
<td style={{ width: '200px' }}>{app.icon}</td>
|
||||
{!snapshot.isDragging && (
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteAppHandler(app)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, app, deleteAppHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
onKeyDown={(e) =>
|
||||
keyboardActionHandler(
|
||||
e,
|
||||
app,
|
||||
deleteAppHandler
|
||||
)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiDelete" />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateAppHandler(app)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, app, props.updateAppHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
onKeyDown={(e) =>
|
||||
keyboardActionHandler(
|
||||
e,
|
||||
app,
|
||||
props.updateAppHandler
|
||||
)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiPencil" />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.pinApp(app)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, app, props.pinApp)}
|
||||
tabIndex={0}>
|
||||
{app.isPinned
|
||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
||||
: <Icon icon='mdiPin' />
|
||||
onKeyDown={(e) =>
|
||||
keyboardActionHandler(e, app, props.pinApp)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{app.isPinned ? (
|
||||
<Icon
|
||||
icon="mdiPinOff"
|
||||
color="var(--color-accent)"
|
||||
/>
|
||||
) : (
|
||||
<Icon icon="mdiPin" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
apps: state.app.apps
|
||||
}
|
||||
}
|
||||
apps: state.app.apps,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
const actions = {
|
||||
pinApp,
|
||||
deleteApp,
|
||||
reorderApps,
|
||||
updateConfig,
|
||||
createNotification
|
||||
}
|
||||
createNotification,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, actions)(AppTable);
|
@ -1,12 +1,14 @@
|
||||
import { Bookmark, Category } from '../../../interfaces';
|
||||
import { Bookmark, Category, Config, GlobalState } from '../../../interfaces';
|
||||
import classes from './BookmarkCard.module.css';
|
||||
|
||||
import Icon from '../../UI/Icons/Icon/Icon';
|
||||
import { iconParser, urlParser, searchConfig } from '../../../utility';
|
||||
import { iconParser, urlParser } from '../../../utility';
|
||||
import { Fragment } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface ComponentProps {
|
||||
category: Category;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||
@ -54,7 +56,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
href={redirectUrl}
|
||||
target={searchConfig('bookmarksSameTab', false) ? '' : '_blank'}
|
||||
target={props.config.bookmarksSameTab ? '' : '_blank'}
|
||||
rel="noreferrer"
|
||||
key={`bookmark-${bookmark.id}`}
|
||||
>
|
||||
@ -68,4 +70,10 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkCard;
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(BookmarkCard);
|
||||
|
@ -1,13 +1,30 @@
|
||||
import { KeyboardEvent, useState, useEffect, Fragment } from 'react';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
import {
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
Draggable,
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
// Redux
|
||||
import { connect } from 'react-redux';
|
||||
import { pinCategory, deleteCategory, deleteBookmark, createNotification, reorderCategories } from '../../../store/actions';
|
||||
import {
|
||||
pinCategory,
|
||||
deleteCategory,
|
||||
deleteBookmark,
|
||||
createNotification,
|
||||
reorderCategories,
|
||||
} from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { Bookmark, Category, NewNotification } from '../../../interfaces';
|
||||
import {
|
||||
Bookmark,
|
||||
Category,
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
} from '../../../interfaces';
|
||||
import { ContentType } from '../Bookmarks';
|
||||
|
||||
// CSS
|
||||
@ -17,12 +34,10 @@ import classes from './BookmarkTable.module.css';
|
||||
import Table from '../../UI/Table/Table';
|
||||
import Icon from '../../UI/Icons/Icon/Icon';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
contentType: ContentType;
|
||||
categories: Category[];
|
||||
config: Config;
|
||||
pinCategory: (category: Category) => void;
|
||||
deleteCategory: (id: number) => void;
|
||||
updateHandler: (data: Category | Bookmark) => void;
|
||||
@ -38,45 +53,53 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
// Copy categories array
|
||||
useEffect(() => {
|
||||
setLocalCategories([...props.categories]);
|
||||
}, [props.categories])
|
||||
}, [props.categories]);
|
||||
|
||||
// Check ordering
|
||||
useEffect(() => {
|
||||
const order = searchConfig('useOrdering', '');
|
||||
const order = props.config.useOrdering;
|
||||
|
||||
if (order === 'orderId') {
|
||||
setIsCustomOrder(true);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const deleteCategoryHandler = (category: Category): void => {
|
||||
const proceed = window.confirm(`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`);
|
||||
const proceed = window.confirm(
|
||||
`Are you sure you want to delete ${category.name}? It will delete ALL assigned bookmarks`
|
||||
);
|
||||
|
||||
if (proceed) {
|
||||
props.deleteCategory(category.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteBookmarkHandler = (bookmark: Bookmark): void => {
|
||||
const proceed = window.confirm(`Are you sure you want to delete ${bookmark.name}?`);
|
||||
const proceed = window.confirm(
|
||||
`Are you sure you want to delete ${bookmark.name}?`
|
||||
);
|
||||
|
||||
if (proceed) {
|
||||
props.deleteBookmark(bookmark.id, bookmark.categoryId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const keyboardActionHandler = (e: KeyboardEvent, category: Category, handler: Function) => {
|
||||
const keyboardActionHandler = (
|
||||
e: KeyboardEvent,
|
||||
category: Category,
|
||||
handler: Function
|
||||
) => {
|
||||
if (e.key === 'Enter') {
|
||||
handler(category);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const dragEndHanlder = (result: DropResult): void => {
|
||||
if (!isCustomOrder) {
|
||||
props.createNotification({
|
||||
title: 'Error',
|
||||
message: 'Custom order is disabled'
|
||||
})
|
||||
message: 'Custom order is disabled',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -90,136 +113,171 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
|
||||
|
||||
setLocalCategories(tmpCategories);
|
||||
props.reorderCategories(tmpCategories);
|
||||
}
|
||||
};
|
||||
|
||||
if (props.contentType === ContentType.category) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={classes.Message}>
|
||||
{isCustomOrder
|
||||
? <p>You can drag and drop single rows to reorder categories</p>
|
||||
: <p>Custom order is disabled. You can change it in <Link to='/settings/other'>settings</Link></p>
|
||||
}
|
||||
{isCustomOrder ? (
|
||||
<p>You can drag and drop single rows to reorder categories</p>
|
||||
) : (
|
||||
<p>
|
||||
Custom order is disabled. You can change it in{' '}
|
||||
<Link to="/settings/other">settings</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DragDropContext onDragEnd={dragEndHanlder}>
|
||||
<Droppable droppableId='categories'>
|
||||
<Droppable droppableId="categories">
|
||||
{(provided) => (
|
||||
<Table headers={[
|
||||
'Name',
|
||||
'Actions'
|
||||
]}
|
||||
innerRef={provided.innerRef}>
|
||||
{localCategories.map((category: Category, index): JSX.Element => {
|
||||
return (
|
||||
<Draggable key={category.id} draggableId={category.id.toString()} index={index}>
|
||||
{(provided, snapshot) => {
|
||||
const style = {
|
||||
border: snapshot.isDragging ? '1px solid var(--color-accent)' : 'none',
|
||||
borderRadius: '4px',
|
||||
...provided.draggableProps.style,
|
||||
};
|
||||
<Table headers={['Name', 'Actions']} innerRef={provided.innerRef}>
|
||||
{localCategories.map(
|
||||
(category: Category, index): JSX.Element => {
|
||||
return (
|
||||
<Draggable
|
||||
key={category.id}
|
||||
draggableId={category.id.toString()}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => {
|
||||
const style = {
|
||||
border: snapshot.isDragging
|
||||
? '1px solid var(--color-accent)'
|
||||
: 'none',
|
||||
borderRadius: '4px',
|
||||
...provided.draggableProps.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={style}
|
||||
>
|
||||
<td>{category.name}</td>
|
||||
{!snapshot.isDragging && (
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteCategoryHandler(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, deleteCategoryHandler)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(category)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.pinCategory(category)}
|
||||
onKeyDown={(e) => keyboardActionHandler(e, category, props.pinCategory)}
|
||||
tabIndex={0}>
|
||||
{category.isPinned
|
||||
? <Icon icon='mdiPinOff' color='var(--color-accent)' />
|
||||
: <Icon icon='mdiPin' />
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</Draggable>
|
||||
)
|
||||
})}
|
||||
return (
|
||||
<tr
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
style={style}
|
||||
>
|
||||
<td>{category.name}</td>
|
||||
{!snapshot.isDragging && (
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() =>
|
||||
deleteCategoryHandler(category)
|
||||
}
|
||||
onKeyDown={(e) =>
|
||||
keyboardActionHandler(
|
||||
e,
|
||||
category,
|
||||
deleteCategoryHandler
|
||||
)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiDelete" />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() =>
|
||||
props.updateHandler(category)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiPencil" />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.pinCategory(category)}
|
||||
onKeyDown={(e) =>
|
||||
keyboardActionHandler(
|
||||
e,
|
||||
category,
|
||||
props.pinCategory
|
||||
)
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
{category.isPinned ? (
|
||||
<Icon
|
||||
icon="mdiPinOff"
|
||||
color="var(--color-accent)"
|
||||
/>
|
||||
) : (
|
||||
<Icon icon="mdiPin" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}}
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Fragment>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
const bookmarks: {bookmark: Bookmark, categoryName: string}[] = [];
|
||||
const bookmarks: { bookmark: Bookmark; categoryName: string }[] = [];
|
||||
props.categories.forEach((category: Category) => {
|
||||
category.bookmarks.forEach((bookmark: Bookmark) => {
|
||||
bookmarks.push({
|
||||
bookmark,
|
||||
categoryName: category.name
|
||||
categoryName: category.name,
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Table headers={[
|
||||
'Name',
|
||||
'URL',
|
||||
'Icon',
|
||||
'Category',
|
||||
'Actions'
|
||||
]}>
|
||||
{bookmarks.map((bookmark: {bookmark: Bookmark, categoryName: string}) => {
|
||||
return (
|
||||
<tr key={bookmark.bookmark.id}>
|
||||
<td>{bookmark.bookmark.name}</td>
|
||||
<td>{bookmark.bookmark.url}</td>
|
||||
<td>{bookmark.bookmark.icon}</td>
|
||||
<td>{bookmark.categoryName}</td>
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiDelete' />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(bookmark.bookmark)}
|
||||
tabIndex={0}>
|
||||
<Icon icon='mdiPencil' />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
<Table headers={['Name', 'URL', 'Icon', 'Category', 'Actions']}>
|
||||
{bookmarks.map(
|
||||
(bookmark: { bookmark: Bookmark; categoryName: string }) => {
|
||||
return (
|
||||
<tr key={bookmark.bookmark.id}>
|
||||
<td>{bookmark.bookmark.name}</td>
|
||||
<td>{bookmark.bookmark.url}</td>
|
||||
<td>{bookmark.bookmark.icon}</td>
|
||||
<td>{bookmark.categoryName}</td>
|
||||
<td className={classes.TableActions}>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => deleteBookmarkHandler(bookmark.bookmark)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiDelete" />
|
||||
</div>
|
||||
<div
|
||||
className={classes.TableAction}
|
||||
onClick={() => props.updateHandler(bookmark.bookmark)}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon icon="mdiPencil" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Table>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
const actions = {
|
||||
pinCategory,
|
||||
deleteCategory,
|
||||
deleteBookmark,
|
||||
createNotification,
|
||||
reorderCategories
|
||||
}
|
||||
reorderCategories,
|
||||
};
|
||||
|
||||
export default connect(null, actions)(BookmarkTable);
|
||||
export default connect(mapStateToProps, actions)(BookmarkTable);
|
||||
|
@ -7,7 +7,7 @@ import { getApps, getCategories } from '../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { GlobalState } from '../../interfaces/GlobalState';
|
||||
import { App, Category } from '../../interfaces';
|
||||
import { App, Category, Config } from '../../interfaces';
|
||||
|
||||
// UI
|
||||
import Icon from '../UI/Icons/Icon/Icon';
|
||||
@ -28,9 +28,6 @@ import SearchBar from '../SearchBar/SearchBar';
|
||||
import { greeter } from './functions/greeter';
|
||||
import { dateTime } from './functions/dateTime';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
getApps: Function;
|
||||
getCategories: Function;
|
||||
@ -38,6 +35,7 @@ interface ComponentProps {
|
||||
apps: App[];
|
||||
categoriesLoading: boolean;
|
||||
categories: Category[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const Home = (props: ComponentProps): JSX.Element => {
|
||||
@ -77,7 +75,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||
let interval: any;
|
||||
|
||||
// Start interval only when hideHeader is false
|
||||
if (searchConfig('hideHeader', 0) !== 1) {
|
||||
if (!props.config.hideHeader) {
|
||||
interval = setInterval(() => {
|
||||
setHeader({
|
||||
dateTime: dateTime(),
|
||||
@ -103,13 +101,13 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{searchConfig('hideSearch', 0) !== 1 ? (
|
||||
{!props.config.hideSearch ? (
|
||||
<SearchBar setLocalSearch={setLocalSearch} />
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{searchConfig('hideHeader', 0) !== 1 ? (
|
||||
{!props.config.hideHeader ? (
|
||||
<header className={classes.Header}>
|
||||
<p>{header.dateTime}</p>
|
||||
<Link to="/settings" className={classes.SettingsLink}>
|
||||
@ -124,7 +122,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{searchConfig('hideApps', 0) !== 1 ? (
|
||||
{!props.config.hideApps ? (
|
||||
<Fragment>
|
||||
<SectionHeadline title="Applications" link="/applications" />
|
||||
{appsLoading ? (
|
||||
@ -148,7 +146,7 @@ const Home = (props: ComponentProps): JSX.Element => {
|
||||
<div></div>
|
||||
)}
|
||||
|
||||
{searchConfig('hideCategories', 0) !== 1 ? (
|
||||
{!props.config.hideCategories ? (
|
||||
<Fragment>
|
||||
<SectionHeadline title="Bookmarks" link="/bookmarks" />
|
||||
{categoriesLoading ? (
|
||||
@ -182,6 +180,7 @@ const mapStateToProps = (state: GlobalState) => {
|
||||
apps: state.app.apps,
|
||||
categoriesLoading: state.bookmark.loading,
|
||||
categories: state.bookmark.categories,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -11,9 +11,10 @@ import {
|
||||
|
||||
// Typescript
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
SettingsForm,
|
||||
OtherSettingsForm,
|
||||
} from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
@ -22,50 +23,29 @@ import Button from '../../UI/Buttons/Button/Button';
|
||||
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { otherSettingsTemplate, inputHandler } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
updateConfig: (formData: SettingsForm) => void;
|
||||
updateConfig: (formData: OtherSettingsForm) => void;
|
||||
sortApps: () => void;
|
||||
sortCategories: () => void;
|
||||
loading: boolean;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
const { config } = props;
|
||||
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<SettingsForm>({
|
||||
customTitle: document.title,
|
||||
pinAppsByDefault: 1,
|
||||
pinCategoriesByDefault: 1,
|
||||
hideHeader: 0,
|
||||
hideApps: 0,
|
||||
hideCategories: 0,
|
||||
useOrdering: 'createdAt',
|
||||
appsSameTab: 0,
|
||||
bookmarksSameTab: 0,
|
||||
dockerApps: 1,
|
||||
dockerHost: 'localhost',
|
||||
kubernetesApps: 1,
|
||||
unpinStoppedApps: 1,
|
||||
});
|
||||
const [formData, setFormData] = useState<OtherSettingsForm>(
|
||||
otherSettingsTemplate
|
||||
);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
customTitle: searchConfig('customTitle', 'Flame'),
|
||||
pinAppsByDefault: searchConfig('pinAppsByDefault', 1),
|
||||
pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1),
|
||||
hideHeader: searchConfig('hideHeader', 0),
|
||||
hideApps: searchConfig('hideApps', 0),
|
||||
hideCategories: searchConfig('hideCategories', 0),
|
||||
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
||||
appsSameTab: searchConfig('appsSameTab', 0),
|
||||
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
||||
dockerApps: searchConfig('dockerApps', 0),
|
||||
dockerHost: searchConfig('dockerHost', 'localhost'),
|
||||
kubernetesApps: searchConfig('kubernetesApps', 0),
|
||||
unpinStoppedApps: searchConfig('unpinStoppedApps', 0),
|
||||
...config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
@ -87,17 +67,13 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
// Input handler
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
isNumber?: boolean
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value,
|
||||
inputHandler<OtherSettingsForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
@ -126,8 +102,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="pinAppsByDefault"
|
||||
name="pinAppsByDefault"
|
||||
value={formData.pinAppsByDefault}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.pinAppsByDefault ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -140,8 +116,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="pinCategoriesByDefault"
|
||||
name="pinCategoriesByDefault"
|
||||
value={formData.pinCategoriesByDefault}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.pinCategoriesByDefault ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -165,8 +141,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="appsSameTab"
|
||||
name="appsSameTab"
|
||||
value={formData.appsSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.appsSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -177,8 +153,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="bookmarksSameTab"
|
||||
name="bookmarksSameTab"
|
||||
value={formData.bookmarksSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.bookmarksSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -192,8 +168,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideHeader"
|
||||
name="hideHeader"
|
||||
value={formData.hideHeader}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideHeader ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -204,8 +180,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideApps"
|
||||
name="hideApps"
|
||||
value={formData.hideApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -216,8 +192,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="hideCategories"
|
||||
name="hideCategories"
|
||||
value={formData.hideCategories}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideCategories ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -242,8 +218,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="dockerApps"
|
||||
name="dockerApps"
|
||||
value={formData.dockerApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.dockerApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -256,8 +232,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="unpinStoppedApps"
|
||||
name="unpinStoppedApps"
|
||||
value={formData.unpinStoppedApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.unpinStoppedApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -271,8 +247,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
<select
|
||||
id="kubernetesApps"
|
||||
name="kubernetesApps"
|
||||
value={formData.kubernetesApps}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.kubernetesApps ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -286,6 +262,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -5,16 +5,21 @@ import classes from './CustomQueries.module.css';
|
||||
|
||||
import Modal from '../../../UI/Modal/Modal';
|
||||
import Icon from '../../../UI/Icons/Icon/Icon';
|
||||
import { GlobalState, NewNotification, Query } from '../../../../interfaces';
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Query,
|
||||
} from '../../../../interfaces';
|
||||
import QueriesForm from './QueriesForm';
|
||||
import { deleteQuery, createNotification } from '../../../../store/actions';
|
||||
import Button from '../../../UI/Buttons/Button/Button';
|
||||
import { searchConfig } from '../../../../utility';
|
||||
|
||||
interface Props {
|
||||
customQueries: Query[];
|
||||
deleteQuery: (prefix: string) => {};
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const CustomQueries = (props: Props): JSX.Element => {
|
||||
@ -29,7 +34,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const deleteHandler = (query: Query) => {
|
||||
const currentProvider = searchConfig('defaultSearchProvider', 'l');
|
||||
const currentProvider = props.config.defaultSearchProvider;
|
||||
const isCurrent = currentProvider === query.prefix;
|
||||
|
||||
if (isCurrent) {
|
||||
@ -104,6 +109,7 @@ const CustomQueries = (props: Props): JSX.Element => {
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
customQueries: state.config.customQueries,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { createNotification, updateConfig } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import {
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Query,
|
||||
@ -22,7 +23,7 @@ import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadli
|
||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { inputHandler, searchSettingsTemplate } from '../../../utility';
|
||||
|
||||
// Data
|
||||
import { queries } from '../../../utility/searchQueries.json';
|
||||
@ -32,22 +33,17 @@ interface Props {
|
||||
updateConfig: (formData: SearchForm) => void;
|
||||
loading: boolean;
|
||||
customQueries: Query[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const SearchSettings = (props: Props): JSX.Element => {
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<SearchForm>({
|
||||
hideSearch: 0,
|
||||
defaultSearchProvider: 'l',
|
||||
searchSameTab: 0,
|
||||
});
|
||||
const [formData, setFormData] = useState<SearchForm>(searchSettingsTemplate);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
hideSearch: searchConfig('hideSearch', 0),
|
||||
defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'),
|
||||
searchSameTab: searchConfig('searchSameTab', 0),
|
||||
...props.config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
@ -62,17 +58,13 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
// Input handler
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
isNumber?: boolean
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value,
|
||||
inputHandler<SearchForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
@ -110,8 +102,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
<select
|
||||
id="searchSameTab"
|
||||
name="searchSameTab"
|
||||
value={formData.searchSameTab}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.searchSameTab ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -122,8 +114,8 @@ const SearchSettings = (props: Props): JSX.Element => {
|
||||
<select
|
||||
id="hideSearch"
|
||||
name="hideSearch"
|
||||
value={formData.hideSearch}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.hideSearch ? 1 : 0}
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
>
|
||||
<option value={1}>True</option>
|
||||
<option value={0}>False</option>
|
||||
@ -143,6 +135,7 @@ const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading,
|
||||
customQueries: state.config.customQueries,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -6,38 +6,40 @@ import { connect } from 'react-redux';
|
||||
import { createNotification, updateConfig } from '../../../store/actions';
|
||||
|
||||
// Typescript
|
||||
import { ApiResponse, GlobalState, NewNotification, Weather, WeatherForm } from '../../../interfaces';
|
||||
import {
|
||||
ApiResponse,
|
||||
Config,
|
||||
GlobalState,
|
||||
NewNotification,
|
||||
Weather,
|
||||
WeatherForm,
|
||||
} from '../../../interfaces';
|
||||
|
||||
// UI
|
||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||
import Button from '../../UI/Buttons/Button/Button';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
import { inputHandler, weatherSettingsTemplate } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
createNotification: (notification: NewNotification) => void;
|
||||
updateConfig: (formData: WeatherForm) => void;
|
||||
loading: boolean;
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||
// Initial state
|
||||
const [formData, setFormData] = useState<WeatherForm>({
|
||||
WEATHER_API_KEY: '',
|
||||
lat: 0,
|
||||
long: 0,
|
||||
isCelsius: 1
|
||||
})
|
||||
const [formData, setFormData] = useState<WeatherForm>(
|
||||
weatherSettingsTemplate
|
||||
);
|
||||
|
||||
// Get config
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
WEATHER_API_KEY: searchConfig('WEATHER_API_KEY', ''),
|
||||
lat: searchConfig('lat', 0),
|
||||
long: searchConfig('long', 0),
|
||||
isCelsius: searchConfig('isCelsius', 1)
|
||||
})
|
||||
...props.config,
|
||||
});
|
||||
}, [props.loading]);
|
||||
|
||||
// Form handler
|
||||
@ -48,120 +50,124 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
|
||||
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
|
||||
props.createNotification({
|
||||
title: 'Warning',
|
||||
message: 'API key is missing. Weather Module will NOT work'
|
||||
})
|
||||
message: 'API key is missing. Weather Module will NOT work',
|
||||
});
|
||||
}
|
||||
|
||||
// Save settings
|
||||
await props.updateConfig(formData);
|
||||
|
||||
// Update weather
|
||||
axios.get<ApiResponse<Weather>>('/api/weather/update')
|
||||
axios
|
||||
.get<ApiResponse<Weather>>('/api/weather/update')
|
||||
.then(() => {
|
||||
props.createNotification({
|
||||
title: 'Success',
|
||||
message: 'Weather updated'
|
||||
})
|
||||
message: 'Weather updated',
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
props.createNotification({
|
||||
title: 'Error',
|
||||
message: err.response.data.error
|
||||
})
|
||||
message: err.response.data.error,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Input handler
|
||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
|
||||
let value: string | number = e.target.value;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: value
|
||||
})
|
||||
}
|
||||
const inputChangeHandler = (
|
||||
e: ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||
options?: { isNumber?: boolean; isBool?: boolean }
|
||||
) => {
|
||||
inputHandler<WeatherForm>({
|
||||
e,
|
||||
options,
|
||||
setStateHandler: setFormData,
|
||||
state: formData,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||
<InputGroup>
|
||||
<label htmlFor='WEATHER_API_KEY'>API key</label>
|
||||
<label htmlFor="WEATHER_API_KEY">API key</label>
|
||||
<input
|
||||
type='text'
|
||||
id='WEATHER_API_KEY'
|
||||
name='WEATHER_API_KEY'
|
||||
placeholder='secret'
|
||||
type="text"
|
||||
id="WEATHER_API_KEY"
|
||||
name="WEATHER_API_KEY"
|
||||
placeholder="secret"
|
||||
value={formData.WEATHER_API_KEY}
|
||||
onChange={(e) => inputChangeHandler(e)}
|
||||
/>
|
||||
<span>
|
||||
Using
|
||||
<a
|
||||
href='https://www.weatherapi.com/pricing.aspx'
|
||||
target='blank'>
|
||||
{' '}Weather API
|
||||
<a href="https://www.weatherapi.com/pricing.aspx" target="blank">
|
||||
{' '}
|
||||
Weather API
|
||||
</a>
|
||||
. Key is required for weather module to work.
|
||||
</span>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='lat'>Location latitude</label>
|
||||
<label htmlFor="lat">Location latitude</label>
|
||||
<input
|
||||
type='number'
|
||||
id='lat'
|
||||
name='lat'
|
||||
placeholder='52.22'
|
||||
type="number"
|
||||
id="lat"
|
||||
name="lat"
|
||||
placeholder="52.22"
|
||||
value={formData.lat}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
step='any'
|
||||
lang='en-150'
|
||||
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||
step="any"
|
||||
lang="en-150"
|
||||
/>
|
||||
<span>
|
||||
You can use
|
||||
<a
|
||||
href='https://www.latlong.net/convert-address-to-lat-long.html'
|
||||
target='blank'>
|
||||
{' '}latlong.net
|
||||
href="https://www.latlong.net/convert-address-to-lat-long.html"
|
||||
target="blank"
|
||||
>
|
||||
{' '}
|
||||
latlong.net
|
||||
</a>
|
||||
</span>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='long'>Location longitude</label>
|
||||
<label htmlFor="long">Location longitude</label>
|
||||
<input
|
||||
type='number'
|
||||
id='long'
|
||||
name='long'
|
||||
placeholder='21.01'
|
||||
type="number"
|
||||
id="long"
|
||||
name="long"
|
||||
placeholder="21.01"
|
||||
value={formData.long}
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
step='any'
|
||||
lang='en-150'
|
||||
onChange={(e) => inputChangeHandler(e, { isNumber: true })}
|
||||
step="any"
|
||||
lang="en-150"
|
||||
/>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<label htmlFor='isCelsius'>Temperature unit</label>
|
||||
<label htmlFor="isCelsius">Temperature unit</label>
|
||||
<select
|
||||
id='isCelsius'
|
||||
name='isCelsius'
|
||||
onChange={(e) => inputChangeHandler(e, true)}
|
||||
value={formData.isCelsius}
|
||||
id="isCelsius"
|
||||
name="isCelsius"
|
||||
onChange={(e) => inputChangeHandler(e, { isBool: true })}
|
||||
value={formData.isCelsius ? 1 : 0}
|
||||
>
|
||||
<option value={1}>Celsius</option>
|
||||
<option value={0}>Fahrenheit</option>
|
||||
</select>
|
||||
</InputGroup>
|
||||
<Button>Save changes</Button>
|
||||
<Button>Save changes</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
loading: state.config.loading
|
||||
}
|
||||
}
|
||||
loading: state.config.loading,
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig })(WeatherSettings);
|
||||
export default connect(mapStateToProps, { createNotification, updateConfig })(
|
||||
WeatherSettings
|
||||
);
|
||||
|
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// Typescript
|
||||
import { Weather, ApiResponse, Config, GlobalState } from '../../../interfaces';
|
||||
import { Weather, ApiResponse, GlobalState, Config } from '../../../interfaces';
|
||||
|
||||
// CSS
|
||||
import classes from './WeatherWidget.module.css';
|
||||
@ -13,12 +13,9 @@ import classes from './WeatherWidget.module.css';
|
||||
// UI
|
||||
import WeatherIcon from '../../UI/Icons/WeatherIcon/WeatherIcon';
|
||||
|
||||
// Utils
|
||||
import { searchConfig } from '../../../utility';
|
||||
|
||||
interface ComponentProps {
|
||||
configLoading: boolean;
|
||||
config: Config[];
|
||||
config: Config;
|
||||
}
|
||||
|
||||
const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||
@ -32,26 +29,28 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||
conditionCode: 1000,
|
||||
id: -1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Initial request to get data
|
||||
useEffect(() => {
|
||||
axios.get<ApiResponse<Weather[]>>('/api/weather')
|
||||
.then(data => {
|
||||
axios
|
||||
.get<ApiResponse<Weather[]>>('/api/weather')
|
||||
.then((data) => {
|
||||
const weatherData = data.data.data[0];
|
||||
if (weatherData) {
|
||||
setWeather(weatherData);
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
.catch((err) => console.log(err));
|
||||
}, []);
|
||||
|
||||
// Open socket for data updates
|
||||
useEffect(() => {
|
||||
const socketProtocol = document.location.protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
const socketProtocol =
|
||||
document.location.protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
const socketAddress = `${socketProtocol}//${window.location.host}/socket`;
|
||||
const webSocketClient = new WebSocket(socketAddress);
|
||||
|
||||
@ -59,43 +58,44 @@ const WeatherWidget = (props: ComponentProps): JSX.Element => {
|
||||
const data = JSON.parse(e.data);
|
||||
setWeather({
|
||||
...weather,
|
||||
...data
|
||||
})
|
||||
}
|
||||
...data,
|
||||
});
|
||||
};
|
||||
|
||||
return () => webSocketClient.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={classes.WeatherWidget}>
|
||||
{(isLoading || props.configLoading || searchConfig('WEATHER_API_KEY', '')) &&
|
||||
(weather.id > 0 &&
|
||||
(<Fragment>
|
||||
<div className={classes.WeatherIcon}>
|
||||
<WeatherIcon
|
||||
weatherStatusCode={weather.conditionCode}
|
||||
isDay={weather.isDay}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.WeatherDetails}>
|
||||
{searchConfig('isCelsius', true)
|
||||
? <span>{weather.tempC}°C</span>
|
||||
: <span>{weather.tempF}°F</span>
|
||||
}
|
||||
<span>{weather.cloud}%</span>
|
||||
</div>
|
||||
</Fragment>)
|
||||
)
|
||||
}
|
||||
{isLoading ||
|
||||
props.configLoading ||
|
||||
(props.config.WEATHER_API_KEY && weather.id > 0 && (
|
||||
<Fragment>
|
||||
<div className={classes.WeatherIcon}>
|
||||
<WeatherIcon
|
||||
weatherStatusCode={weather.conditionCode}
|
||||
isDay={weather.isDay}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.WeatherDetails}>
|
||||
{props.config.isCelsius ? (
|
||||
<span>{weather.tempC}°C</span>
|
||||
) : (
|
||||
<span>{weather.tempF}°F</span>
|
||||
)}
|
||||
<span>{weather.cloud}%</span>
|
||||
</div>
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: GlobalState) => {
|
||||
return {
|
||||
configLoading: state.config.loading,
|
||||
config: state.config.config
|
||||
}
|
||||
}
|
||||
config: state.config.config,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(WeatherWidget);
|
@ -1,8 +1,22 @@
|
||||
import { Model } from './';
|
||||
|
||||
export interface Config extends Model {
|
||||
key: string;
|
||||
value: string;
|
||||
valueType: string;
|
||||
isLocked: boolean;
|
||||
export interface Config {
|
||||
WEATHER_API_KEY: string;
|
||||
lat: number;
|
||||
long: number;
|
||||
isCelsius: boolean;
|
||||
customTitle: string;
|
||||
pinAppsByDefault: boolean;
|
||||
pinCategoriesByDefault: boolean;
|
||||
hideHeader: boolean;
|
||||
useOrdering: string;
|
||||
appsSameTab: boolean;
|
||||
bookmarksSameTab: boolean;
|
||||
searchSameTab: boolean;
|
||||
hideApps: boolean;
|
||||
hideCategories: boolean;
|
||||
hideSearch: boolean;
|
||||
defaultSearchProvider: string;
|
||||
dockerApps: boolean;
|
||||
dockerHost: string;
|
||||
kubernetesApps: boolean;
|
||||
unpinStoppedApps: boolean;
|
||||
}
|
@ -2,30 +2,27 @@ export interface WeatherForm {
|
||||
WEATHER_API_KEY: string;
|
||||
lat: number;
|
||||
long: number;
|
||||
isCelsius: number;
|
||||
isCelsius: boolean;
|
||||
}
|
||||
|
||||
export interface SearchForm {
|
||||
hideSearch: number;
|
||||
hideSearch: boolean;
|
||||
defaultSearchProvider: string;
|
||||
searchSameTab: number;
|
||||
searchSameTab: boolean;
|
||||
}
|
||||
|
||||
export interface SettingsForm {
|
||||
export interface OtherSettingsForm {
|
||||
customTitle: string;
|
||||
pinAppsByDefault: number;
|
||||
pinCategoriesByDefault: number;
|
||||
hideHeader: number;
|
||||
hideApps: number;
|
||||
hideCategories: number;
|
||||
// hideSearch: number;
|
||||
// defaultSearchProvider: string;
|
||||
pinAppsByDefault: boolean;
|
||||
pinCategoriesByDefault: boolean;
|
||||
hideHeader: boolean;
|
||||
hideApps: boolean;
|
||||
hideCategories: boolean;
|
||||
useOrdering: string;
|
||||
appsSameTab: number;
|
||||
bookmarksSameTab: number;
|
||||
// searchSameTab: number;
|
||||
dockerApps: number;
|
||||
appsSameTab: boolean;
|
||||
bookmarksSameTab: boolean;
|
||||
dockerApps: boolean;
|
||||
dockerHost: string;
|
||||
kubernetesApps: number;
|
||||
unpinStoppedApps: number;
|
||||
kubernetesApps: boolean;
|
||||
unpinStoppedApps: boolean;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
export * from './iconParser';
|
||||
export * from './urlParser';
|
||||
export * from './searchConfig';
|
||||
export * from './checkVersion';
|
||||
export * from './sortData';
|
||||
export * from './searchParser';
|
||||
export * from './redirectUrl';
|
||||
export * from './templateObjects';
|
||||
export * from './inputHandler';
|
||||
|
39
client/src/utility/inputHandler.ts
Normal file
39
client/src/utility/inputHandler.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { ChangeEvent, SetStateAction } from 'react';
|
||||
|
||||
type Event = ChangeEvent<HTMLInputElement | HTMLSelectElement>;
|
||||
|
||||
interface Options {
|
||||
isNumber?: boolean;
|
||||
isBool?: boolean;
|
||||
}
|
||||
|
||||
interface Params<T> {
|
||||
e: Event;
|
||||
options?: Options;
|
||||
setStateHandler: (v: SetStateAction<T>) => void;
|
||||
state: T;
|
||||
}
|
||||
|
||||
export const inputHandler = <T>(params: Params<T>): void => {
|
||||
const { e, options, setStateHandler, state } = params;
|
||||
|
||||
const rawValue = e.target.value;
|
||||
let value: string | number | boolean = e.target.value;
|
||||
|
||||
if (options) {
|
||||
const { isNumber = false, isBool = false } = options;
|
||||
|
||||
if (isNumber) {
|
||||
value = parseFloat(rawValue);
|
||||
}
|
||||
|
||||
if (isBool) {
|
||||
value = !!parseInt(rawValue);
|
||||
}
|
||||
}
|
||||
|
||||
setStateHandler({
|
||||
...state,
|
||||
[e.target.name]: value,
|
||||
});
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { store } from '../store/store';
|
||||
|
||||
/**
|
||||
* Search config store with given key
|
||||
* @param key Config pair key to search
|
||||
* @param _default Value to return if key is not found
|
||||
*/
|
||||
export const searchConfig = (key: string, _default: any) => {
|
||||
const state = store.getState();
|
||||
|
||||
const pair = state.config.config.find(p => p.key === key);
|
||||
|
||||
if (pair) {
|
||||
if (pair.valueType === 'number') {
|
||||
return parseFloat(pair.value);
|
||||
} else if (pair.valueType === 'boolean') {
|
||||
return parseInt(pair.value);
|
||||
} else {
|
||||
return pair.value;
|
||||
}
|
||||
}
|
||||
|
||||
return _default;
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { queries } from './searchQueries.json';
|
||||
import { Query, SearchResult } from '../interfaces';
|
||||
import { store } from '../store/store';
|
||||
import { searchConfig } from '.';
|
||||
|
||||
export const searchParser = (searchQuery: string): SearchResult => {
|
||||
const result: SearchResult = {
|
||||
@ -16,7 +15,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||
},
|
||||
};
|
||||
|
||||
const customQueries = store.getState().config.customQueries;
|
||||
const { customQueries, config } = store.getState().config;
|
||||
|
||||
// Check if url or ip was passed
|
||||
const urlRegex =
|
||||
@ -27,9 +26,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||
// Match prefix and query
|
||||
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
||||
|
||||
const prefix = splitQuery
|
||||
? splitQuery[1]
|
||||
: searchConfig('defaultSearchProvider', 'l');
|
||||
const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider;
|
||||
|
||||
const search = splitQuery
|
||||
? encodeURIComponent(splitQuery[2])
|
||||
@ -47,7 +44,7 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||
if (prefix === 'l') {
|
||||
result.isLocal = true;
|
||||
} else {
|
||||
result.sameTab = searchConfig('searchSameTab', false);
|
||||
result.sameTab = config.searchSameTab;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
24
client/src/utility/templateObjects/configTemplate.ts
Normal file
24
client/src/utility/templateObjects/configTemplate.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Config } from '../../interfaces';
|
||||
|
||||
export const configTemplate: Config = {
|
||||
WEATHER_API_KEY: '',
|
||||
lat: 0,
|
||||
long: 0,
|
||||
isCelsius: true,
|
||||
customTitle: 'Flame',
|
||||
pinAppsByDefault: true,
|
||||
pinCategoriesByDefault: true,
|
||||
hideHeader: false,
|
||||
useOrdering: 'createdAt',
|
||||
appsSameTab: false,
|
||||
bookmarksSameTab: false,
|
||||
searchSameTab: false,
|
||||
hideApps: false,
|
||||
hideCategories: false,
|
||||
hideSearch: false,
|
||||
defaultSearchProvider: 'l',
|
||||
dockerApps: false,
|
||||
dockerHost: 'localhost',
|
||||
kubernetesApps: false,
|
||||
unpinStoppedApps: false,
|
||||
};
|
2
client/src/utility/templateObjects/index.ts
Normal file
2
client/src/utility/templateObjects/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './configTemplate';
|
||||
export * from './settingsTemplate';
|
30
client/src/utility/templateObjects/settingsTemplate.ts
Normal file
30
client/src/utility/templateObjects/settingsTemplate.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { OtherSettingsForm, SearchForm, WeatherForm } from '../../interfaces';
|
||||
|
||||
export const otherSettingsTemplate: OtherSettingsForm = {
|
||||
customTitle: document.title,
|
||||
pinAppsByDefault: true,
|
||||
pinCategoriesByDefault: true,
|
||||
hideHeader: false,
|
||||
hideApps: false,
|
||||
hideCategories: false,
|
||||
useOrdering: 'createdAt',
|
||||
appsSameTab: false,
|
||||
bookmarksSameTab: false,
|
||||
dockerApps: true,
|
||||
dockerHost: 'localhost',
|
||||
kubernetesApps: true,
|
||||
unpinStoppedApps: true,
|
||||
};
|
||||
|
||||
export const weatherSettingsTemplate: WeatherForm = {
|
||||
WEATHER_API_KEY: '',
|
||||
lat: 0,
|
||||
long: 0,
|
||||
isCelsius: true,
|
||||
};
|
||||
|
||||
export const searchSettingsTemplate: SearchForm = {
|
||||
hideSearch: false,
|
||||
searchSameTab: false,
|
||||
defaultSearchProvider: 'l',
|
||||
};
|
Loading…
Reference in New Issue
Block a user