Merge branch 'feature' into v1.7.0
This commit is contained in:
commit
55f192f664
53 changed files with 1172 additions and 385 deletions
|
@ -1,3 +1,9 @@
|
||||||
|
### v1.7.0 (TBA)
|
||||||
|
- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67))
|
||||||
|
- Fixed bug related to creating new apps/bookmarks with custom icon ([#83](https://github.com/pawelmalak/flame/issues/83))
|
||||||
|
- URL can now be assigned to notifications. Clicking on "New version is available" popup will now redirect to changelog ([#86](https://github.com/pawelmalak/flame/issues/86))
|
||||||
|
- Added static fonts ([#94](https://github.com/pawelmalak/flame/issues/94))
|
||||||
|
|
||||||
### v1.6.9 (2021-10-09)
|
### v1.6.9 (2021-10-09)
|
||||||
- Added option for remote docker host ([#97](https://github.com/pawelmalak/flame/issues/97))
|
- Added option for remote docker host ([#97](https://github.com/pawelmalak/flame/issues/97))
|
||||||
|
|
||||||
|
|
1
api.js
1
api.js
|
@ -20,6 +20,7 @@ api.use('/api/config', require('./routes/config'));
|
||||||
api.use('/api/weather', require('./routes/weather'));
|
api.use('/api/weather', require('./routes/weather'));
|
||||||
api.use('/api/categories', require('./routes/category'));
|
api.use('/api/categories', require('./routes/category'));
|
||||||
api.use('/api/bookmarks', require('./routes/bookmark'));
|
api.use('/api/bookmarks', require('./routes/bookmark'));
|
||||||
|
api.use('/api/queries', require('./routes/queries'));
|
||||||
|
|
||||||
// Custom error handler
|
// Custom error handler
|
||||||
api.use(errorHandler);
|
api.use(errorHandler);
|
||||||
|
|
|
@ -4,16 +4,15 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="Flame - self-hosted startpage for your server" />
|
<meta
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
name="description"
|
||||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet">
|
content="Flame - self-hosted startpage for your server"
|
||||||
<link rel="stylesheet" href="%PUBLIC_URL%/flame.css">
|
/>
|
||||||
|
<link rel="stylesheet" href="%PUBLIC_URL%/flame.css" />
|
||||||
<title>Flame</title>
|
<title>Flame</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
||||||
import { getConfig, setTheme } from './store/actions';
|
import { fetchQueries, getConfig, setTheme } from './store/actions';
|
||||||
import 'external-svg-loader';
|
import 'external-svg-loader';
|
||||||
|
|
||||||
// Redux
|
// Redux
|
||||||
|
@ -27,15 +27,18 @@ if (localStorage.theme) {
|
||||||
// Check for updates
|
// Check for updates
|
||||||
checkVersion();
|
checkVersion();
|
||||||
|
|
||||||
|
// fetch queries
|
||||||
|
store.dispatch<any>(fetchQueries());
|
||||||
|
|
||||||
const App = (): JSX.Element => {
|
const App = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path='/' component={Home} />
|
<Route exact path="/" component={Home} />
|
||||||
<Route path='/settings' component={Settings} />
|
<Route path="/settings" component={Settings} />
|
||||||
<Route path='/applications' component={Apps} />
|
<Route path="/applications" component={Apps} />
|
||||||
<Route path='/bookmarks' component={Bookmarks} />
|
<Route path="/bookmarks" component={Bookmarks} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
<NotificationCenter />
|
<NotificationCenter />
|
||||||
|
|
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff
Normal file
Binary file not shown.
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2
Normal file
BIN
client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2
Normal file
Binary file not shown.
|
@ -22,7 +22,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
const [formData, setFormData] = useState<NewApp>({
|
const [formData, setFormData] = useState<NewApp>({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -30,13 +30,13 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: props.app.name,
|
name: props.app.name,
|
||||||
url: props.app.url,
|
url: props.app.url,
|
||||||
icon: props.app.icon
|
icon: props.app.icon,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [props.app]);
|
}, [props.app]);
|
||||||
|
@ -44,7 +44,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[e.target.name]: e.target.value
|
[e.target.name]: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
const createFormData = (): FormData => {
|
const createFormData = (): FormData => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|
||||||
if (customIcon) {
|
if (customIcon) {
|
||||||
data.append('icon', customIcon);
|
data.append('icon', customIcon);
|
||||||
}
|
}
|
||||||
|
@ -88,10 +89,8 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setCustomIcon(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -100,33 +99,33 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
formHandler={formSubmitHandler}
|
formHandler={formSubmitHandler}
|
||||||
>
|
>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='name'>App Name</label>
|
<label htmlFor="name">App Name</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='name'
|
name="name"
|
||||||
id='name'
|
id="name"
|
||||||
placeholder='Bookstack'
|
placeholder="Bookstack"
|
||||||
required
|
required
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='url'>App URL</label>
|
<label htmlFor="url">App URL</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='url'
|
name="url"
|
||||||
id='url'
|
id="url"
|
||||||
placeholder='bookstack.example.com'
|
placeholder="bookstack.example.com"
|
||||||
required
|
required
|
||||||
value={formData.url}
|
value={formData.url}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
|
href="https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks"
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noreferrer'
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
Check supported URL formats
|
Check supported URL formats
|
||||||
|
@ -136,19 +135,19 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
{!useCustomIcon ? (
|
{!useCustomIcon ? (
|
||||||
// use mdi icon
|
// use mdi icon
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='icon'>App Icon</label>
|
<label htmlFor="icon">App Icon</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='icon'
|
name="icon"
|
||||||
id='icon'
|
id="icon"
|
||||||
placeholder='book-open-outline'
|
placeholder="book-open-outline"
|
||||||
required
|
required
|
||||||
value={formData.icon}
|
value={formData.icon}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Use icon name from MDI.
|
Use icon name from MDI.
|
||||||
<a href='https://materialdesignicons.com/' target='blank'>
|
<a href="https://materialdesignicons.com/" target="blank">
|
||||||
{' '}
|
{' '}
|
||||||
Click here for reference
|
Click here for reference
|
||||||
</a>
|
</a>
|
||||||
|
@ -163,17 +162,20 @@ const AppForm = (props: ComponentProps): JSX.Element => {
|
||||||
) : (
|
) : (
|
||||||
// upload custom icon
|
// upload custom icon
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='icon'>App Icon</label>
|
<label htmlFor="icon">App Icon</label>
|
||||||
<input
|
<input
|
||||||
type='file'
|
type="file"
|
||||||
name='icon'
|
name="icon"
|
||||||
id='icon'
|
id="icon"
|
||||||
required
|
required
|
||||||
onChange={e => fileChangeHandler(e)}
|
onChange={(e) => fileChangeHandler(e)}
|
||||||
accept='.jpg,.jpeg,.png,.svg'
|
accept=".jpg,.jpeg,.png,.svg"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
|
onClick={() => {
|
||||||
|
setCustomIcon(null);
|
||||||
|
toggleUseCustomIcon(!useCustomIcon);
|
||||||
|
}}
|
||||||
className={classes.Switch}
|
className={classes.Switch}
|
||||||
>
|
>
|
||||||
Switch to MDI
|
Switch to MDI
|
||||||
|
|
|
@ -1,32 +1,40 @@
|
||||||
|
// React
|
||||||
import {
|
import {
|
||||||
useState,
|
useState,
|
||||||
SyntheticEvent,
|
SyntheticEvent,
|
||||||
Fragment,
|
Fragment,
|
||||||
ChangeEvent,
|
ChangeEvent,
|
||||||
useEffect
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
|
// Redux
|
||||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
import { connect } from 'react-redux';
|
||||||
import {
|
|
||||||
Bookmark,
|
|
||||||
Category,
|
|
||||||
GlobalState,
|
|
||||||
NewBookmark,
|
|
||||||
NewCategory,
|
|
||||||
NewNotification
|
|
||||||
} from '../../../interfaces';
|
|
||||||
import { ContentType } from '../Bookmarks';
|
|
||||||
import {
|
import {
|
||||||
getCategories,
|
getCategories,
|
||||||
addCategory,
|
addCategory,
|
||||||
addBookmark,
|
addBookmark,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
updateBookmark,
|
updateBookmark,
|
||||||
createNotification
|
createNotification,
|
||||||
} from '../../../store/actions';
|
} from '../../../store/actions';
|
||||||
|
|
||||||
|
// 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 Button from '../../UI/Buttons/Button/Button';
|
||||||
|
|
||||||
|
// CSS
|
||||||
import classes from './BookmarkForm.module.css';
|
import classes from './BookmarkForm.module.css';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
|
@ -53,14 +61,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
|
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
|
||||||
const [customIcon, setCustomIcon] = useState<File | null>(null);
|
const [customIcon, setCustomIcon] = useState<File | null>(null);
|
||||||
const [categoryName, setCategoryName] = useState<NewCategory>({
|
const [categoryName, setCategoryName] = useState<NewCategory>({
|
||||||
name: ''
|
name: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState<NewBookmark>({
|
const [formData, setFormData] = useState<NewBookmark>({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
categoryId: -1,
|
categoryId: -1,
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load category data if provided for editing
|
// Load category data if provided for editing
|
||||||
|
@ -79,14 +87,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
name: props.bookmark.name,
|
name: props.bookmark.name,
|
||||||
url: props.bookmark.url,
|
url: props.bookmark.url,
|
||||||
categoryId: props.bookmark.categoryId,
|
categoryId: props.bookmark.categoryId,
|
||||||
icon: props.bookmark.icon
|
icon: props.bookmark.icon,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
categoryId: -1,
|
categoryId: -1,
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [props.bookmark]);
|
}, [props.bookmark]);
|
||||||
|
@ -117,7 +125,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
if (formData.categoryId === -1) {
|
if (formData.categoryId === -1) {
|
||||||
props.createNotification({
|
props.createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Please select category'
|
message: 'Please select category',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -133,10 +141,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
categoryId: formData.categoryId,
|
categoryId: formData.categoryId,
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setCustomIcon(null);
|
// setCustomIcon(null);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Update
|
// Update
|
||||||
|
@ -150,12 +158,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
const data = createFormData();
|
const data = createFormData();
|
||||||
props.updateBookmark(props.bookmark.id, data, {
|
props.updateBookmark(props.bookmark.id, data, {
|
||||||
prev: props.bookmark.categoryId,
|
prev: props.bookmark.categoryId,
|
||||||
curr: formData.categoryId
|
curr: formData.categoryId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
props.updateBookmark(props.bookmark.id, formData, {
|
props.updateBookmark(props.bookmark.id, formData, {
|
||||||
prev: props.bookmark.categoryId,
|
prev: props.bookmark.categoryId,
|
||||||
curr: formData.categoryId
|
curr: formData.categoryId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,7 +171,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
categoryId: -1,
|
categoryId: -1,
|
||||||
icon: ''
|
icon: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
setCustomIcon(null);
|
setCustomIcon(null);
|
||||||
|
@ -176,14 +184,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[e.target.name]: e.target.value
|
[e.target.name]: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectChangeHandler = (e: ChangeEvent<HTMLSelectElement>): void => {
|
const selectChangeHandler = (e: ChangeEvent<HTMLSelectElement>): void => {
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
categoryId: parseInt(e.target.value)
|
categoryId: parseInt(e.target.value),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -215,48 +223,48 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
{props.contentType === ContentType.category ? (
|
{props.contentType === ContentType.category ? (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='categoryName'>Category Name</label>
|
<label htmlFor="categoryName">Category Name</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='categoryName'
|
name="categoryName"
|
||||||
id='categoryName'
|
id="categoryName"
|
||||||
placeholder='Social Media'
|
placeholder="Social Media"
|
||||||
required
|
required
|
||||||
value={categoryName.name}
|
value={categoryName.name}
|
||||||
onChange={e => setCategoryName({ name: e.target.value })}
|
onChange={(e) => setCategoryName({ name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='name'>Bookmark Name</label>
|
<label htmlFor="name">Bookmark Name</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='name'
|
name="name"
|
||||||
id='name'
|
id="name"
|
||||||
placeholder='Reddit'
|
placeholder="Reddit"
|
||||||
required
|
required
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='url'>Bookmark URL</label>
|
<label htmlFor="url">Bookmark URL</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='url'
|
name="url"
|
||||||
id='url'
|
id="url"
|
||||||
placeholder='reddit.com'
|
placeholder="reddit.com"
|
||||||
required
|
required
|
||||||
value={formData.url}
|
value={formData.url}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<a
|
<a
|
||||||
href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
|
href="https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks"
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noreferrer'
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{' '}
|
{' '}
|
||||||
Check supported URL formats
|
Check supported URL formats
|
||||||
|
@ -264,12 +272,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
</span>
|
</span>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='categoryId'>Bookmark Category</label>
|
<label htmlFor="categoryId">Bookmark Category</label>
|
||||||
<select
|
<select
|
||||||
name='categoryId'
|
name="categoryId"
|
||||||
id='categoryId'
|
id="categoryId"
|
||||||
required
|
required
|
||||||
onChange={e => selectChangeHandler(e)}
|
onChange={(e) => selectChangeHandler(e)}
|
||||||
value={formData.categoryId}
|
value={formData.categoryId}
|
||||||
>
|
>
|
||||||
<option value={-1}>Select category</option>
|
<option value={-1}>Select category</option>
|
||||||
|
@ -285,18 +293,18 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
{!useCustomIcon ? (
|
{!useCustomIcon ? (
|
||||||
// mdi
|
// mdi
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='icon'>Bookmark Icon (optional)</label>
|
<label htmlFor="icon">Bookmark Icon (optional)</label>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type="text"
|
||||||
name='icon'
|
name="icon"
|
||||||
id='icon'
|
id="icon"
|
||||||
placeholder='book-open-outline'
|
placeholder="book-open-outline"
|
||||||
value={formData.icon}
|
value={formData.icon}
|
||||||
onChange={e => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
Use icon name from MDI.
|
Use icon name from MDI.
|
||||||
<a href='https://materialdesignicons.com/' target='blank'>
|
<a href="https://materialdesignicons.com/" target="blank">
|
||||||
{' '}
|
{' '}
|
||||||
Click here for reference
|
Click here for reference
|
||||||
</a>
|
</a>
|
||||||
|
@ -311,16 +319,19 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
) : (
|
) : (
|
||||||
// custom
|
// custom
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor='icon'>Bookmark Icon (optional)</label>
|
<label htmlFor="icon">Bookmark Icon (optional)</label>
|
||||||
<input
|
<input
|
||||||
type='file'
|
type="file"
|
||||||
name='icon'
|
name="icon"
|
||||||
id='icon'
|
id="icon"
|
||||||
onChange={e => fileChangeHandler(e)}
|
onChange={(e) => fileChangeHandler(e)}
|
||||||
accept='.jpg,.jpeg,.png,.svg'
|
accept=".jpg,.jpeg,.png,.svg"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
|
onClick={() => {
|
||||||
|
setCustomIcon(null);
|
||||||
|
toggleUseCustomIcon(!useCustomIcon);
|
||||||
|
}}
|
||||||
className={classes.Switch}
|
className={classes.Switch}
|
||||||
>
|
>
|
||||||
Switch to MDI
|
Switch to MDI
|
||||||
|
@ -336,7 +347,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
categories: state.bookmark.categories
|
categories: state.bookmark.categories,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -346,7 +357,7 @@ const dispatchMap = {
|
||||||
addBookmark,
|
addBookmark,
|
||||||
updateCategory,
|
updateCategory,
|
||||||
updateBookmark,
|
updateBookmark,
|
||||||
createNotification
|
createNotification,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, dispatchMap)(BookmarkForm);
|
export default connect(mapStateToProps, dispatchMap)(BookmarkForm);
|
||||||
|
|
|
@ -20,19 +20,20 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => {
|
||||||
<Notification
|
<Notification
|
||||||
title={notification.title}
|
title={notification.title}
|
||||||
message={notification.message}
|
message={notification.message}
|
||||||
|
url={notification.url || null}
|
||||||
id={notification.id}
|
id={notification.id}
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state: GlobalState) => {
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
return {
|
return {
|
||||||
notifications: state.notification.notifications
|
notifications: state.notification.notifications,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NotificationCenter);
|
export default connect(mapStateToProps)(NotificationCenter);
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { NewNotification } from '../../interfaces';
|
||||||
import classes from './SearchBar.module.css';
|
import classes from './SearchBar.module.css';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchParser } from '../../utility';
|
import { searchParser, urlParser, redirectUrl } from '../../utility';
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
|
@ -28,28 +28,28 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
const searchResult = searchParser(inputRef.current.value);
|
const { isLocal, search, query, isURL, sameTab } = searchParser(
|
||||||
|
inputRef.current.value
|
||||||
|
);
|
||||||
|
|
||||||
if (searchResult.isLocal) {
|
if (isLocal) {
|
||||||
setLocalSearch(searchResult.search);
|
setLocalSearch(search);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.code === 'Enter') {
|
if (e.code === 'Enter') {
|
||||||
if (!searchResult.query.prefix) {
|
if (!query.prefix) {
|
||||||
createNotification({
|
createNotification({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
message: 'Prefix not found',
|
message: 'Prefix not found',
|
||||||
});
|
});
|
||||||
} else if (searchResult.isLocal) {
|
} else if (isURL) {
|
||||||
setLocalSearch(searchResult.search);
|
const url = urlParser(inputRef.current.value)[1];
|
||||||
|
redirectUrl(url, sameTab);
|
||||||
|
} else if (isLocal) {
|
||||||
|
setLocalSearch(search);
|
||||||
} else {
|
} else {
|
||||||
if (searchResult.sameTab) {
|
const url = `${query.template}${search}`;
|
||||||
document.location.replace(
|
redirectUrl(url, sameTab);
|
||||||
`${searchResult.query.template}${searchResult.search}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
window.open(`${searchResult.query.template}${searchResult.search}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,20 +13,16 @@ import {
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
NewNotification,
|
NewNotification,
|
||||||
Query,
|
|
||||||
SettingsForm,
|
SettingsForm,
|
||||||
} from '../../../interfaces';
|
} from '../../../interfaces';
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
|
||||||
import Button from '../../UI/Buttons/Button/Button';
|
import Button from '../../UI/Buttons/Button/Button';
|
||||||
|
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
|
||||||
// CSS
|
|
||||||
import classes from './OtherSettings.module.css';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { searchConfig } from '../../../utility';
|
import { searchConfig } from '../../../utility';
|
||||||
import { queries } from '../../../utility/searchQueries.json';
|
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
createNotification: (notification: NewNotification) => void;
|
createNotification: (notification: NewNotification) => void;
|
||||||
|
@ -45,12 +41,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
hideHeader: 0,
|
hideHeader: 0,
|
||||||
hideApps: 0,
|
hideApps: 0,
|
||||||
hideCategories: 0,
|
hideCategories: 0,
|
||||||
hideSearch: 0,
|
|
||||||
defaultSearchProvider: 'd',
|
|
||||||
useOrdering: 'createdAt',
|
useOrdering: 'createdAt',
|
||||||
appsSameTab: 0,
|
appsSameTab: 0,
|
||||||
bookmarksSameTab: 0,
|
bookmarksSameTab: 0,
|
||||||
searchSameTab: 0,
|
|
||||||
dockerApps: 1,
|
dockerApps: 1,
|
||||||
dockerHost: 'localhost',
|
dockerHost: 'localhost',
|
||||||
kubernetesApps: 1,
|
kubernetesApps: 1,
|
||||||
|
@ -66,12 +59,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
hideHeader: searchConfig('hideHeader', 0),
|
hideHeader: searchConfig('hideHeader', 0),
|
||||||
hideApps: searchConfig('hideApps', 0),
|
hideApps: searchConfig('hideApps', 0),
|
||||||
hideCategories: searchConfig('hideCategories', 0),
|
hideCategories: searchConfig('hideCategories', 0),
|
||||||
hideSearch: searchConfig('hideSearch', 0),
|
|
||||||
defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'),
|
|
||||||
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
useOrdering: searchConfig('useOrdering', 'createdAt'),
|
||||||
appsSameTab: searchConfig('appsSameTab', 0),
|
appsSameTab: searchConfig('appsSameTab', 0),
|
||||||
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
|
||||||
searchSameTab: searchConfig('searchSameTab', 0),
|
|
||||||
dockerApps: searchConfig('dockerApps', 0),
|
dockerApps: searchConfig('dockerApps', 0),
|
||||||
dockerHost: searchConfig('dockerHost', 'localhost'),
|
dockerHost: searchConfig('dockerHost', 'localhost'),
|
||||||
kubernetesApps: searchConfig('kubernetesApps', 0),
|
kubernetesApps: searchConfig('kubernetesApps', 0),
|
||||||
|
@ -114,7 +104,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<form onSubmit={(e) => formSubmitHandler(e)}>
|
<form onSubmit={(e) => formSubmitHandler(e)}>
|
||||||
{/* OTHER OPTIONS */}
|
{/* OTHER OPTIONS */}
|
||||||
<h2 className={classes.SettingsSection}>Miscellaneous</h2>
|
<SettingsHeadline text="Miscellaneous" />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="customTitle">Custom page title</label>
|
<label htmlFor="customTitle">Custom page title</label>
|
||||||
<input
|
<input
|
||||||
|
@ -128,7 +118,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* BEAHVIOR OPTIONS */}
|
{/* BEAHVIOR OPTIONS */}
|
||||||
<h2 className={classes.SettingsSection}>App Behavior</h2>
|
<SettingsHeadline text="App Behavior" />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="pinAppsByDefault">
|
<label htmlFor="pinAppsByDefault">
|
||||||
Pin new applications by default
|
Pin new applications by default
|
||||||
|
@ -170,35 +160,6 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
<option value="orderId">Custom order</option>
|
<option value="orderId">Custom order</option>
|
||||||
</select>
|
</select>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<InputGroup>
|
|
||||||
<label htmlFor="defaultSearchProvider">Default Search Provider</label>
|
|
||||||
<select
|
|
||||||
id="defaultSearchProvider"
|
|
||||||
name="defaultSearchProvider"
|
|
||||||
value={formData.defaultSearchProvider}
|
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
|
||||||
>
|
|
||||||
{queries.map((query: Query, idx) => (
|
|
||||||
<option key={idx} value={query.prefix}>
|
|
||||||
{query.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup>
|
|
||||||
<label htmlFor="searchSameTab">
|
|
||||||
Open search results in the same tab
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="searchSameTab"
|
|
||||||
name="searchSameTab"
|
|
||||||
value={formData.searchSameTab}
|
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
|
||||||
>
|
|
||||||
<option value={1}>True</option>
|
|
||||||
<option value={0}>False</option>
|
|
||||||
</select>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="appsSameTab">Open applications in the same tab</label>
|
<label htmlFor="appsSameTab">Open applications in the same tab</label>
|
||||||
<select
|
<select
|
||||||
|
@ -225,19 +186,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* MODULES OPTIONS */}
|
{/* MODULES OPTIONS */}
|
||||||
<h2 className={classes.SettingsSection}>Modules</h2>
|
<SettingsHeadline text="Modules" />
|
||||||
<InputGroup>
|
|
||||||
<label htmlFor="hideSearch">Hide search bar</label>
|
|
||||||
<select
|
|
||||||
id="hideSearch"
|
|
||||||
name="hideSearch"
|
|
||||||
value={formData.hideSearch}
|
|
||||||
onChange={(e) => inputChangeHandler(e, true)}
|
|
||||||
>
|
|
||||||
<option value={1}>True</option>
|
|
||||||
<option value={0}>False</option>
|
|
||||||
</select>
|
|
||||||
</InputGroup>
|
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="hideHeader">Hide greeting and date</label>
|
<label htmlFor="hideHeader">Hide greeting and date</label>
|
||||||
<select
|
<select
|
||||||
|
@ -276,7 +225,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* DOCKER SETTINGS */}
|
{/* DOCKER SETTINGS */}
|
||||||
<h2 className={classes.SettingsSection}>Docker</h2>
|
<SettingsHeadline text="Docker" />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="dockerHost">Docker Host</label>
|
<label htmlFor="dockerHost">Docker Host</label>
|
||||||
<input
|
<input
|
||||||
|
@ -316,7 +265,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
{/* KUBERNETES SETTINGS */}
|
{/* KUBERNETES SETTINGS */}
|
||||||
<h2 className={classes.SettingsSection}>Kubernetes</h2>
|
<SettingsHeadline text="Kubernetes" />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
|
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
|
||||||
<select
|
<select
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
.QueriesGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.QueriesGrid span {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.QueriesGrid span:last-child {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ActionIcons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ActionIcons svg {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ActionIcons svg:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Separator {
|
||||||
|
grid-column: 1 / 4;
|
||||||
|
border-bottom: 1px solid var(--color-primary);
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomQueries = (props: Props): JSX.Element => {
|
||||||
|
const { customQueries, deleteQuery, createNotification } = props;
|
||||||
|
|
||||||
|
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||||
|
const [editableQuery, setEditableQuery] = useState<Query | null>(null);
|
||||||
|
|
||||||
|
const updateHandler = (query: Query) => {
|
||||||
|
setEditableQuery(query);
|
||||||
|
setModalIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteHandler = (query: Query) => {
|
||||||
|
const currentProvider = searchConfig('defaultSearchProvider', 'l');
|
||||||
|
const isCurrent = currentProvider === query.prefix;
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
createNotification({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Cannot delete active provider',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
window.confirm(`Are you sure you want to delete this provider?`)
|
||||||
|
) {
|
||||||
|
deleteQuery(query.prefix);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<Modal
|
||||||
|
isOpen={modalIsOpen}
|
||||||
|
setIsOpen={() => setModalIsOpen(!modalIsOpen)}
|
||||||
|
>
|
||||||
|
{editableQuery ? (
|
||||||
|
<QueriesForm
|
||||||
|
modalHandler={() => setModalIsOpen(!modalIsOpen)}
|
||||||
|
query={editableQuery}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<QueriesForm modalHandler={() => setModalIsOpen(!modalIsOpen)} />
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className={classes.QueriesGrid}>
|
||||||
|
{customQueries.length > 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<span>Name</span>
|
||||||
|
<span>Prefix</span>
|
||||||
|
<span>Actions</span>
|
||||||
|
|
||||||
|
<div className={classes.Separator}></div>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{customQueries.map((q: Query, idx) => (
|
||||||
|
<Fragment key={idx}>
|
||||||
|
<span>{q.name}</span>
|
||||||
|
<span>{q.prefix}</span>
|
||||||
|
<span className={classes.ActionIcons}>
|
||||||
|
<span onClick={() => updateHandler(q)}>
|
||||||
|
<Icon icon="mdiPencil" />
|
||||||
|
</span>
|
||||||
|
<span onClick={() => deleteHandler(q)}>
|
||||||
|
<Icon icon="mdiDelete" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
click={() => {
|
||||||
|
setEditableQuery(null);
|
||||||
|
setModalIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add new search provider
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
|
return {
|
||||||
|
customQueries: state.config.customQueries,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, { deleteQuery, createNotification })(
|
||||||
|
CustomQueries
|
||||||
|
);
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { ChangeEvent, FormEvent, useState, useEffect } from 'react';
|
||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<Query>({
|
||||||
|
name: '',
|
||||||
|
prefix: '',
|
||||||
|
template: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formHandler = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
updateQuery(formData, query.prefix);
|
||||||
|
} else {
|
||||||
|
addQuery(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// close modal
|
||||||
|
modalHandler();
|
||||||
|
|
||||||
|
// clear form
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
prefix: '',
|
||||||
|
template: '',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query) {
|
||||||
|
setFormData(query);
|
||||||
|
} else {
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
prefix: '',
|
||||||
|
template: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm modalHandler={modalHandler} formHandler={formHandler}>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="name">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
id="name"
|
||||||
|
placeholder="Google"
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="name">Prefix</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="prefix"
|
||||||
|
id="prefix"
|
||||||
|
placeholder="g"
|
||||||
|
required
|
||||||
|
value={formData.prefix}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="name">Query Template</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="template"
|
||||||
|
id="template"
|
||||||
|
placeholder="https://www.google.com/search?q="
|
||||||
|
required
|
||||||
|
value={formData.template}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
{query ? <Button>Update provider</Button> : <Button>Add provider</Button>}
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, { addQuery, updateQuery })(QueriesForm);
|
154
client/src/components/Settings/SearchSettings/SearchSettings.tsx
Normal file
154
client/src/components/Settings/SearchSettings/SearchSettings.tsx
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
// React
|
||||||
|
import { useState, useEffect, FormEvent, ChangeEvent, Fragment } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// State
|
||||||
|
import { createNotification, updateConfig } from '../../../store/actions';
|
||||||
|
|
||||||
|
// Typescript
|
||||||
|
import {
|
||||||
|
GlobalState,
|
||||||
|
NewNotification,
|
||||||
|
Query,
|
||||||
|
SearchForm,
|
||||||
|
} from '../../../interfaces';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { searchConfig } from '../../../utility';
|
||||||
|
|
||||||
|
// Data
|
||||||
|
import { queries } from '../../../utility/searchQueries.json';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
createNotification: (notification: NewNotification) => void;
|
||||||
|
updateConfig: (formData: SearchForm) => void;
|
||||||
|
loading: boolean;
|
||||||
|
customQueries: Query[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchSettings = (props: Props): JSX.Element => {
|
||||||
|
// Initial state
|
||||||
|
const [formData, setFormData] = useState<SearchForm>({
|
||||||
|
hideSearch: 0,
|
||||||
|
defaultSearchProvider: 'l',
|
||||||
|
searchSameTab: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get config
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData({
|
||||||
|
hideSearch: searchConfig('hideSearch', 0),
|
||||||
|
defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'),
|
||||||
|
searchSameTab: searchConfig('searchSameTab', 0),
|
||||||
|
});
|
||||||
|
}, [props.loading]);
|
||||||
|
|
||||||
|
// Form handler
|
||||||
|
const formSubmitHandler = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
await props.updateConfig(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{/* GENERAL SETTINGS */}
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => formSubmitHandler(e)}
|
||||||
|
style={{ marginBottom: '30px' }}
|
||||||
|
>
|
||||||
|
<SettingsHeadline text="General" />
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="defaultSearchProvider">Default Search Provider</label>
|
||||||
|
<select
|
||||||
|
id="defaultSearchProvider"
|
||||||
|
name="defaultSearchProvider"
|
||||||
|
value={formData.defaultSearchProvider}
|
||||||
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
|
>
|
||||||
|
{[...queries, ...props.customQueries].map((query: Query, idx) => {
|
||||||
|
const isCustom = idx >= queries.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<option key={idx} value={query.prefix}>
|
||||||
|
{isCustom && '+'} {query.name}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="searchSameTab">
|
||||||
|
Open search results in the same tab
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="searchSameTab"
|
||||||
|
name="searchSameTab"
|
||||||
|
value={formData.searchSameTab}
|
||||||
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<label htmlFor="hideSearch">Hide search bar</label>
|
||||||
|
<select
|
||||||
|
id="hideSearch"
|
||||||
|
name="hideSearch"
|
||||||
|
value={formData.hideSearch}
|
||||||
|
onChange={(e) => inputChangeHandler(e, true)}
|
||||||
|
>
|
||||||
|
<option value={1}>True</option>
|
||||||
|
<option value={0}>False</option>
|
||||||
|
</select>
|
||||||
|
</InputGroup>
|
||||||
|
<Button>Save changes</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* CUSTOM QUERIES */}
|
||||||
|
<SettingsHeadline text="Custom search providers" />
|
||||||
|
<CustomQueries />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = (state: GlobalState) => {
|
||||||
|
return {
|
||||||
|
loading: state.config.loading,
|
||||||
|
customQueries: state.config.customQueries,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
createNotification,
|
||||||
|
updateConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, actions)(SearchSettings);
|
|
@ -1,73 +1,61 @@
|
||||||
|
//
|
||||||
import { NavLink, Link, Switch, Route } from 'react-router-dom';
|
import { NavLink, Link, Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
// Typescript
|
||||||
|
import { Route as SettingsRoute } from '../../interfaces';
|
||||||
|
|
||||||
|
// CSS
|
||||||
import classes from './Settings.module.css';
|
import classes from './Settings.module.css';
|
||||||
|
|
||||||
import { Container } from '../UI/Layout/Layout';
|
// Components
|
||||||
import Headline from '../UI/Headlines/Headline/Headline';
|
|
||||||
|
|
||||||
import Themer from '../Themer/Themer';
|
import Themer from '../Themer/Themer';
|
||||||
import WeatherSettings from './WeatherSettings/WeatherSettings';
|
import WeatherSettings from './WeatherSettings/WeatherSettings';
|
||||||
import OtherSettings from './OtherSettings/OtherSettings';
|
import OtherSettings from './OtherSettings/OtherSettings';
|
||||||
import AppDetails from './AppDetails/AppDetails';
|
import AppDetails from './AppDetails/AppDetails';
|
||||||
import StyleSettings from './StyleSettings/StyleSettings';
|
import StyleSettings from './StyleSettings/StyleSettings';
|
||||||
|
import SearchSettings from './SearchSettings/SearchSettings';
|
||||||
|
|
||||||
|
// UI
|
||||||
|
import { Container } from '../UI/Layout/Layout';
|
||||||
|
import Headline from '../UI/Headlines/Headline/Headline';
|
||||||
|
|
||||||
|
// Data
|
||||||
|
import { routes } from './settings.json';
|
||||||
|
|
||||||
const Settings = (): JSX.Element => {
|
const Settings = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Headline
|
<Headline title="Settings" subtitle={<Link to="/">Go back</Link>} />
|
||||||
title='Settings'
|
|
||||||
subtitle={<Link to='/'>Go back</Link>}
|
|
||||||
/>
|
|
||||||
<div className={classes.Settings}>
|
<div className={classes.Settings}>
|
||||||
|
{/* NAVIGATION MENU */}
|
||||||
<nav className={classes.SettingsNav}>
|
<nav className={classes.SettingsNav}>
|
||||||
<NavLink
|
{routes.map(({ name, dest }: SettingsRoute, idx) => (
|
||||||
className={classes.SettingsNavLink}
|
<NavLink
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
className={classes.SettingsNavLink}
|
||||||
exact
|
activeClassName={classes.SettingsNavLinkActive}
|
||||||
to='/settings'>
|
exact
|
||||||
Theme
|
to={dest}
|
||||||
</NavLink>
|
key={idx}
|
||||||
<NavLink
|
>
|
||||||
className={classes.SettingsNavLink}
|
{name}
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
</NavLink>
|
||||||
exact
|
))}
|
||||||
to='/settings/weather'>
|
|
||||||
Weather
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
className={classes.SettingsNavLink}
|
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
|
||||||
exact
|
|
||||||
to='/settings/other'>
|
|
||||||
Other
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
className={classes.SettingsNavLink}
|
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
|
||||||
exact
|
|
||||||
to='/settings/css'>
|
|
||||||
CSS
|
|
||||||
</NavLink>
|
|
||||||
<NavLink
|
|
||||||
className={classes.SettingsNavLink}
|
|
||||||
activeClassName={classes.SettingsNavLinkActive}
|
|
||||||
exact
|
|
||||||
to='/settings/app'>
|
|
||||||
App
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* ROUTES */}
|
||||||
<section className={classes.SettingsContent}>
|
<section className={classes.SettingsContent}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path='/settings' component={Themer} />
|
<Route exact path="/settings" component={Themer} />
|
||||||
<Route path='/settings/weather' component={WeatherSettings} />
|
<Route path="/settings/weather" component={WeatherSettings} />
|
||||||
<Route path='/settings/other' component={OtherSettings} />
|
<Route path="/settings/search" component={SearchSettings} />
|
||||||
<Route path='/settings/css' component={StyleSettings} />
|
<Route path="/settings/other" component={OtherSettings} />
|
||||||
<Route path='/settings/app' component={AppDetails} />
|
<Route path="/settings/css" component={StyleSettings} />
|
||||||
|
<Route path="/settings/app" component={AppDetails} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|
28
client/src/components/Settings/settings.json
Normal file
28
client/src/components/Settings/settings.json
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"name": "Theme",
|
||||||
|
"dest": "/settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Weather",
|
||||||
|
"dest": "/settings/weather"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Search",
|
||||||
|
"dest": "/settings/search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Other",
|
||||||
|
"dest": "/settings/other"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CSS",
|
||||||
|
"dest": "/settings/css"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "App",
|
||||||
|
"dest": "/settings/app"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
.SettingsSection {
|
.SettingsHeadline {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
@ -6,4 +6,4 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-bottom: 2px solid var(--color-accent);
|
border-bottom: 2px solid var(--color-accent);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
const classes = require('./SettingsHeadline.module.css');
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsHeadline = (props: Props): JSX.Element => {
|
||||||
|
return <h2 className={classes.SettingsHeadline}>{props.text}</h2>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsHeadline;
|
|
@ -8,12 +8,16 @@ interface ComponentProps {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
url: string | null;
|
||||||
clearNotification: (id: number) => void;
|
clearNotification: (id: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notification = (props: ComponentProps): JSX.Element => {
|
const Notification = (props: ComponentProps): JSX.Element => {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const elementClasses = [classes.Notification, isOpen ? classes.NotificationOpen : classes.NotificationClose].join(' ');
|
const elementClasses = [
|
||||||
|
classes.Notification,
|
||||||
|
isOpen ? classes.NotificationOpen : classes.NotificationClose,
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const closeNotification = setTimeout(() => {
|
const closeNotification = setTimeout(() => {
|
||||||
|
@ -22,21 +26,27 @@ const Notification = (props: ComponentProps): JSX.Element => {
|
||||||
|
|
||||||
const clearNotification = setTimeout(() => {
|
const clearNotification = setTimeout(() => {
|
||||||
props.clearNotification(props.id);
|
props.clearNotification(props.id);
|
||||||
}, 3600)
|
}, 3600);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(closeNotification);
|
window.clearTimeout(closeNotification);
|
||||||
window.clearTimeout(clearNotification);
|
window.clearTimeout(clearNotification);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clickHandler = () => {
|
||||||
|
if (props.url) {
|
||||||
|
window.open(props.url, '_blank');
|
||||||
}
|
}
|
||||||
}, [])
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={elementClasses}>
|
<div className={elementClasses} onClick={clickHandler}>
|
||||||
<h4>{props.title}</h4>
|
<h4>{props.title}</h4>
|
||||||
<p>{props.message}</p>
|
<p>{props.message}</p>
|
||||||
<div className={classes.Pog}></div>
|
<div className={classes.Pog}></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default connect(null, { clearNotification })(Notification);
|
export default connect(null, { clearNotification })(Notification);
|
||||||
|
|
|
@ -1,3 +1,39 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local(''),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff2') format('woff2'),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: local(''),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-500.woff2') format('woff2'),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-500.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src: local(''),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-900.woff2') format('woff2'),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-900.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local(''),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-700.woff2') format('woff2'),
|
||||||
|
url('./assets/fonts/Roboto/roboto-v29-latin-700.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -5,18 +41,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--color-background: #2B2C56;
|
--color-background: #242b33;
|
||||||
--color-primary: #EFF1FC;
|
--color-primary: #effbff;
|
||||||
--color-accent: #6677EB;
|
--color-accent: #6ee2ff;
|
||||||
--spacing-ui: 10px;
|
--spacing-ui: 10px;
|
||||||
|
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif;
|
font-family: Roboto, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,12 @@ export interface WeatherForm {
|
||||||
isCelsius: number;
|
isCelsius: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchForm {
|
||||||
|
hideSearch: number;
|
||||||
|
defaultSearchProvider: string;
|
||||||
|
searchSameTab: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SettingsForm {
|
export interface SettingsForm {
|
||||||
customTitle: string;
|
customTitle: string;
|
||||||
pinAppsByDefault: number;
|
pinAppsByDefault: number;
|
||||||
|
@ -12,12 +18,12 @@ export interface SettingsForm {
|
||||||
hideHeader: number;
|
hideHeader: number;
|
||||||
hideApps: number;
|
hideApps: number;
|
||||||
hideCategories: number;
|
hideCategories: number;
|
||||||
hideSearch: number;
|
// hideSearch: number;
|
||||||
defaultSearchProvider: string;
|
// defaultSearchProvider: string;
|
||||||
useOrdering: string;
|
useOrdering: string;
|
||||||
appsSameTab: number;
|
appsSameTab: number;
|
||||||
bookmarksSameTab: number;
|
bookmarksSameTab: number;
|
||||||
searchSameTab: number;
|
// searchSameTab: number;
|
||||||
dockerApps: number;
|
dockerApps: number;
|
||||||
dockerHost: string;
|
dockerHost: string;
|
||||||
kubernetesApps: number;
|
kubernetesApps: number;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
export interface NewNotification {
|
export interface NewNotification {
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Notification extends NewNotification {
|
export interface Notification extends NewNotification {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
4
client/src/interfaces/Route.ts
Normal file
4
client/src/interfaces/Route.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Route {
|
||||||
|
name: string;
|
||||||
|
dest: string;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { Query } from './Query';
|
||||||
|
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
isLocal: boolean;
|
isLocal: boolean;
|
||||||
|
isURL: boolean;
|
||||||
sameTab: boolean;
|
sameTab: boolean;
|
||||||
search: string;
|
search: string;
|
||||||
query: Query;
|
query: Query;
|
||||||
|
|
|
@ -10,3 +10,4 @@ export * from './Config';
|
||||||
export * from './Forms';
|
export * from './Forms';
|
||||||
export * from './Query';
|
export * from './Query';
|
||||||
export * from './SearchResult';
|
export * from './SearchResult';
|
||||||
|
export * from './Route';
|
||||||
|
|
|
@ -26,8 +26,14 @@ import {
|
||||||
ClearNotificationAction,
|
ClearNotificationAction,
|
||||||
// Config
|
// Config
|
||||||
GetConfigAction,
|
GetConfigAction,
|
||||||
UpdateConfigAction
|
UpdateConfigAction,
|
||||||
} from './';
|
} from './';
|
||||||
|
import {
|
||||||
|
AddQueryAction,
|
||||||
|
DeleteQueryAction,
|
||||||
|
FetchQueriesAction,
|
||||||
|
UpdateQueryAction,
|
||||||
|
} from './config';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
// Theme
|
// Theme
|
||||||
|
@ -62,35 +68,43 @@ export enum ActionTypes {
|
||||||
clearNotification = 'CLEAR_NOTIFICATION',
|
clearNotification = 'CLEAR_NOTIFICATION',
|
||||||
// Config
|
// Config
|
||||||
getConfig = 'GET_CONFIG',
|
getConfig = 'GET_CONFIG',
|
||||||
updateConfig = 'UPDATE_CONFIG'
|
updateConfig = 'UPDATE_CONFIG',
|
||||||
|
fetchQueries = 'FETCH_QUERIES',
|
||||||
|
addQuery = 'ADD_QUERY',
|
||||||
|
deleteQuery = 'DELETE_QUERY',
|
||||||
|
updateQuery = 'UPDATE_QUERY',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
// Theme
|
// Theme
|
||||||
SetThemeAction |
|
| SetThemeAction
|
||||||
// Apps
|
// Apps
|
||||||
GetAppsAction<any> |
|
| GetAppsAction<any>
|
||||||
PinAppAction |
|
| PinAppAction
|
||||||
AddAppAction |
|
| AddAppAction
|
||||||
DeleteAppAction |
|
| DeleteAppAction
|
||||||
UpdateAppAction |
|
| UpdateAppAction
|
||||||
ReorderAppsAction |
|
| ReorderAppsAction
|
||||||
SortAppsAction |
|
| SortAppsAction
|
||||||
// Categories
|
// Categories
|
||||||
GetCategoriesAction<any> |
|
| GetCategoriesAction<any>
|
||||||
AddCategoryAction |
|
| AddCategoryAction
|
||||||
PinCategoryAction |
|
| PinCategoryAction
|
||||||
DeleteCategoryAction |
|
| DeleteCategoryAction
|
||||||
UpdateCategoryAction |
|
| UpdateCategoryAction
|
||||||
SortCategoriesAction |
|
| SortCategoriesAction
|
||||||
ReorderCategoriesAction |
|
| ReorderCategoriesAction
|
||||||
// Bookmarks
|
// Bookmarks
|
||||||
AddBookmarkAction |
|
| AddBookmarkAction
|
||||||
DeleteBookmarkAction |
|
| DeleteBookmarkAction
|
||||||
UpdateBookmarkAction |
|
| UpdateBookmarkAction
|
||||||
// Notifications
|
// Notifications
|
||||||
CreateNotificationAction |
|
| CreateNotificationAction
|
||||||
ClearNotificationAction |
|
| ClearNotificationAction
|
||||||
// Config
|
// Config
|
||||||
GetConfigAction |
|
| GetConfigAction
|
||||||
UpdateConfigAction;
|
| UpdateConfigAction
|
||||||
|
| FetchQueriesAction
|
||||||
|
| AddQueryAction
|
||||||
|
| DeleteQueryAction
|
||||||
|
| UpdateQueryAction;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
import { ActionTypes } from './actionTypes';
|
import { ActionTypes } from './actionTypes';
|
||||||
import { Config, ApiResponse } from '../../interfaces';
|
import { Config, ApiResponse, Query } from '../../interfaces';
|
||||||
import { CreateNotificationAction } from './notification';
|
import { CreateNotificationAction } from './notification';
|
||||||
import { searchConfig } from '../../utility';
|
import { searchConfig } from '../../utility';
|
||||||
|
|
||||||
|
@ -13,18 +13,18 @@ export interface GetConfigAction {
|
||||||
export const getConfig = () => async (dispatch: Dispatch) => {
|
export const getConfig = () => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<ApiResponse<Config[]>>('/api/config');
|
const res = await axios.get<ApiResponse<Config[]>>('/api/config');
|
||||||
|
|
||||||
dispatch<GetConfigAction>({
|
dispatch<GetConfigAction>({
|
||||||
type: ActionTypes.getConfig,
|
type: ActionTypes.getConfig,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
|
|
||||||
// Set custom page title if set
|
// Set custom page title if set
|
||||||
document.title = searchConfig('customTitle', 'Flame');
|
document.title = searchConfig('customTitle', 'Flame');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface UpdateConfigAction {
|
export interface UpdateConfigAction {
|
||||||
type: ActionTypes.updateConfig;
|
type: ActionTypes.updateConfig;
|
||||||
|
@ -34,19 +34,102 @@ export interface UpdateConfigAction {
|
||||||
export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
|
export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.put<ApiResponse<Config[]>>('/api/config', formData);
|
const res = await axios.put<ApiResponse<Config[]>>('/api/config', formData);
|
||||||
|
|
||||||
dispatch<CreateNotificationAction>({
|
dispatch<CreateNotificationAction>({
|
||||||
type: ActionTypes.createNotification,
|
type: ActionTypes.createNotification,
|
||||||
payload: {
|
payload: {
|
||||||
title: 'Success',
|
title: 'Success',
|
||||||
message: 'Settings updated'
|
message: 'Settings updated',
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
dispatch<UpdateConfigAction>({
|
dispatch<UpdateConfigAction>({
|
||||||
type: ActionTypes.updateConfig,
|
type: ActionTypes.updateConfig,
|
||||||
payload: res.data.data
|
payload: res.data.data,
|
||||||
})
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export interface FetchQueriesAction {
|
||||||
|
type: ActionTypes.fetchQueries;
|
||||||
|
payload: Query[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchQueries =
|
||||||
|
() => async (dispatch: Dispatch<FetchQueriesAction>) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get<ApiResponse<Query[]>>('/api/queries');
|
||||||
|
|
||||||
|
dispatch<FetchQueriesAction>({
|
||||||
|
type: ActionTypes.fetchQueries,
|
||||||
|
payload: res.data.data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AddQueryAction {
|
||||||
|
type: ActionTypes.addQuery;
|
||||||
|
payload: Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addQuery =
|
||||||
|
(query: Query) => async (dispatch: Dispatch<AddQueryAction>) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post<ApiResponse<Query>>('/api/queries', query);
|
||||||
|
|
||||||
|
dispatch<AddQueryAction>({
|
||||||
|
type: ActionTypes.addQuery,
|
||||||
|
payload: res.data.data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DeleteQueryAction {
|
||||||
|
type: ActionTypes.deleteQuery;
|
||||||
|
payload: Query[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteQuery =
|
||||||
|
(prefix: string) => async (dispatch: Dispatch<DeleteQueryAction>) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.delete<ApiResponse<Query[]>>(
|
||||||
|
`/api/queries/${prefix}`
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch<DeleteQueryAction>({
|
||||||
|
type: ActionTypes.deleteQuery,
|
||||||
|
payload: res.data.data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface UpdateQueryAction {
|
||||||
|
type: ActionTypes.updateQuery;
|
||||||
|
payload: Query[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateQuery =
|
||||||
|
(query: Query, oldPrefix: string) =>
|
||||||
|
async (dispatch: Dispatch<UpdateQueryAction>) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.put<ApiResponse<Query[]>>(
|
||||||
|
`/api/queries/${oldPrefix}`,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch<UpdateQueryAction>({
|
||||||
|
type: ActionTypes.updateQuery,
|
||||||
|
payload: res.data.data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -1,36 +1,77 @@
|
||||||
import { ActionTypes, Action } from '../actions';
|
import { ActionTypes, Action } from '../actions';
|
||||||
import { Config } from '../../interfaces';
|
import { Config, Query } from '../../interfaces';
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
config: Config[];
|
config: Config[];
|
||||||
|
customQueries: Query[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
loading: true,
|
loading: true,
|
||||||
config: []
|
config: [],
|
||||||
}
|
customQueries: [],
|
||||||
|
};
|
||||||
|
|
||||||
const getConfig = (state: State, action: Action): State => {
|
const getConfig = (state: State, action: Action): State => {
|
||||||
return {
|
return {
|
||||||
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
config: action.payload
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfig = (state: State, action: Action): State => {
|
const updateConfig = (state: State, action: Action): State => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
config: action.payload
|
config: action.payload,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const fetchQueries = (state: State, action: Action): State => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
customQueries: action.payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const addQuery = (state: State, action: Action): State => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
customQueries: [...state.customQueries, action.payload],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteQuery = (state: State, action: Action): State => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
customQueries: action.payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateQuery = (state: State, action: Action): State => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
customQueries: action.payload,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const configReducer = (state: State = initialState, action: Action) => {
|
const configReducer = (state: State = initialState, action: Action) => {
|
||||||
switch(action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.getConfig: return getConfig(state, action);
|
case ActionTypes.getConfig:
|
||||||
case ActionTypes.updateConfig: return updateConfig(state, action);
|
return getConfig(state, action);
|
||||||
default: return state;
|
case ActionTypes.updateConfig:
|
||||||
|
return updateConfig(state, action);
|
||||||
|
case ActionTypes.fetchQueries:
|
||||||
|
return fetchQueries(state, action);
|
||||||
|
case ActionTypes.addQuery:
|
||||||
|
return addQuery(state, action);
|
||||||
|
case ActionTypes.deleteQuery:
|
||||||
|
return deleteQuery(state, action);
|
||||||
|
case ActionTypes.updateQuery:
|
||||||
|
return updateQuery(state, action);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default configReducer;
|
export default configReducer;
|
||||||
|
|
|
@ -4,24 +4,31 @@ import { createNotification } from '../store/actions';
|
||||||
|
|
||||||
export const checkVersion = async (isForced: boolean = false) => {
|
export const checkVersion = async (isForced: boolean = false) => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get<string>('https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env');
|
const res = await axios.get<string>(
|
||||||
|
'https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env'
|
||||||
|
);
|
||||||
|
|
||||||
const githubVersion = res.data
|
const githubVersion = res.data
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(pair => pair.split('='))[0][1];
|
.map((pair) => pair.split('='))[0][1];
|
||||||
|
|
||||||
if (githubVersion !== process.env.REACT_APP_VERSION) {
|
if (githubVersion !== process.env.REACT_APP_VERSION) {
|
||||||
store.dispatch<any>(createNotification({
|
store.dispatch<any>(
|
||||||
title: 'Info',
|
createNotification({
|
||||||
message: 'New version is available!'
|
title: 'Info',
|
||||||
}))
|
message: 'New version is available!',
|
||||||
|
url: 'https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md',
|
||||||
|
})
|
||||||
|
);
|
||||||
} else if (isForced) {
|
} else if (isForced) {
|
||||||
store.dispatch<any>(createNotification({
|
store.dispatch<any>(
|
||||||
title: 'Info',
|
createNotification({
|
||||||
message: 'You are using the latest version!'
|
title: 'Info',
|
||||||
}))
|
message: 'You are using the latest version!',
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,4 +3,5 @@ export * from './urlParser';
|
||||||
export * from './searchConfig';
|
export * from './searchConfig';
|
||||||
export * from './checkVersion';
|
export * from './checkVersion';
|
||||||
export * from './sortData';
|
export * from './sortData';
|
||||||
export * from './searchParser';
|
export * from './searchParser';
|
||||||
|
export * from './redirectUrl';
|
||||||
|
|
7
client/src/utility/redirectUrl.ts
Normal file
7
client/src/utility/redirectUrl.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const redirectUrl = (url: string, sameTab: boolean) => {
|
||||||
|
if (sameTab) {
|
||||||
|
document.location.replace(url);
|
||||||
|
} else {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,11 +1,12 @@
|
||||||
import { queries } from './searchQueries.json';
|
import { queries } from './searchQueries.json';
|
||||||
import { Query, SearchResult } from '../interfaces';
|
import { Query, SearchResult } from '../interfaces';
|
||||||
|
import { store } from '../store/store';
|
||||||
import { searchConfig } from '.';
|
import { searchConfig } from '.';
|
||||||
|
|
||||||
export const searchParser = (searchQuery: string): SearchResult => {
|
export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
const result: SearchResult = {
|
const result: SearchResult = {
|
||||||
isLocal: false,
|
isLocal: false,
|
||||||
|
isURL: false,
|
||||||
sameTab: false,
|
sameTab: false,
|
||||||
search: '',
|
search: '',
|
||||||
query: {
|
query: {
|
||||||
|
@ -15,6 +16,15 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customQueries = store.getState().config.customQueries;
|
||||||
|
|
||||||
|
// Check if url or ip was passed
|
||||||
|
const urlRegex =
|
||||||
|
/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
|
||||||
|
|
||||||
|
result.isURL = urlRegex.test(searchQuery);
|
||||||
|
|
||||||
|
// Match prefix and query
|
||||||
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
|
||||||
|
|
||||||
const prefix = splitQuery
|
const prefix = splitQuery
|
||||||
|
@ -25,8 +35,11 @@ export const searchParser = (searchQuery: string): SearchResult => {
|
||||||
? encodeURIComponent(splitQuery[2])
|
? encodeURIComponent(splitQuery[2])
|
||||||
: encodeURIComponent(searchQuery);
|
: encodeURIComponent(searchQuery);
|
||||||
|
|
||||||
const query = queries.find((q: Query) => q.prefix === prefix);
|
const query = [...queries, ...customQueries].find(
|
||||||
|
(q: Query) => q.prefix === prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
// If search provider was found
|
||||||
if (query) {
|
if (query) {
|
||||||
result.query = query;
|
result.query = query;
|
||||||
result.search = search;
|
result.search = search;
|
||||||
|
|
|
@ -162,7 +162,7 @@ exports.getCss = asyncWrapper(async (req, res, next) => {
|
||||||
// @access Public
|
// @access Public
|
||||||
exports.updateCss = asyncWrapper(async (req, res, next) => {
|
exports.updateCss = asyncWrapper(async (req, res, next) => {
|
||||||
const file = new File(join(__dirname, '../public/flame.css'));
|
const file = new File(join(__dirname, '../public/flame.css'));
|
||||||
file.write(req.body.styles);
|
file.write(req.body.styles, false);
|
||||||
|
|
||||||
// Copy file to docker volume
|
// Copy file to docker volume
|
||||||
fs.copyFileSync(
|
fs.copyFileSync(
|
||||||
|
|
81
controllers/queries/index.js
Normal file
81
controllers/queries/index.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
const asyncWrapper = require('../../middleware/asyncWrapper');
|
||||||
|
const File = require('../../utils/File');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
const QUERIES_PATH = join(__dirname, '../../data/customQueries.json');
|
||||||
|
|
||||||
|
// @desc Add custom search query
|
||||||
|
// @route POST /api/queries
|
||||||
|
// @access Public
|
||||||
|
exports.addQuery = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(QUERIES_PATH);
|
||||||
|
let content = JSON.parse(file.read());
|
||||||
|
|
||||||
|
// Add new query
|
||||||
|
content.queries.push(req.body);
|
||||||
|
file.write(content, true);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: req.body,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @desc Get custom queries file
|
||||||
|
// @route GET /api/queries
|
||||||
|
// @access Public
|
||||||
|
exports.getQueries = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(QUERIES_PATH);
|
||||||
|
const content = JSON.parse(file.read());
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: content.queries,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @desc Update query
|
||||||
|
// @route PUT /api/queries/:prefix
|
||||||
|
// @access Public
|
||||||
|
exports.updateQuery = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(QUERIES_PATH);
|
||||||
|
let content = JSON.parse(file.read());
|
||||||
|
|
||||||
|
let queryIdx = content.queries.findIndex(
|
||||||
|
(q) => q.prefix == req.params.prefix
|
||||||
|
);
|
||||||
|
|
||||||
|
// query found
|
||||||
|
if (queryIdx > -1) {
|
||||||
|
content.queries = [
|
||||||
|
...content.queries.slice(0, queryIdx),
|
||||||
|
req.body,
|
||||||
|
...content.queries.slice(queryIdx + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write(content, true);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: content.queries,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @desc Delete query
|
||||||
|
// @route DELETE /api/queries/:prefix
|
||||||
|
// @access Public
|
||||||
|
exports.deleteQuery = asyncWrapper(async (req, res, next) => {
|
||||||
|
const file = new File(QUERIES_PATH);
|
||||||
|
let content = JSON.parse(file.read());
|
||||||
|
|
||||||
|
content.queries = content.queries.filter(
|
||||||
|
(q) => q.prefix != req.params.prefix
|
||||||
|
);
|
||||||
|
file.write(content, true);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: content.queries,
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,10 +5,11 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"init-server": "echo Instaling server dependencies && npm install && mkdir public && touch public/flame.css",
|
"init-server": "echo Instaling server dependencies && npm install",
|
||||||
"init-client": "cd client && echo Instaling client dependencies && npm install",
|
"init-client": "cd client && echo Instaling client dependencies && npm install",
|
||||||
"dev-init": "npm run init-server && npm run init-client",
|
"dir-init": "npx mkdirp data public && touch public/flame.css public/customQueries.json",
|
||||||
"dev-server": "nodemon server.js",
|
"dev-init": "npm run dir-init && npm run init-server && npm run init-client",
|
||||||
|
"dev-server": "nodemon server.js -e js",
|
||||||
"dev-client": "npm start --prefix client",
|
"dev-client": "npm start --prefix client",
|
||||||
"dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"",
|
"dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"",
|
||||||
"skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\""
|
"skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\""
|
||||||
|
|
|
@ -12,21 +12,10 @@ const {
|
||||||
getCss,
|
getCss,
|
||||||
} = require('../controllers/config');
|
} = require('../controllers/config');
|
||||||
|
|
||||||
router
|
router.route('/').post(createPair).get(getAllPairs).put(updateValues);
|
||||||
.route('/')
|
|
||||||
.post(createPair)
|
|
||||||
.get(getAllPairs)
|
|
||||||
.put(updateValues);
|
|
||||||
|
|
||||||
router
|
router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair);
|
||||||
.route('/:key')
|
|
||||||
.get(getSinglePair)
|
|
||||||
.put(updateValue)
|
|
||||||
.delete(deletePair);
|
|
||||||
|
|
||||||
router
|
router.route('/0/css').get(getCss).put(updateCss);
|
||||||
.route('/0/css')
|
|
||||||
.get(getCss)
|
|
||||||
.put(updateCss);
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
14
routes/queries.js
Normal file
14
routes/queries.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const {
|
||||||
|
getQueries,
|
||||||
|
addQuery,
|
||||||
|
deleteQuery,
|
||||||
|
updateQuery,
|
||||||
|
} = require('../controllers/queries/');
|
||||||
|
|
||||||
|
router.route('/').post(addQuery).get(getQueries);
|
||||||
|
router.route('/:prefix').delete(deleteQuery).put(updateQuery);
|
||||||
|
|
||||||
|
module.exports = router;
|
27
server.js
27
server.js
|
@ -1,23 +1,28 @@
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
|
||||||
|
// Database
|
||||||
const { connectDB } = require('./db');
|
const { connectDB } = require('./db');
|
||||||
|
const associateModels = require('./models/associateModels');
|
||||||
|
|
||||||
|
// Server
|
||||||
const api = require('./api');
|
const api = require('./api');
|
||||||
const jobs = require('./utils/jobs');
|
const jobs = require('./utils/jobs');
|
||||||
const Socket = require('./Socket');
|
const Socket = require('./Socket');
|
||||||
const Sockets = require('./Sockets');
|
const Sockets = require('./Sockets');
|
||||||
const associateModels = require('./models/associateModels');
|
|
||||||
const initConfig = require('./utils/initConfig');
|
// Utils
|
||||||
const findCss = require('./utils/findCss');
|
const initApp = require('./utils/init');
|
||||||
const Logger = require('./utils/Logger');
|
const Logger = require('./utils/Logger');
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
const PORT = process.env.PORT || 5005;
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const PORT = process.env.PORT || 5005;
|
||||||
|
|
||||||
|
// Init app
|
||||||
await connectDB();
|
await connectDB();
|
||||||
await associateModels();
|
await associateModels();
|
||||||
await initConfig();
|
await initApp();
|
||||||
findCss();
|
|
||||||
|
|
||||||
// Create server for Express API and WebSockets
|
// Create server for Express API and WebSockets
|
||||||
const server = http.createServer();
|
const server = http.createServer();
|
||||||
|
@ -28,6 +33,8 @@ const PORT = process.env.PORT || 5005;
|
||||||
Sockets.registerSocket('weather', weatherSocket);
|
Sockets.registerSocket('weather', weatherSocket);
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`);
|
logger.log(
|
||||||
})
|
`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`
|
||||||
})();
|
);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
|
@ -3,7 +3,7 @@ const fs = require('fs');
|
||||||
class File {
|
class File {
|
||||||
constructor(path) {
|
constructor(path) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.content = '';
|
this.content = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
read() {
|
read() {
|
||||||
|
@ -16,10 +16,13 @@ class File {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write(data) {
|
write(data, isJSON) {
|
||||||
this.content = data;
|
this.content = data;
|
||||||
fs.writeFileSync(this.path, this.content);
|
fs.writeFileSync(
|
||||||
|
this.path,
|
||||||
|
isJSON ? JSON.stringify(this.content) : this.content
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = File;
|
module.exports = File;
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
const fs = require('fs');
|
|
||||||
const { join } = require('path');
|
|
||||||
const Logger = require('./Logger');
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
// Check if flame.css exists in mounted docker volume. Create new file if not
|
|
||||||
const findCss = () => {
|
|
||||||
const srcPath = join(__dirname, '../data/flame.css');
|
|
||||||
const destPath = join(__dirname, '../public/flame.css');
|
|
||||||
|
|
||||||
if (fs.existsSync(srcPath)) {
|
|
||||||
fs.copyFileSync(srcPath, destPath);
|
|
||||||
logger.log('Custom CSS file found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log('Creating empty CSS file');
|
|
||||||
fs.writeFileSync(destPath, '');
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = findCss;
|
|
32
utils/init/createFile.js
Normal file
32
utils/init/createFile.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const { join } = require('path');
|
||||||
|
|
||||||
|
const Logger = require('../Logger');
|
||||||
|
const logger = new Logger();
|
||||||
|
|
||||||
|
const createFile = async (file) => {
|
||||||
|
const { name, msg, template, isJSON, paths } = file;
|
||||||
|
|
||||||
|
const srcPath = join(__dirname, paths.src, name);
|
||||||
|
const destPath = join(__dirname, paths.dest, name);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (fs.existsSync(srcPath)) {
|
||||||
|
fs.copyFileSync(srcPath, destPath);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
logger.log(msg.found);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file if not
|
||||||
|
fs.writeFileSync(destPath, isJSON ? JSON.stringify(template) : template);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
logger.log(msg.created);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = createFile;
|
9
utils/init/index.js
Normal file
9
utils/init/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
const initConfig = require('./initConfig');
|
||||||
|
const initFiles = require('./initFiles');
|
||||||
|
|
||||||
|
const initApp = async () => {
|
||||||
|
await initConfig();
|
||||||
|
await initFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = initApp;
|
|
@ -1,7 +1,8 @@
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const Config = require('../models/Config');
|
const Config = require('../../models/Config');
|
||||||
const { config } = require('./initialConfig.json');
|
const { config } = require('./initialConfig.json');
|
||||||
const Logger = require('./Logger');
|
|
||||||
|
const Logger = require('../Logger');
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
|
|
||||||
const initConfig = async () => {
|
const initConfig = async () => {
|
||||||
|
@ -28,7 +29,10 @@ const initConfig = async () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log('Initial config created');
|
if (process.env.NODE_ENV == 'development') {
|
||||||
|
logger.log('Initial config created');
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
8
utils/init/initFiles.js
Normal file
8
utils/init/initFiles.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const createFile = require('./createFile');
|
||||||
|
const { files } = require('./initialFiles.json');
|
||||||
|
|
||||||
|
const initFiles = async () => {
|
||||||
|
files.forEach(async (file) => await createFile(file));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = initFiles;
|
32
utils/init/initialFiles.json
Normal file
32
utils/init/initialFiles.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "flame.css",
|
||||||
|
"msg": {
|
||||||
|
"created": "Created empty CSS file",
|
||||||
|
"found": "Custom CSS file found"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"src": "../../data",
|
||||||
|
"dest": "../../public"
|
||||||
|
},
|
||||||
|
"template": "",
|
||||||
|
"isJSON": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "customQueries.json",
|
||||||
|
"msg": {
|
||||||
|
"created": "Created empty queries file",
|
||||||
|
"found": "Custom queries file found"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"src": "../../data",
|
||||||
|
"dest": "../../data"
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"queries": []
|
||||||
|
},
|
||||||
|
"isJSON": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in a new issue