Compare commits

..

10 Commits
v1.0 ... v1.2

Author SHA1 Message Date
pawelmalak
91ab1c5ae4 Merge pull request #21 from pawelmalak/feature
Release v1.2
2021-06-10 13:21:51 +02:00
unknown
78de8752c6 Fixed bug with overwriting opened tabs. Added proxy for websocket 2021-06-10 13:05:55 +02:00
unknown
936da301b8 Clear weather data job. Fixed bug with displaying bookmark icons on mobile devices 2021-06-10 01:51:59 +02:00
unknown
80c807bfba Fixed typo in Dockerfile. Added some checks to weather module settings 2021-06-09 22:26:39 +02:00
unknown
4583ca00e9 Added ability to set icons on bookmarks. Added hover indicator for apps 2021-06-09 12:45:55 +02:00
unknown
8b87ad92f1 Added option to pin new apps/categories by default 2021-06-09 10:58:45 +02:00
pawelmalak
43e110d378 Merge pull request #10 from pawelmalak/dependabot/npm_and_yarn/ws-7.4.6
Bump ws from 7.4.5 to 7.4.6
2021-06-09 01:24:22 +02:00
dependabot[bot]
a8217e2632 Bump ws from 7.4.5 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.4.5 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.5...7.4.6)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-08 23:22:59 +00:00
pawelmalak
cf44f45fde Merge pull request #9 from pawelmalak/qol-improvements
Added favicon and changed page title. Changed message when there are …
2021-06-09 01:04:26 +02:00
unknown
30ed700521 Added favicon and changed page title. Changed message when there are apps/bookmarks created but they are not pinned to homescreen. Added functionality to set custom page title. 2021-06-09 01:01:32 +02:00
40 changed files with 423 additions and 210 deletions

View File

@@ -2,9 +2,9 @@ FROM node:14-alpine
WORKDIR /app
COPY package*.json .
COPY package*.json ./
RUN npm install --only=production
RUN npm install --production
COPY . .

View File

@@ -5,11 +5,11 @@ class Socket {
this.webSocketServer = new WebSocket.Server({ server })
this.webSocketServer.on('listening', () => {
console.log('socket listen');
console.log('Socket: listen');
})
this.webSocketServer.on('connection', (webSocketClient) => {
console.log('new connection');
console.log('Socket: new connection');
})
}

130
client/package-lock.json generated
View File

@@ -2304,6 +2304,14 @@
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA=="
},
"@types/http-proxy": {
"version": "1.17.6",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.6.tgz",
"integrity": "sha512-+qsjqR75S/ib0ig0R9WN+CDoZeOBU6F2XLewgC4KVgdXiNHiKKHFEMRHOrs5PbYE97D5vataw5wPj4KLYfUkuQ==",
"requires": {
"@types/node": "*"
}
},
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -7449,110 +7457,21 @@
}
},
"http-proxy-middleware": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
"integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz",
"integrity": "sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow==",
"requires": {
"http-proxy": "^1.17.0",
"is-glob": "^4.0.0",
"lodash": "^4.17.11",
"micromatch": "^3.1.10"
"@types/http-proxy": "^1.17.5",
"http-proxy": "^1.18.1",
"is-glob": "^4.0.1",
"is-plain-obj": "^3.0.0",
"micromatch": "^4.0.2"
},
"dependencies": {
"braces": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"requires": {
"arr-flatten": "^1.1.0",
"array-unique": "^0.3.2",
"extend-shallow": "^2.0.1",
"fill-range": "^4.0.0",
"isobject": "^3.0.1",
"repeat-element": "^1.1.2",
"snapdragon": "^0.8.1",
"snapdragon-node": "^2.0.1",
"split-string": "^3.0.2",
"to-regex": "^3.0.1"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"requires": {
"is-extendable": "^0.1.0"
}
}
}
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"requires": {
"extend-shallow": "^2.0.1",
"is-number": "^3.0.0",
"repeat-string": "^1.6.1",
"to-regex-range": "^2.1.0"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"requires": {
"is-extendable": "^0.1.0"
}
}
}
},
"is-number": {
"is-plain-obj": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"requires": {
"kind-of": "^3.0.2"
},
"dependencies": {
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"requires": {
"is-buffer": "^1.1.5"
}
}
}
},
"micromatch": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
"integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
"requires": {
"arr-diff": "^4.0.0",
"array-unique": "^0.3.2",
"braces": "^2.3.1",
"define-property": "^2.0.2",
"extend-shallow": "^3.0.2",
"extglob": "^2.0.4",
"fragment-cache": "^0.2.1",
"kind-of": "^6.0.2",
"nanomatch": "^1.2.9",
"object.pick": "^1.3.0",
"regex-not": "^1.0.0",
"snapdragon": "^0.8.1",
"to-regex": "^3.0.2"
}
},
"to-regex-range": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
"integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
"requires": {
"is-number": "^3.0.0",
"repeat-string": "^1.6.1"
}
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
}
}
},
@@ -16033,6 +15952,17 @@
}
}
},
"http-proxy-middleware": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz",
"integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==",
"requires": {
"http-proxy": "^1.17.0",
"is-glob": "^4.0.0",
"lodash": "^4.17.11",
"micromatch": "^3.1.10"
}
},
"import-local": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz",

View File

@@ -15,6 +15,7 @@
"@types/react-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1",
"http-proxy-middleware": "^2.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
@@ -50,6 +51,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:5005"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -10,36 +10,15 @@
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet">
<title>React App</title>
<title>Flame</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -18,6 +18,10 @@ if (localStorage.theme) {
store.dispatch<any>(setTheme(localStorage.theme));
}
if (localStorage.customTitle) {
document.title = localStorage.customTitle;
}
const App = (): JSX.Element => {
return (
<Provider store={store}>

View File

@@ -27,4 +27,16 @@
font-weight: 400;
font-size: 0.8em;
opacity: 1;
}
@media (min-width: 500px) {
.AppCard {
padding: 2px;
border-radius: 4px;
transition: all 0.10s;
}
.AppCard:hover {
background-color: rgba(0,0,0,0.2);
}
}

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router-dom';
import classes from './AppCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser } from '../../../utility/iconParser';
import { App } from '../../../interfaces';
@@ -11,22 +12,12 @@ interface ComponentProps {
}
const AppCard = (props: ComponentProps): JSX.Element => {
const iconParser = (mdiName: string): string => {
let parsedName = mdiName
.split('-')
.map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
.join('');
parsedName = `mdi${parsedName}`;
return parsedName;
}
const redirectHandler = (url: string): void => {
window.open(url);
}
return (
<a href={`http://${props.app.url}`} target='blank' className={classes.AppCard}>
<a href={`http://${props.app.url}`} target='_blank' className={classes.AppCard}>
<div className={classes.AppCardIcon}>
<Icon icon={iconParser(props.app.icon)} />
</div>

View File

@@ -98,7 +98,7 @@ const AppForm = (props: ComponentProps): JSX.Element => {
value={formData.url}
onChange={(e) => inputChangeHandler(e)}
/>
<span>Use URL without protocol</span>
<span>Only urls without http[s]:// are supported</span>
</InputGroup>
<InputGroup>
<label htmlFor='icon'>App Icon</label>

View File

@@ -6,6 +6,7 @@ import AppCard from '../AppCard/AppCard';
interface ComponentProps {
apps: App[];
totalApps?: number;
}
const AppGrid = (props: ComponentProps): JSX.Element => {
@@ -23,9 +24,15 @@ const AppGrid = (props: ComponentProps): JSX.Element => {
</div>
)
} else {
apps = (
<p className={classes.AppsMessage}>You don't have any applications. You can add a new one from <Link to='/applications'>/application</Link> menu</p>
);
if (props.totalApps) {
apps = (
<p className={classes.AppsMessage}>There are no pinned applications. You can pin them from the <Link to='/applications'>/applications</Link> menu</p>
);
} else {
apps = (
<p className={classes.AppsMessage}>You don't have any applications. You can add a new one from <Link to='/applications'>/applications</Link> menu</p>
);
}
}
return apps;

View File

@@ -18,9 +18,18 @@
.Bookmarks a {
line-height: 2;
transition: all 0.25s;
display: flex;
}
.BookmarkCard a:hover {
text-decoration: underline;
padding-left: 10px;
}
.BookmarkIcon {
width: 20px;
height: 20px;
display: flex;
margin-top: 3px;
margin-right: 2px;
}

View File

@@ -1,6 +1,9 @@
import { Bookmark, Category } from '../../../interfaces';
import classes from './BookmarkCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser } from '../../../utility/iconParser';
interface ComponentProps {
category: Category;
}
@@ -13,8 +16,13 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
{props.category.bookmarks.map((bookmark: Bookmark) => (
<a
href={`http://${bookmark.url}`}
target='blank'
target='_blank'
key={`bookmark-${bookmark.id}`}>
{bookmark.icon && (
<div className={classes.BookmarkIcon}>
<Icon icon={iconParser(bookmark.icon)} />
</div>
)}
{bookmark.name}
</a>
))}

View File

@@ -29,9 +29,11 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const [formData, setFormData] = useState<NewBookmark>({
name: '',
url: '',
categoryId: -1
categoryId: -1,
icon: ''
})
// Load category data if provided for editing
useEffect(() => {
if (props.category) {
setCategoryName({ name: props.category.name });
@@ -40,18 +42,21 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
}
}, [props.category])
// Load bookmark data if provided for editing
useEffect(() => {
if (props.bookmark) {
setFormData({
name: props.bookmark.name,
url: props.bookmark.url,
categoryId: props.bookmark.categoryId
categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon
})
} else {
setFormData({
name: '',
url: '',
categoryId: -1
categoryId: -1,
icon: ''
})
}
}, [props.bookmark])
@@ -79,7 +84,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
setFormData({
name: '',
url: '',
categoryId: formData.categoryId
categoryId: formData.categoryId,
icon: ''
})
}
} else {
@@ -94,7 +100,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
setFormData({
name: '',
url: '',
categoryId: -1
categoryId: -1,
icon: ''
})
}
@@ -177,6 +184,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
value={formData.url}
onChange={(e) => inputChangeHandler(e)}
/>
<span>Only urls without http[s]:// are supported</span>
</InputGroup>
<InputGroup>
<label htmlFor='categoryId'>Bookmark Category</label>
@@ -200,6 +208,25 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
})}
</select>
</InputGroup>
<InputGroup>
<label htmlFor='icon'>Bookmark Icon (optional)</label>
<input
type='text'
name='icon'
id='icon'
placeholder='book-open-outline'
value={formData.icon}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Use icon name from MDI.
<a
href='https://materialdesignicons.com/'
target='blank'>
{' '}Click here for reference
</a>
</span>
</InputGroup>
</Fragment>
)
}

View File

@@ -8,6 +8,7 @@ import BookmarkCard from '../BookmarkCard/BookmarkCard';
interface ComponentProps {
categories: Category[];
totalCategories?: number;
}
const BookmarkGrid = (props: ComponentProps): JSX.Element => {
@@ -20,9 +21,15 @@ const BookmarkGrid = (props: ComponentProps): JSX.Element => {
</div>
);
} else {
bookmarks = (
<p className={classes.BookmarksMessage}>You don't have any bookmarks. You can add a new one from <Link to='/bookmarks'>/bookmarks</Link> menu</p>
);
if (props.totalCategories) {
bookmarks = (
<p className={classes.BookmarksMessage}>There are no pinned categories. You can pin them from the <Link to='/bookmarks'>/bookmarks</Link> menu</p>
);
} else {
bookmarks = (
<p className={classes.BookmarksMessage}>You don't have any bookmarks. You can add a new one from <Link to='/bookmarks'>/bookmarks</Link> menu</p>
);
}
}
return bookmarks;

View File

@@ -96,6 +96,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
<Table headers={[
'Name',
'URL',
'Icon',
'Category',
'Actions'
]}>
@@ -104,6 +105,7 @@ const BookmarkTable = (props: ComponentProps): JSX.Element => {
<tr key={bookmark.bookmark.id}>
<td>{bookmark.bookmark.name}</td>
<td>{bookmark.bookmark.url}</td>
<td>{bookmark.bookmark.icon}</td>
<td>{bookmark.categoryName}</td>
<td className={classes.TableActions}>
<div

View File

@@ -45,6 +45,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
name: '',
url: '',
categoryId: -1,
icon: '',
id: -1,
createdAt: new Date(),
updatedAt: new Date()

View File

@@ -81,7 +81,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<SectionHeadline title='Applications' link='/applications' />
{props.appsLoading
? <Spinner />
: <AppGrid apps={props.apps.filter((app: App) => app.isPinned)} />
: <AppGrid
apps={props.apps.filter((app: App) => app.isPinned)}
totalApps={props.apps.length}
/>
}
<div className={classes.HomeSpace}></div>
@@ -89,7 +92,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<SectionHeadline title='Bookmarks' link='/bookmarks' />
{props.categoriesLoading
? <Spinner />
: <BookmarkGrid categories={props.categories.filter((category: Category) => category.isPinned)} />
: <BookmarkGrid
categories={props.categories.filter((category: Category) => category.isPinned)}
totalCategories={props.categories.length}
/>
}
<Link to='/settings' className={classes.SettingsButton}>

View File

@@ -0,0 +1,122 @@
import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
import axios from 'axios';
import { connect } from 'react-redux';
import InputGroup from '../../UI/Forms/InputGroup/InputGroup';
import Button from '../../UI/Buttons/Button/Button';
import { createNotification } from '../../../store/actions';
import { ApiResponse, Config, NewNotification } from '../../../interfaces';
interface FormState {
customTitle: string;
pinAppsByDefault: number;
pinCategoriesByDefault: number;
}
interface ComponentProps {
createNotification: (notification: NewNotification) => void;
}
const OtherSettings = (props: ComponentProps): JSX.Element => {
const [formData, setFormData] = useState<FormState>({
customTitle: document.title,
pinAppsByDefault: 0,
pinCategoriesByDefault: 0
})
// get initial config
useEffect(() => {
axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault')
.then(data => {
let tmpFormData = { ...formData };
data.data.data.forEach((config: Config) => {
let value: string | number = config.value;
if (config.valueType === 'number') {
value = parseFloat(value);
}
tmpFormData = {
...tmpFormData,
[config.key]: value
}
})
setFormData(tmpFormData);
})
.catch(err => console.log(err));
}, [])
const formSubmitHandler = (e: FormEvent) => {
e.preventDefault();
axios.put<ApiResponse<{}>>('/api/config', formData)
.then(() => {
props.createNotification({
title: 'Success',
message: 'Settings updated'
})
})
.catch((err) => console.log(err));
// update local page title
localStorage.setItem('customTitle', formData.customTitle);
document.title = formData.customTitle;
}
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 (
<form onSubmit={(e) => formSubmitHandler(e)}>
<InputGroup>
<label htmlFor='customTitle'>Custom Page Title</label>
<input
type='text'
id='customTitle'
name='customTitle'
placeholder='Flame'
value={formData.customTitle}
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
<InputGroup>
<label htmlFor='pinAppsByDefault'>Pin new applications by default</label>
<select
id='pinAppsByDefault'
name='pinAppsByDefault'
value={formData.pinAppsByDefault}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<InputGroup>
<label htmlFor='pinCategoriesByDefault'>Pin new categories by default</label>
<select
id='pinCategoriesByDefault'
name='pinCategoriesByDefault'
value={formData.pinCategoriesByDefault}
onChange={(e) => inputChangeHandler(e, true)}
>
<option value={1}>True</option>
<option value={0}>False</option>
</select>
</InputGroup>
<Button>Save changes</Button>
</form>
)
}
export default connect(null, { createNotification })(OtherSettings);

View File

@@ -6,6 +6,7 @@ import { Container } from '../UI/Layout/Layout';
import Headline from '../UI/Headlines/Headline/Headline';
import Themer from '../Themer/Themer';
import WeatherSettings from './WeatherSettings/WeatherSettings';
import OtherSettings from './OtherSettings/OtherSettings';
const Settings = (): JSX.Element => {
return (
@@ -30,11 +31,19 @@ const Settings = (): JSX.Element => {
to='/settings/weather'>
Weather
</NavLink>
<NavLink
className={classes.SettingsNavLink}
activeClassName={classes.SettingsNavLinkActive}
exact
to='/settings/other'>
Other
</NavLink>
</nav>
<section className={classes.SettingsContent}>
<Switch>
<Route exact path='/settings' component={Themer} />
<Route path='/settings/weather' component={WeatherSettings} />
<Route path='/settings/other' component={OtherSettings} />
</Switch>
</section>
</div>

View File

@@ -64,6 +64,15 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
const formSubmitHandler = (e: FormEvent) => {
e.preventDefault();
// Check for api key input
if ((formData.lat || formData.long) && !formData.WEATHER_API_KEY) {
props.createNotification({
title: 'Warning',
message: 'API Key is missing. Weather Module will NOT work'
})
}
// Save settings
axios.put<ApiResponse<{}>>('/api/config', formData)
.then(() => {
props.createNotification({
@@ -111,6 +120,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
target='blank'>
{' '}Weather API
</a>
. Key is required for weather module to work.
</span>
</InputGroup>
<InputGroup>

View File

@@ -12,8 +12,8 @@ interface ComponentProps {
const WeatherIcon = (props: ComponentProps): JSX.Element => {
const icon = props.isDay
? (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.day)
: (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.night);
? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day)
: new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.night);
useEffect(() => {
const delay = setTimeout(() => {
@@ -25,7 +25,7 @@ const WeatherIcon = (props: ComponentProps): JSX.Element => {
return () => {
clearTimeout(delay);
}
}, [props.weatherStatusCode]);
}, [props.weatherStatusCode, icon, props.theme.colors.accent]);
return <canvas id={`weather-icon`} width='50' height='50'></canvas>
}

View File

@@ -50,7 +50,11 @@ const WeatherWidget = (): JSX.Element => {
// Open socket for data updates
useEffect(() => {
const webSocketClient = new WebSocket('ws://localhost:5005');
const webSocketClient = new WebSocket(`ws://${window.location.host}/socket`);
webSocketClient.onopen = () => {
console.log('Socket: listen')
}
webSocketClient.onmessage = (e) => {
const data = JSON.parse(e.data);

View File

@@ -4,10 +4,12 @@ export interface Bookmark extends Model {
name: string;
url: string;
categoryId: number;
icon: string;
}
export interface NewBookmark {
name: string;
url: string;
categoryId: number;
icon: string;
}

15
client/src/setupProxy.js Normal file
View File

@@ -0,0 +1,15 @@
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
const apiProxy = createProxyMiddleware('/api', {
target: 'http://localhost:5005'
})
const wsProxy = createProxyMiddleware('/socket', {
target: 'http://localhost:5005',
ws: true
})
app.use(apiProxy);
app.use(wsProxy);
};

View File

@@ -23,10 +23,7 @@ export const getApps = () => async (dispatch: Dispatch) => {
payload: res.data.data
})
} catch (err) {
dispatch<GetAppsAction<string>>({
type: ActionTypes.getAppsError,
payload: err.data.data
})
console.log(err);
}
}

View File

@@ -0,0 +1,9 @@
export const iconParser = (mdiName: string): string => {
let parsedName = mdiName
.split('-')
.map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
.join('');
parsedName = `mdi${parsedName}`;
return parsedName;
}

View File

@@ -1,12 +1,29 @@
const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const App = require('../models/App');
const Config = require('../models/Config');
// @desc Create new app
// @route POST /api/apps
// @access Public
exports.createApp = asyncWrapper(async (req, res, next) => {
const app = await App.create(req.body);
// Get config from database
const pinApps = await Config.findOne({
where: { key: 'pinAppsByDefault' }
});
let app;
if (pinApps) {
if (parseInt(pinApps.value)) {
app = await App.create({
...req.body,
isPinned: true
})
} else {
app = await App.create(req.body);
}
}
res.status(201).json({
success: true,

View File

@@ -2,12 +2,29 @@ const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const Category = require('../models/Category');
const Bookmark = require('../models/Bookmark');
const Config = require('../models/Config');
// @desc Create new category
// @route POST /api/categories
// @access Public
exports.createCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.create(req.body);
// Get config from database
const pinCategories = await Config.findOne({
where: { key: 'pinCategoriesByDefault' }
});
let category;
if (pinCategories) {
if (parseInt(pinCategories.value)) {
category = await Category.create({
...req.body,
isPinned: true
})
} else {
category = await Category.create(req.body);
}
}
res.status(201).json({
success: true,

7
db.js
View File

@@ -8,13 +8,10 @@ const sequelize = new Sequelize({
const connectDB = async () => {
try {
await sequelize.authenticate({ logging: false });
await sequelize.authenticate();
console.log('Connected to database');
await sequelize.sync({
// alter: true,
logging: false
});
await sequelize.sync({ alter: true });
console.log('All models were synced');
} catch (error) {
console.error('Unable to connect to the database:', error);

View File

@@ -13,6 +13,10 @@ const Bookmark = sequelize.define('Bookmark', {
categoryId: {
type: DataTypes.INTEGER,
allowNull: false
},
icon: {
type: DataTypes.STRING,
defaultValue: ''
}
}, {
tableName: 'bookmarks'

6
package-lock.json generated
View File

@@ -2794,9 +2794,9 @@
}
},
"ws": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g=="
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
},
"xdg-basedir": {
"version": "4.0.0",

View File

@@ -24,7 +24,7 @@
"node-schedule": "^2.0.0",
"sequelize": "^6.6.2",
"sqlite3": "^5.0.2",
"ws": "^7.4.5"
"ws": "^7.4.6"
},
"devDependencies": {
"nodemon": "^2.0.7"

22
utils/clearWeatherData.js Normal file
View File

@@ -0,0 +1,22 @@
const { Op } = require('sequelize');
const Weather = require('../models/Weather');
const clearWeatherData = async () => {
const weather = await Weather.findOne({
order: [[ 'createdAt', 'DESC' ]]
});
if (weather) {
await Weather.destroy({
where: {
id: {
[Op.lt]: weather.id
}
}
})
}
console.log('Old weather data was deleted');
}
module.exports = clearWeatherData;

View File

@@ -1,16 +1,13 @@
const { Op } = require('sequelize');
const Config = require('../models/Config');
const { config } = require('./initialConfig.json');
const initConfig = async () => {
// Config keys
const keys = ['WEATHER_API_KEY', 'lat', 'long', 'isCelsius'];
const values = ['', 0, 0, true];
// Get config values
const configPairs = await Config.findAll({
where: {
key: {
[Op.or]: keys
[Op.or]: config.map(pair => pair.key)
}
}
})
@@ -19,12 +16,12 @@ const initConfig = async () => {
const configKeys = configPairs.map((pair) => pair.key);
// Create missing pairs
keys.forEach(async (key, idx) => {
config.forEach(async ({ key, value}) => {
if (!configKeys.includes(key)) {
await Config.create({
key,
value: values[idx],
valueType: typeof values[idx]
value,
valueType: typeof value
})
}
})

32
utils/initialConfig.json Normal file
View File

@@ -0,0 +1,32 @@
{
"config": [
{
"key": "WEATHER_API_KEY",
"value": ""
},
{
"key": "lat",
"value": 0
},
{
"key": "long",
"value": 0
},
{
"key": "isCelsius",
"value": true
},
{
"key": "customTitle",
"value": "Flame"
},
{
"key": "pinAppsByDefault",
"value": true
},
{
"key": "pinCategoriesByDefault",
"value": true
}
]
}

View File

@@ -1,5 +1,6 @@
const schedule = require('node-schedule');
const getExternalWeather = require('./getExternalWeather');
const clearWeatherData = require('./clearWeatherData');
const Sockets = require('../Sockets');
// Update weather data every 15 minutes
@@ -14,6 +15,6 @@ const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async
})
// Clear old weather data every 4 hours
const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 0 */4 * * *', async () => {
console.log('clean')
const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 5 */4 * * *', async () => {
clearWeatherData();
})