1
0
Fork 0
mirror of https://github.com/pawelmalak/flame.git synced 2025-08-04 02:15:18 +02:00

Merge branch 'feature' into v1.7.0

This commit is contained in:
Paweł Malak 2021-10-11 14:31:59 +02:00
commit 55f192f664
53 changed files with 1172 additions and 385 deletions

View file

@ -22,7 +22,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
const [formData, setFormData] = useState<NewApp>({
name: '',
url: '',
icon: ''
icon: '',
});
useEffect(() => {
@ -30,13 +30,13 @@ const AppForm = (props: ComponentProps): JSX.Element => {
setFormData({
name: props.app.name,
url: props.app.url,
icon: props.app.icon
icon: props.app.icon,
});
} else {
setFormData({
name: '',
url: '',
icon: ''
icon: '',
});
}
}, [props.app]);
@ -44,7 +44,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
setFormData({
...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 data = new FormData();
if (customIcon) {
data.append('icon', customIcon);
}
@ -88,10 +89,8 @@ const AppForm = (props: ComponentProps): JSX.Element => {
setFormData({
name: '',
url: '',
icon: ''
icon: '',
});
setCustomIcon(null);
};
return (
@ -100,33 +99,33 @@ const AppForm = (props: ComponentProps): JSX.Element => {
formHandler={formSubmitHandler}
>
<InputGroup>
<label htmlFor='name'>App Name</label>
<label htmlFor="name">App Name</label>
<input
type='text'
name='name'
id='name'
placeholder='Bookstack'
type="text"
name="name"
id="name"
placeholder="Bookstack"
required
value={formData.name}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor='url'>App URL</label>
<label htmlFor="url">App URL</label>
<input
type='text'
name='url'
id='url'
placeholder='bookstack.example.com'
type="text"
name="url"
id="url"
placeholder="bookstack.example.com"
required
value={formData.url}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
<a
href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
target='_blank'
rel='noreferrer'
href="https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks"
target="_blank"
rel="noreferrer"
>
{' '}
Check supported URL formats
@ -136,19 +135,19 @@ const AppForm = (props: ComponentProps): JSX.Element => {
{!useCustomIcon ? (
// use mdi icon
<InputGroup>
<label htmlFor='icon'>App Icon</label>
<label htmlFor="icon">App Icon</label>
<input
type='text'
name='icon'
id='icon'
placeholder='book-open-outline'
type="text"
name="icon"
id="icon"
placeholder="book-open-outline"
required
value={formData.icon}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Use icon name from MDI.
<a href='https://materialdesignicons.com/' target='blank'>
<a href="https://materialdesignicons.com/" target="blank">
{' '}
Click here for reference
</a>
@ -163,17 +162,20 @@ const AppForm = (props: ComponentProps): JSX.Element => {
) : (
// upload custom icon
<InputGroup>
<label htmlFor='icon'>App Icon</label>
<label htmlFor="icon">App Icon</label>
<input
type='file'
name='icon'
id='icon'
type="file"
name="icon"
id="icon"
required
onChange={e => fileChangeHandler(e)}
accept='.jpg,.jpeg,.png,.svg'
onChange={(e) => fileChangeHandler(e)}
accept=".jpg,.jpeg,.png,.svg"
/>
<span
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
onClick={() => {
setCustomIcon(null);
toggleUseCustomIcon(!useCustomIcon);
}}
className={classes.Switch}
>
Switch to MDI

View file

@ -1,32 +1,40 @@
// React
import {
useState,
SyntheticEvent,
Fragment,
ChangeEvent,
useEffect
useEffect,
} from 'react';
import { connect } from 'react-redux';
import ModalForm from '../../UI/Forms/ModalForm/ModalForm';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import {
Bookmark,
Category,
GlobalState,
NewBookmark,
NewCategory,
NewNotification
} from '../../../interfaces';
import { ContentType } from '../Bookmarks';
// Redux
import { connect } from 'react-redux';
import {
getCategories,
addCategory,
addBookmark,
updateCategory,
updateBookmark,
createNotification
createNotification,
} 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';
// CSS
import classes from './BookmarkForm.module.css';
interface ComponentProps {
@ -53,14 +61,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const [useCustomIcon, toggleUseCustomIcon] = useState<boolean>(false);
const [customIcon, setCustomIcon] = useState<File | null>(null);
const [categoryName, setCategoryName] = useState<NewCategory>({
name: ''
name: '',
});
const [formData, setFormData] = useState<NewBookmark>({
name: '',
url: '',
categoryId: -1,
icon: ''
icon: '',
});
// Load category data if provided for editing
@ -79,14 +87,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
name: props.bookmark.name,
url: props.bookmark.url,
categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon
icon: props.bookmark.icon,
});
} else {
setFormData({
name: '',
url: '',
categoryId: -1,
icon: ''
icon: '',
});
}
}, [props.bookmark]);
@ -117,7 +125,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
if (formData.categoryId === -1) {
props.createNotification({
title: 'Error',
message: 'Please select category'
message: 'Please select category',
});
return;
}
@ -133,10 +141,10 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
name: '',
url: '',
categoryId: formData.categoryId,
icon: ''
icon: '',
});
setCustomIcon(null);
// setCustomIcon(null);
}
} else {
// Update
@ -150,12 +158,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const data = createFormData();
props.updateBookmark(props.bookmark.id, data, {
prev: props.bookmark.categoryId,
curr: formData.categoryId
curr: formData.categoryId,
});
} else {
props.updateBookmark(props.bookmark.id, formData, {
prev: props.bookmark.categoryId,
curr: formData.categoryId
curr: formData.categoryId,
});
}
@ -163,7 +171,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
name: '',
url: '',
categoryId: -1,
icon: ''
icon: '',
});
setCustomIcon(null);
@ -176,14 +184,14 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
setFormData({
...formData,
[e.target.name]: e.target.value
[e.target.name]: e.target.value,
});
};
const selectChangeHandler = (e: ChangeEvent<HTMLSelectElement>): void => {
setFormData({
...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 ? (
<Fragment>
<InputGroup>
<label htmlFor='categoryName'>Category Name</label>
<label htmlFor="categoryName">Category Name</label>
<input
type='text'
name='categoryName'
id='categoryName'
placeholder='Social Media'
type="text"
name="categoryName"
id="categoryName"
placeholder="Social Media"
required
value={categoryName.name}
onChange={e => setCategoryName({ name: e.target.value })}
onChange={(e) => setCategoryName({ name: e.target.value })}
/>
</InputGroup>
</Fragment>
) : (
<Fragment>
<InputGroup>
<label htmlFor='name'>Bookmark Name</label>
<label htmlFor="name">Bookmark Name</label>
<input
type='text'
name='name'
id='name'
placeholder='Reddit'
type="text"
name="name"
id="name"
placeholder="Reddit"
required
value={formData.name}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor='url'>Bookmark URL</label>
<label htmlFor="url">Bookmark URL</label>
<input
type='text'
name='url'
id='url'
placeholder='reddit.com'
type="text"
name="url"
id="url"
placeholder="reddit.com"
required
value={formData.url}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
<a
href='https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks'
target='_blank'
rel='noreferrer'
href="https://github.com/pawelmalak/flame#supported-url-formats-for-applications-and-bookmarks"
target="_blank"
rel="noreferrer"
>
{' '}
Check supported URL formats
@ -264,12 +272,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
</span>
</InputGroup>
<InputGroup>
<label htmlFor='categoryId'>Bookmark Category</label>
<label htmlFor="categoryId">Bookmark Category</label>
<select
name='categoryId'
id='categoryId'
name="categoryId"
id="categoryId"
required
onChange={e => selectChangeHandler(e)}
onChange={(e) => selectChangeHandler(e)}
value={formData.categoryId}
>
<option value={-1}>Select category</option>
@ -285,18 +293,18 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
{!useCustomIcon ? (
// mdi
<InputGroup>
<label htmlFor='icon'>Bookmark Icon (optional)</label>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type='text'
name='icon'
id='icon'
placeholder='book-open-outline'
type="text"
name="icon"
id="icon"
placeholder="book-open-outline"
value={formData.icon}
onChange={e => inputChangeHandler(e)}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Use icon name from MDI.
<a href='https://materialdesignicons.com/' target='blank'>
<a href="https://materialdesignicons.com/" target="blank">
{' '}
Click here for reference
</a>
@ -311,16 +319,19 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
) : (
// custom
<InputGroup>
<label htmlFor='icon'>Bookmark Icon (optional)</label>
<label htmlFor="icon">Bookmark Icon (optional)</label>
<input
type='file'
name='icon'
id='icon'
onChange={e => fileChangeHandler(e)}
accept='.jpg,.jpeg,.png,.svg'
type="file"
name="icon"
id="icon"
onChange={(e) => fileChangeHandler(e)}
accept=".jpg,.jpeg,.png,.svg"
/>
<span
onClick={() => toggleUseCustomIcon(!useCustomIcon)}
onClick={() => {
setCustomIcon(null);
toggleUseCustomIcon(!useCustomIcon);
}}
className={classes.Switch}
>
Switch to MDI
@ -336,7 +347,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const mapStateToProps = (state: GlobalState) => {
return {
categories: state.bookmark.categories
categories: state.bookmark.categories,
};
};
@ -346,7 +357,7 @@ const dispatchMap = {
addBookmark,
updateCategory,
updateBookmark,
createNotification
createNotification,
};
export default connect(mapStateToProps, dispatchMap)(BookmarkForm);

View file

@ -20,19 +20,20 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => {
<Notification
title={notification.title}
message={notification.message}
url={notification.url || null}
id={notification.id}
key={notification.id}
/>
)
);
})}
</div>
)
}
);
};
const mapStateToProps = (state: GlobalState) => {
return {
notifications: state.notification.notifications
}
}
notifications: state.notification.notifications,
};
};
export default connect(mapStateToProps)(NotificationCenter);
export default connect(mapStateToProps)(NotificationCenter);

View file

@ -11,7 +11,7 @@ import { NewNotification } from '../../interfaces';
import classes from './SearchBar.module.css';
// Utils
import { searchParser } from '../../utility';
import { searchParser, urlParser, redirectUrl } from '../../utility';
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
@ -28,28 +28,28 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
}, []);
const searchHandler = (e: KeyboardEvent<HTMLInputElement>) => {
const searchResult = searchParser(inputRef.current.value);
const { isLocal, search, query, isURL, sameTab } = searchParser(
inputRef.current.value
);
if (searchResult.isLocal) {
setLocalSearch(searchResult.search);
if (isLocal) {
setLocalSearch(search);
}
if (e.code === 'Enter') {
if (!searchResult.query.prefix) {
if (!query.prefix) {
createNotification({
title: 'Error',
message: 'Prefix not found',
});
} else if (searchResult.isLocal) {
setLocalSearch(searchResult.search);
} else if (isURL) {
const url = urlParser(inputRef.current.value)[1];
redirectUrl(url, sameTab);
} else if (isLocal) {
setLocalSearch(search);
} else {
if (searchResult.sameTab) {
document.location.replace(
`${searchResult.query.template}${searchResult.search}`
);
} else {
window.open(`${searchResult.query.template}${searchResult.search}`);
}
const url = `${query.template}${search}`;
redirectUrl(url, sameTab);
}
}
};

View file

@ -13,20 +13,16 @@ import {
import {
GlobalState,
NewNotification,
Query,
SettingsForm,
} from '../../../interfaces';
// UI
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
// CSS
import classes from './OtherSettings.module.css';
import SettingsHeadline from '../../UI/Headlines/SettingsHeadline/SettingsHeadline';
// Utils
import { searchConfig } from '../../../utility';
import { queries } from '../../../utility/searchQueries.json';
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
@ -45,12 +41,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
hideHeader: 0,
hideApps: 0,
hideCategories: 0,
hideSearch: 0,
defaultSearchProvider: 'd',
useOrdering: 'createdAt',
appsSameTab: 0,
bookmarksSameTab: 0,
searchSameTab: 0,
dockerApps: 1,
dockerHost: 'localhost',
kubernetesApps: 1,
@ -66,12 +59,9 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
hideHeader: searchConfig('hideHeader', 0),
hideApps: searchConfig('hideApps', 0),
hideCategories: searchConfig('hideCategories', 0),
hideSearch: searchConfig('hideSearch', 0),
defaultSearchProvider: searchConfig('defaultSearchProvider', 'd'),
useOrdering: searchConfig('useOrdering', 'createdAt'),
appsSameTab: searchConfig('appsSameTab', 0),
bookmarksSameTab: searchConfig('bookmarksSameTab', 0),
searchSameTab: searchConfig('searchSameTab', 0),
dockerApps: searchConfig('dockerApps', 0),
dockerHost: searchConfig('dockerHost', 'localhost'),
kubernetesApps: searchConfig('kubernetesApps', 0),
@ -114,7 +104,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
return (
<form onSubmit={(e) => formSubmitHandler(e)}>
{/* OTHER OPTIONS */}
<h2 className={classes.SettingsSection}>Miscellaneous</h2>
<SettingsHeadline text="Miscellaneous" />
<InputGroup>
<label htmlFor="customTitle">Custom page title</label>
<input
@ -128,7 +118,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup>
{/* BEAHVIOR OPTIONS */}
<h2 className={classes.SettingsSection}>App Behavior</h2>
<SettingsHeadline text="App Behavior" />
<InputGroup>
<label htmlFor="pinAppsByDefault">
Pin new applications by default
@ -170,35 +160,6 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value="orderId">Custom order</option>
</select>
</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>
<label htmlFor="appsSameTab">Open applications in the same tab</label>
<select
@ -225,19 +186,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup>
{/* MODULES OPTIONS */}
<h2 className={classes.SettingsSection}>Modules</h2>
<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>
<SettingsHeadline text="Modules" />
<InputGroup>
<label htmlFor="hideHeader">Hide greeting and date</label>
<select
@ -276,7 +225,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup>
{/* DOCKER SETTINGS */}
<h2 className={classes.SettingsSection}>Docker</h2>
<SettingsHeadline text="Docker" />
<InputGroup>
<label htmlFor="dockerHost">Docker Host</label>
<input
@ -316,7 +265,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
</InputGroup>
{/* KUBERNETES SETTINGS */}
<h2 className={classes.SettingsSection}>Kubernetes</h2>
<SettingsHeadline text="Kubernetes" />
<InputGroup>
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
<select

View file

@ -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;
}

View file

@ -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
);

View file

@ -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);

View 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);

View file

@ -1,73 +1,61 @@
//
import { NavLink, Link, Switch, Route } from 'react-router-dom';
// Typescript
import { Route as SettingsRoute } from '../../interfaces';
// CSS
import classes from './Settings.module.css';
import { Container } from '../UI/Layout/Layout';
import Headline from '../UI/Headlines/Headline/Headline';
// Components
import Themer from '../Themer/Themer';
import WeatherSettings from './WeatherSettings/WeatherSettings';
import OtherSettings from './OtherSettings/OtherSettings';
import AppDetails from './AppDetails/AppDetails';
import StyleSettings from './StyleSettings/StyleSettings';
import SearchSettings from './SearchSettings/SearchSettings';
// UI
import { Container } from '../UI/Layout/Layout';
import Headline from '../UI/Headlines/Headline/Headline';
// Data
import { routes } from './settings.json';
const Settings = (): JSX.Element => {
return (
<Container>
<Headline
title='Settings'
subtitle={<Link to='/'>Go back</Link>}
/>
<Headline title="Settings" subtitle={<Link to="/">Go back</Link>} />
<div className={classes.Settings}>
{/* NAVIGATION MENU */}
<nav className={classes.SettingsNav}>
<NavLink
className={classes.SettingsNavLink}
activeClassName={classes.SettingsNavLinkActive}
exact
to='/settings'>
Theme
</NavLink>
<NavLink
className={classes.SettingsNavLink}
activeClassName={classes.SettingsNavLinkActive}
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>
{routes.map(({ name, dest }: SettingsRoute, idx) => (
<NavLink
className={classes.SettingsNavLink}
activeClassName={classes.SettingsNavLinkActive}
exact
to={dest}
key={idx}
>
{name}
</NavLink>
))}
</nav>
{/* ROUTES */}
<section className={classes.SettingsContent}>
<Switch>
<Route exact path='/settings' component={Themer} />
<Route path='/settings/weather' component={WeatherSettings} />
<Route path='/settings/other' component={OtherSettings} />
<Route path='/settings/css' component={StyleSettings} />
<Route path='/settings/app' component={AppDetails} />
<Route exact path="/settings" component={Themer} />
<Route path="/settings/weather" component={WeatherSettings} />
<Route path="/settings/search" component={SearchSettings} />
<Route path="/settings/other" component={OtherSettings} />
<Route path="/settings/css" component={StyleSettings} />
<Route path="/settings/app" component={AppDetails} />
</Switch>
</section>
</div>
</Container>
)
}
);
};
export default Settings;
export default Settings;

View 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"
}
]
}

View file

@ -1,4 +1,4 @@
.SettingsSection {
.SettingsHeadline {
color: var(--color-primary);
padding-bottom: 3px;
margin-bottom: 10px;
@ -6,4 +6,4 @@
font-weight: 500;
border-bottom: 2px solid var(--color-accent);
display: inline-block;
}
}

View file

@ -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;

View file

@ -8,12 +8,16 @@ interface ComponentProps {
title: string;
message: string;
id: number;
url: string | null;
clearNotification: (id: number) => void;
}
const Notification = (props: ComponentProps): JSX.Element => {
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(() => {
const closeNotification = setTimeout(() => {
@ -22,21 +26,27 @@ const Notification = (props: ComponentProps): JSX.Element => {
const clearNotification = setTimeout(() => {
props.clearNotification(props.id);
}, 3600)
}, 3600);
return () => {
window.clearTimeout(closeNotification);
window.clearTimeout(clearNotification);
};
}, []);
const clickHandler = () => {
if (props.url) {
window.open(props.url, '_blank');
}
}, [])
};
return (
<div className={elementClasses}>
<div className={elementClasses} onClick={clickHandler}>
<h4>{props.title}</h4>
<p>{props.message}</p>
<div className={classes.Pog}></div>
</div>
)
}
);
};
export default connect(null, { clearNotification })(Notification);
export default connect(null, { clearNotification })(Notification);