diff --git a/.env b/.env index feb06862644d3e6ffe883616e70809be0b9876fe..1bb2edb93e4b779ae3624e197862ce17c4cb3640 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ PORT=5005 NODE_ENV=development -VERSION=1.6.9 \ No newline at end of file +VERSION=1.7.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2320b4f97ea06693fb1d99e4ca2186880046a5..54c68e1b95b7c8651f827342a2fec0234cf40144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### v1.7.0 (2021-10-11) +- Search bar will now redirect if valid URL or IP is provided ([#67](https://github.com/pawelmalak/flame/issues/67)) +- Users can now add their custom search providers ([#71](https://github.com/pawelmalak/flame/issues/71)) +- 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)) +- Fixed bug with overriding app icon created with docker labels + ### v1.6.9 (2021-10-09) - Added option for remote docker host ([#97](https://github.com/pawelmalak/flame/issues/97)) diff --git a/api.js b/api.js index 1c2d863556480cf89f47875e7b20e5d15de25788..9eb9b9f61148305f83f00346667f768bd3f9e162 100644 --- a/api.js +++ b/api.js @@ -20,6 +20,7 @@ api.use('/api/config', require('./routes/config')); api.use('/api/weather', require('./routes/weather')); api.use('/api/categories', require('./routes/category')); api.use('/api/bookmarks', require('./routes/bookmark')); +api.use('/api/queries', require('./routes/queries')); // Custom error handler api.use(errorHandler); diff --git a/client/.env b/client/.env index e9f8924595c0b362b9d2eb0ab7f2ec427cb5bca1..6dbe18b101cd0e2b50273215de71ac0985f7c082 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.6.9 \ No newline at end of file +REACT_APP_VERSION=1.7.0 \ No newline at end of file diff --git a/client/public/index.html b/client/public/index.html index 3f43c40421356e987beaa7de221200b0402b6154..c93d95eb1900df666249de95f3f58b52fcb986fa 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -4,16 +4,15 @@ - - - - + + Flame -
- - \ No newline at end of file + diff --git a/client/src/App.tsx b/client/src/App.tsx index 05db80558273993c8bf56da79ddc615b9d99284f..9311b4b76d003c3423061668b001c9f58c76ed2c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,5 +1,5 @@ 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'; // Redux @@ -27,15 +27,18 @@ if (localStorage.theme) { // Check for updates checkVersion(); +// fetch queries +store.dispatch(fetchQueries()); + const App = (): JSX.Element => { return ( - - - - + + + + diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff new file mode 100644 index 0000000000000000000000000000000000000000..c9eb5cabfba7d5ff961fce4c067f0d3fae77db11 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ce795fa8bdd3ca2b9c42124cc89e61e9272e9fb2 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff new file mode 100644 index 0000000000000000000000000000000000000000..a5d98fc6202f5cf5fd8b556ca834e8e9dbaafac1 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..01d05fa509b7f91526cabe90c9bafa130e4c118a Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff new file mode 100644 index 0000000000000000000000000000000000000000..c3933ba5beefd0e12a58f446168cc9f201489f2f Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..06437aef75ed94a43db5e819568decb69e6e88b7 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2 differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..86b386372664a278c839b4a6fedbf6a05b396b70 Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff differ diff --git a/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ebe1795f85a661c205e4a4612eaf47d56273e68e Binary files /dev/null and b/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2 differ diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 5d05f0a4d52381cff6055989526f1fb3e39fb798..d44418ee8131944d24f9c4e22148d7ca0c89439c 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -22,7 +22,7 @@ const AppForm = (props: ComponentProps): JSX.Element => { const [formData, setFormData] = useState({ 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): 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} > - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -136,19 +135,19 @@ const AppForm = (props: ComponentProps): JSX.Element => { {!useCustomIcon ? ( // use mdi icon - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -163,17 +162,20 @@ const AppForm = (props: ComponentProps): JSX.Element => { ) : ( // upload custom icon - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> toggleUseCustomIcon(!useCustomIcon)} + onClick={() => { + setCustomIcon(null); + toggleUseCustomIcon(!useCustomIcon); + }} className={classes.Switch} > Switch to MDI diff --git a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx index 10d6de230d495bb9f91de6a3b44085d23629d72f..5162c89d1c7cf8e0131e98ef5c51c023d4e64b21 100644 --- a/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx +++ b/client/src/components/Bookmarks/BookmarkForm/BookmarkForm.tsx @@ -1,32 +1,40 @@ +// React import { useState, SyntheticEvent, Fragment, ChangeEvent, - useEffect + useEffect, } from 'react'; + +// Redux import { connect } from 'react-redux'; +import { + getCategories, + addCategory, + addBookmark, + updateCategory, + updateBookmark, + createNotification, +} from '../../../store/actions'; -import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; -import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; +// Typescript import { Bookmark, Category, GlobalState, NewBookmark, NewCategory, - NewNotification + NewNotification, } from '../../../interfaces'; import { ContentType } from '../Bookmarks'; -import { - getCategories, - addCategory, - addBookmark, - updateCategory, - updateBookmark, - createNotification -} from '../../../store/actions'; + +// 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(false); const [customIcon, setCustomIcon] = useState(null); const [categoryName, setCategoryName] = useState({ - name: '' + name: '', }); const [formData, setFormData] = useState({ 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): void => { setFormData({ ...formData, - [e.target.name]: e.target.value + [e.target.name]: e.target.value, }); }; const selectChangeHandler = (e: ChangeEvent): 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 ? ( - + setCategoryName({ name: e.target.value })} + onChange={(e) => setCategoryName({ name: e.target.value })} /> ) : ( - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> {' '} Check supported URL formats @@ -264,12 +272,12 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { - + inputChangeHandler(e)} + onChange={(e) => inputChangeHandler(e)} /> Use icon name from MDI. - + {' '} Click here for reference @@ -311,16 +319,19 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => { ) : ( // custom - + fileChangeHandler(e)} - accept='.jpg,.jpeg,.png,.svg' + type="file" + name="icon" + id="icon" + onChange={(e) => fileChangeHandler(e)} + accept=".jpg,.jpeg,.png,.svg" /> 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); diff --git a/client/src/components/NotificationCenter/NotificationCenter.tsx b/client/src/components/NotificationCenter/NotificationCenter.tsx index 29c9cb248199d2029ce86b3672e2b14c36422cac..733316b2d6131bb2eed128e0c4f61a0e4c36c727 100644 --- a/client/src/components/NotificationCenter/NotificationCenter.tsx +++ b/client/src/components/NotificationCenter/NotificationCenter.tsx @@ -20,19 +20,20 @@ const NotificationCenter = (props: ComponentProps): JSX.Element => { - ) + ); })} - ) -} + ); +}; const mapStateToProps = (state: GlobalState) => { return { - notifications: state.notification.notifications - } -} + notifications: state.notification.notifications, + }; +}; -export default connect(mapStateToProps)(NotificationCenter); \ No newline at end of file +export default connect(mapStateToProps)(NotificationCenter); diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index f2ccdecc019ebb1551ef98f0581ab22942c30420..887a2ef92c4e151cf5bf67358534d9539d22f337 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -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) => { - 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); } } }; diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index bd9bf4962cd5653fa648f754848cccb6c31a7503..c3525f8c1b9a236d2a7caf55b32f18c3304c5d9a 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -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 (
formSubmitHandler(e)}> {/* OTHER OPTIONS */} -

Miscellaneous

+ { {/* BEAHVIOR OPTIONS */} -

App Behavior

+ - - - - - - - - inputChangeHandler(e, true)} - > - - - - + { {/* KUBERNETES SETTINGS */} -

Kubernetes

+ inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + + + inputChangeHandler(e)} + /> + + {query ? : } + + ); +}; + +export default connect(null, { addQuery, updateQuery })(QueriesForm); diff --git a/client/src/components/Settings/SearchSettings/SearchSettings.tsx b/client/src/components/Settings/SearchSettings/SearchSettings.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b2ac42245d7d2da147c52f5ddd7b529da34a89f9 --- /dev/null +++ b/client/src/components/Settings/SearchSettings/SearchSettings.tsx @@ -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({ + hideSearch: 0, + defaultSearchProvider: 'l', + searchSameTab: 0, + }); + + // Get config + useEffect(() => { + setFormData({ + hideSearch: searchConfig('hideSearch', 0), + defaultSearchProvider: searchConfig('defaultSearchProvider', 'l'), + 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, + isNumber?: boolean + ) => { + let value: string | number = e.target.value; + + if (isNumber) { + value = parseFloat(value); + } + + setFormData({ + ...formData, + [e.target.name]: value, + }); + }; + + return ( + + {/* GENERAL SETTINGS */} + formSubmitHandler(e)} + style={{ marginBottom: '30px' }} + > + + + + + + + + + + + + + + + + + {/* CUSTOM QUERIES */} + + + + ); +}; + +const mapStateToProps = (state: GlobalState) => { + return { + loading: state.config.loading, + customQueries: state.config.customQueries, + }; +}; + +const actions = { + createNotification, + updateConfig, +}; + +export default connect(mapStateToProps, actions)(SearchSettings); diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index b1eb300d1589d7af493ca4479033b74fdb305740..5df8ec64d1e4fa5356f70970033e340237254fe1 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -1,73 +1,61 @@ +// import { NavLink, Link, Switch, Route } from 'react-router-dom'; -import classes from './Settings.module.css'; +// Typescript +import { Route as SettingsRoute } from '../../interfaces'; -import { Container } from '../UI/Layout/Layout'; -import Headline from '../UI/Headlines/Headline/Headline'; +// CSS +import classes from './Settings.module.css'; +// Components import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; import AppDetails from './AppDetails/AppDetails'; import StyleSettings from './StyleSettings/StyleSettings'; +import SearchSettings from './SearchSettings/SearchSettings'; + +// 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 ( - Go back} - /> + Go back} />
+ {/* NAVIGATION MENU */} + + {/* ROUTES */}
- - - - - + + + + + +
- ) -} + ); +}; -export default Settings; \ No newline at end of file +export default Settings; diff --git a/client/src/components/Settings/settings.json b/client/src/components/Settings/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..3cc24e9b481f288f28285fcda56cab9cb300bb28 --- /dev/null +++ b/client/src/components/Settings/settings.json @@ -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" + } + ] +} diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.module.css b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css similarity index 89% rename from client/src/components/Settings/OtherSettings/OtherSettings.module.css rename to client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css index 36e4deb048fd138e9026e35085e7c05267893a75..137667c821892fe78309cbb4a487d14e5bef9398 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.module.css +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css @@ -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; -} \ No newline at end of file +} diff --git a/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5d149498162089faa8d96c1be8a1ba49fa42ad99 --- /dev/null +++ b/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx @@ -0,0 +1,11 @@ +const classes = require('./SettingsHeadline.module.css'); + +interface Props { + text: string; +} + +const SettingsHeadline = (props: Props): JSX.Element => { + return

{props.text}

; +}; + +export default SettingsHeadline; diff --git a/client/src/components/UI/Notification/Notification.tsx b/client/src/components/UI/Notification/Notification.tsx index 95109e1d424dbdd6e00939e726394a4c75354afe..2bd51851874f065a2d643cb2752f3518bfe50e4c 100644 --- a/client/src/components/UI/Notification/Notification.tsx +++ b/client/src/components/UI/Notification/Notification.tsx @@ -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 ( -
+

{props.title}

{props.message}

- ) -} + ); +}; -export default connect(null, { clearNotification })(Notification); \ No newline at end of file +export default connect(null, { clearNotification })(Notification); diff --git a/client/src/index.css b/client/src/index.css index db1d21b6e74f9420511921f60ec4288274e4ec94..8661f9778c4ac182c9cf09094134a09e13086f0c 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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; padding: 0; @@ -5,18 +41,18 @@ } body { - --color-background: #2B2C56; - --color-primary: #EFF1FC; - --color-accent: #6677EB; + --color-background: #242b33; + --color-primary: #effbff; + --color-accent: #6ee2ff; --spacing-ui: 10px; background-color: var(--color-background); transition: background-color 0.3s; - font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif; + font-family: Roboto, sans-serif; font-size: 14px; } a { color: var(--color-primary); text-decoration: none; -} \ No newline at end of file +} diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index bd53e2d28c56e9036a1c7efa558cd237daf424e4..9b195da0c9695e053f81fe84e8d6e98fe998bcb6 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -5,6 +5,12 @@ export interface WeatherForm { isCelsius: number; } +export interface SearchForm { + hideSearch: number; + defaultSearchProvider: string; + searchSameTab: number; +} + export interface SettingsForm { customTitle: string; pinAppsByDefault: number; @@ -12,12 +18,12 @@ export interface SettingsForm { hideHeader: number; hideApps: number; hideCategories: number; - hideSearch: number; - defaultSearchProvider: string; + // hideSearch: number; + // defaultSearchProvider: string; useOrdering: string; appsSameTab: number; bookmarksSameTab: number; - searchSameTab: number; + // searchSameTab: number; dockerApps: number; dockerHost: string; kubernetesApps: number; diff --git a/client/src/interfaces/Notification.ts b/client/src/interfaces/Notification.ts index 80a49f260dc2f705dcf7ce7193a16a83e8eaf801..50549228041e22edbefb9afb85671a201691d341 100644 --- a/client/src/interfaces/Notification.ts +++ b/client/src/interfaces/Notification.ts @@ -1,8 +1,9 @@ export interface NewNotification { title: string; message: string; + url?: string; } export interface Notification extends NewNotification { id: number; -} \ No newline at end of file +} diff --git a/client/src/interfaces/Route.ts b/client/src/interfaces/Route.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d571ddb9c30891261fead4c133971157e5dc66b --- /dev/null +++ b/client/src/interfaces/Route.ts @@ -0,0 +1,4 @@ +export interface Route { + name: string; + dest: string; +} diff --git a/client/src/interfaces/SearchResult.ts b/client/src/interfaces/SearchResult.ts index 271bdc2e1a404bc65de5d7f4e7695f46851d5ad3..3d6c8aede968004942c99b36bcdbe4243533276d 100644 --- a/client/src/interfaces/SearchResult.ts +++ b/client/src/interfaces/SearchResult.ts @@ -2,6 +2,7 @@ import { Query } from './Query'; export interface SearchResult { isLocal: boolean; + isURL: boolean; sameTab: boolean; search: string; query: Query; diff --git a/client/src/interfaces/index.ts b/client/src/interfaces/index.ts index 6892fb51dc09efb512812cfaa500628a8c08ef55..b9683dd80231dd97f7e355da6529424a6cdc2aa1 100644 --- a/client/src/interfaces/index.ts +++ b/client/src/interfaces/index.ts @@ -10,3 +10,4 @@ export * from './Config'; export * from './Forms'; export * from './Query'; export * from './SearchResult'; +export * from './Route'; diff --git a/client/src/store/actions/actionTypes.ts b/client/src/store/actions/actionTypes.ts index 4324834bc9df7a359dbe32e1d6d070bd98d9e6dc..c670b2fa7cbf30be84281720b5a86bfe5e1e6da6 100644 --- a/client/src/store/actions/actionTypes.ts +++ b/client/src/store/actions/actionTypes.ts @@ -26,8 +26,14 @@ import { ClearNotificationAction, // Config GetConfigAction, - UpdateConfigAction + UpdateConfigAction, } from './'; +import { + AddQueryAction, + DeleteQueryAction, + FetchQueriesAction, + UpdateQueryAction, +} from './config'; export enum ActionTypes { // Theme @@ -62,35 +68,43 @@ export enum ActionTypes { clearNotification = 'CLEAR_NOTIFICATION', // 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 - SetThemeAction | + | SetThemeAction // Apps - GetAppsAction | - PinAppAction | - AddAppAction | - DeleteAppAction | - UpdateAppAction | - ReorderAppsAction | - SortAppsAction | + | GetAppsAction + | PinAppAction + | AddAppAction + | DeleteAppAction + | UpdateAppAction + | ReorderAppsAction + | SortAppsAction // Categories - GetCategoriesAction | - AddCategoryAction | - PinCategoryAction | - DeleteCategoryAction | - UpdateCategoryAction | - SortCategoriesAction | - ReorderCategoriesAction | + | GetCategoriesAction + | AddCategoryAction + | PinCategoryAction + | DeleteCategoryAction + | UpdateCategoryAction + | SortCategoriesAction + | ReorderCategoriesAction // Bookmarks - AddBookmarkAction | - DeleteBookmarkAction | - UpdateBookmarkAction | + | AddBookmarkAction + | DeleteBookmarkAction + | UpdateBookmarkAction // Notifications - CreateNotificationAction | - ClearNotificationAction | + | CreateNotificationAction + | ClearNotificationAction // Config - GetConfigAction | - UpdateConfigAction; \ No newline at end of file + | GetConfigAction + | UpdateConfigAction + | FetchQueriesAction + | AddQueryAction + | DeleteQueryAction + | UpdateQueryAction; diff --git a/client/src/store/actions/config.ts b/client/src/store/actions/config.ts index a14e21e04ab2fde8286cd89cf9cca72cd466dc5b..29c5186b192e53f18ffce646c3a772732cfb0b5d 100644 --- a/client/src/store/actions/config.ts +++ b/client/src/store/actions/config.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { Dispatch } from 'redux'; import { ActionTypes } from './actionTypes'; -import { Config, ApiResponse } from '../../interfaces'; +import { Config, ApiResponse, Query } from '../../interfaces'; import { CreateNotificationAction } from './notification'; import { searchConfig } from '../../utility'; @@ -13,18 +13,18 @@ export interface GetConfigAction { export const getConfig = () => async (dispatch: Dispatch) => { try { const res = await axios.get>('/api/config'); - + dispatch({ type: ActionTypes.getConfig, - payload: res.data.data - }) + payload: res.data.data, + }); // Set custom page title if set document.title = searchConfig('customTitle', 'Flame'); } catch (err) { - console.log(err) + console.log(err); } -} +}; export interface UpdateConfigAction { type: ActionTypes.updateConfig; @@ -34,19 +34,102 @@ export interface UpdateConfigAction { export const updateConfig = (formData: any) => async (dispatch: Dispatch) => { try { const res = await axios.put>('/api/config', formData); + dispatch({ type: ActionTypes.createNotification, payload: { title: 'Success', - message: 'Settings updated' - } - }) + message: 'Settings updated', + }, + }); dispatch({ type: ActionTypes.updateConfig, - payload: res.data.data - }) + payload: res.data.data, + }); } catch (err) { console.log(err); } -} \ No newline at end of file +}; + +export interface FetchQueriesAction { + type: ActionTypes.fetchQueries; + payload: Query[]; +} + +export const fetchQueries = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/queries'); + + dispatch({ + 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) => { + try { + const res = await axios.post>('/api/queries', query); + + dispatch({ + 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) => { + try { + const res = await axios.delete>( + `/api/queries/${prefix}` + ); + + dispatch({ + 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) => { + try { + const res = await axios.put>( + `/api/queries/${oldPrefix}`, + query + ); + + dispatch({ + type: ActionTypes.updateQuery, + payload: res.data.data, + }); + } catch (err) { + console.log(err); + } + }; diff --git a/client/src/store/reducers/config.ts b/client/src/store/reducers/config.ts index 071f46133a95431cd80b8dad0dcf5674863de335..ae2699eca72281e37d777be168cb29bef9ee5d00 100644 --- a/client/src/store/reducers/config.ts +++ b/client/src/store/reducers/config.ts @@ -1,36 +1,78 @@ import { ActionTypes, Action } from '../actions'; -import { Config } from '../../interfaces'; +import { Config, Query } from '../../interfaces'; export interface State { loading: boolean; config: Config[]; + customQueries: Query[]; } const initialState: State = { loading: true, - config: [] -} + config: [], + customQueries: [], +}; const getConfig = (state: State, action: Action): State => { return { + ...state, loading: false, - config: action.payload - } -} + config: action.payload, + }; +}; const updateConfig = (state: State, action: Action): State => { return { ...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) => { - switch(action.type) { - case ActionTypes.getConfig: return getConfig(state, action); - case ActionTypes.updateConfig: return updateConfig(state, action); - default: return state; + switch (action.type) { + case ActionTypes.getConfig: + return getConfig(state, action); + 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; \ No newline at end of file +export default configReducer; diff --git a/client/src/utility/checkVersion.ts b/client/src/utility/checkVersion.ts index e1a050894c237ee04d3002cf7101b1bda5056979..d4cdb9a3beaa238d8b53ac04082a2ad38214eec1 100644 --- a/client/src/utility/checkVersion.ts +++ b/client/src/utility/checkVersion.ts @@ -4,24 +4,31 @@ import { createNotification } from '../store/actions'; export const checkVersion = async (isForced: boolean = false) => { try { - const res = await axios.get('https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env'); + const res = await axios.get( + 'https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env' + ); const githubVersion = res.data .split('\n') - .map(pair => pair.split('='))[0][1]; + .map((pair) => pair.split('='))[0][1]; if (githubVersion !== process.env.REACT_APP_VERSION) { - store.dispatch(createNotification({ - title: 'Info', - message: 'New version is available!' - })) + store.dispatch( + createNotification({ + title: 'Info', + message: 'New version is available!', + url: 'https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md', + }) + ); } else if (isForced) { - store.dispatch(createNotification({ - title: 'Info', - message: 'You are using the latest version!' - })) + store.dispatch( + createNotification({ + title: 'Info', + message: 'You are using the latest version!', + }) + ); } } catch (err) { console.log(err); } -} \ No newline at end of file +}; diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 99f8d698f02137feb558b8e1f9ca731a0e9d0a8e..caff9c3ac06fa705bec51d0fe7599b0dd689b646 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -3,4 +3,5 @@ export * from './urlParser'; export * from './searchConfig'; export * from './checkVersion'; export * from './sortData'; -export * from './searchParser'; \ No newline at end of file +export * from './searchParser'; +export * from './redirectUrl'; diff --git a/client/src/utility/redirectUrl.ts b/client/src/utility/redirectUrl.ts new file mode 100644 index 0000000000000000000000000000000000000000..81eca108de0d99a3013f071b24c3934099d35d6b --- /dev/null +++ b/client/src/utility/redirectUrl.ts @@ -0,0 +1,7 @@ +export const redirectUrl = (url: string, sameTab: boolean) => { + if (sameTab) { + document.location.replace(url); + } else { + window.open(url); + } +}; diff --git a/client/src/utility/searchParser.ts b/client/src/utility/searchParser.ts index c477d66e8888bd7963355aba06bc57dca7721e9c..2befdd2589812445ce0d73fb358f6a587b6d2d19 100644 --- a/client/src/utility/searchParser.ts +++ b/client/src/utility/searchParser.ts @@ -1,11 +1,12 @@ import { queries } from './searchQueries.json'; import { Query, SearchResult } from '../interfaces'; - +import { store } from '../store/store'; import { searchConfig } from '.'; export const searchParser = (searchQuery: string): SearchResult => { const result: SearchResult = { isLocal: false, + isURL: false, sameTab: false, search: '', 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 prefix = splitQuery @@ -25,8 +35,11 @@ export const searchParser = (searchQuery: string): SearchResult => { ? encodeURIComponent(splitQuery[2]) : 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) { result.query = query; result.search = search; diff --git a/controllers/apps.js b/controllers/apps.js index 4376d15de40b80a752f3052cda7de09928a57030..b976d45418211ad7eafb43f6c64e6f389e902824 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -131,7 +131,10 @@ exports.getApps = asyncWrapper(async (req, res, next) => { if (apps.some((app) => app.name === item.name)) { const app = apps.filter((e) => e.name === item.name)[0]; - if (item.icon === 'custom') { + if ( + item.icon === 'custom' || + (item.icon === 'docker' && app.icon != 'docker') + ) { await app.update({ name: item.name, url: item.url, diff --git a/controllers/config.js b/controllers/config.js index a9768d23e24a70e10dfbdd14b0085097f8863793..e5290aa203eb9709fdaf400991d4b06c7a5f29db 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -162,7 +162,7 @@ exports.getCss = asyncWrapper(async (req, res, next) => { // @access Public exports.updateCss = asyncWrapper(async (req, res, next) => { 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 fs.copyFileSync( diff --git a/controllers/queries/index.js b/controllers/queries/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ae1ccec0e9734c968838727e217cc8c8100054c8 --- /dev/null +++ b/controllers/queries/index.js @@ -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, + }); +}); diff --git a/package.json b/package.json index dbeb5ee54844e5c48f102ccf8c5a3c699dfd1b2f..dc8b92d57d6305e8a30cf690882c0cb292775943 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "main": "index.js", "scripts": { "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", - "dev-init": "npm run init-server && npm run init-client", - "dev-server": "nodemon server.js", + "dir-init": "npx mkdirp data public && touch public/flame.css public/customQueries.json", + "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": "concurrently \"npm run dev-server\" \"npm run dev-client\"", "skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\"" diff --git a/routes/config.js b/routes/config.js index eebf5dd7b1174d27b9c9d4e0bad8ab6c68fd7180..8c9ac150675776ebe2df130fb38c06018a05c98f 100644 --- a/routes/config.js +++ b/routes/config.js @@ -12,21 +12,10 @@ const { getCss, } = require('../controllers/config'); -router - .route('/') - .post(createPair) - .get(getAllPairs) - .put(updateValues); +router.route('/').post(createPair).get(getAllPairs).put(updateValues); -router - .route('/:key') - .get(getSinglePair) - .put(updateValue) - .delete(deletePair); +router.route('/:key').get(getSinglePair).put(updateValue).delete(deletePair); -router - .route('/0/css') - .get(getCss) - .put(updateCss); +router.route('/0/css').get(getCss).put(updateCss); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/routes/queries.js b/routes/queries.js new file mode 100644 index 0000000000000000000000000000000000000000..afacffd33bbfe4d4b5d39010873c1a464ff309d8 --- /dev/null +++ b/routes/queries.js @@ -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; diff --git a/server.js b/server.js index 8b09803e9be7f3dd6cf17e17217178cd373003b9..5c1d0fa33f9d61912004999b80fe2875dbf71354 100644 --- a/server.js +++ b/server.js @@ -1,23 +1,28 @@ require('dotenv').config(); const http = require('http'); + +// Database const { connectDB } = require('./db'); +const associateModels = require('./models/associateModels'); + +// Server const api = require('./api'); const jobs = require('./utils/jobs'); const Socket = require('./Socket'); const Sockets = require('./Sockets'); -const associateModels = require('./models/associateModels'); -const initConfig = require('./utils/initConfig'); -const findCss = require('./utils/findCss'); + +// Utils +const initApp = require('./utils/init'); const Logger = require('./utils/Logger'); const logger = new Logger(); -const PORT = process.env.PORT || 5005; - (async () => { + const PORT = process.env.PORT || 5005; + + // Init app await connectDB(); await associateModels(); - await initConfig(); - findCss(); + await initApp(); // Create server for Express API and WebSockets const server = http.createServer(); @@ -28,6 +33,8 @@ const PORT = process.env.PORT || 5005; Sockets.registerSocket('weather', weatherSocket); server.listen(PORT, () => { - logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); - }) -})(); \ No newline at end of file + logger.log( + `Server is running on port ${PORT} in ${process.env.NODE_ENV} mode` + ); + }); +})(); diff --git a/utils/File.js b/utils/File.js index 0b2fbdc357feb57adf187d48035f32ecea7291fe..f135da845f7dffb19bdb993e7d74129fdcf85c15 100644 --- a/utils/File.js +++ b/utils/File.js @@ -3,7 +3,7 @@ const fs = require('fs'); class File { constructor(path) { this.path = path; - this.content = ''; + this.content = null; } read() { @@ -16,10 +16,13 @@ class File { } } - write(data) { + write(data, isJSON) { this.content = data; - fs.writeFileSync(this.path, this.content); + fs.writeFileSync( + this.path, + isJSON ? JSON.stringify(this.content) : this.content + ); } } -module.exports = File; \ No newline at end of file +module.exports = File; diff --git a/utils/findCss.js b/utils/findCss.js deleted file mode 100644 index af85a0aadce25437b1ae18ad1b20ef2ea13a7067..0000000000000000000000000000000000000000 --- a/utils/findCss.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/utils/init/createFile.js b/utils/init/createFile.js new file mode 100644 index 0000000000000000000000000000000000000000..b46b4f6f3fbcc039cf20bc3f390d29685b455c9a --- /dev/null +++ b/utils/init/createFile.js @@ -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; diff --git a/utils/init/index.js b/utils/init/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a0e11a111eb314c6275da6de53917562c71e7591 --- /dev/null +++ b/utils/init/index.js @@ -0,0 +1,9 @@ +const initConfig = require('./initConfig'); +const initFiles = require('./initFiles'); + +const initApp = async () => { + await initConfig(); + await initFiles(); +}; + +module.exports = initApp; diff --git a/utils/initConfig.js b/utils/init/initConfig.js similarity index 78% rename from utils/initConfig.js rename to utils/init/initConfig.js index b6a5a18b6b24dd31af26c33846a41a28efa6591d..83ce4ea0852018dc893341ad609ce144095157d1 100644 --- a/utils/initConfig.js +++ b/utils/init/initConfig.js @@ -1,7 +1,8 @@ const { Op } = require('sequelize'); -const Config = require('../models/Config'); +const Config = require('../../models/Config'); const { config } = require('./initialConfig.json'); -const Logger = require('./Logger'); + +const Logger = require('../Logger'); const logger = new Logger(); 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; }; diff --git a/utils/init/initFiles.js b/utils/init/initFiles.js new file mode 100644 index 0000000000000000000000000000000000000000..cee54ca1de1609ff3734dbcc7335fdbcdbeba0bc --- /dev/null +++ b/utils/init/initFiles.js @@ -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; diff --git a/utils/initialConfig.json b/utils/init/initialConfig.json similarity index 100% rename from utils/initialConfig.json rename to utils/init/initialConfig.json diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json new file mode 100644 index 0000000000000000000000000000000000000000..42354d7df95a662527a18f94cbf09dab444d98f1 --- /dev/null +++ b/utils/init/initialFiles.json @@ -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 + } + ] +}