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 WORKDIR /app
COPY package*.json . COPY package*.json ./
RUN npm install --only=production RUN npm install --production
COPY . . COPY . .

View File

@@ -5,11 +5,11 @@ class Socket {
this.webSocketServer = new WebSocket.Server({ server }) this.webSocketServer = new WebSocket.Server({ server })
this.webSocketServer.on('listening', () => { this.webSocketServer.on('listening', () => {
console.log('socket listen'); console.log('Socket: listen');
}) })
this.webSocketServer.on('connection', (webSocketClient) => { 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", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz",
"integrity": "sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==" "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": { "@types/istanbul-lib-coverage": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@@ -7449,110 +7457,21 @@
} }
}, },
"http-proxy-middleware": { "http-proxy-middleware": {
"version": "0.19.1", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz",
"integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", "integrity": "sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow==",
"requires": { "requires": {
"http-proxy": "^1.17.0", "@types/http-proxy": "^1.17.5",
"is-glob": "^4.0.0", "http-proxy": "^1.18.1",
"lodash": "^4.17.11", "is-glob": "^4.0.1",
"micromatch": "^3.1.10" "is-plain-obj": "^3.0.0",
"micromatch": "^4.0.2"
}, },
"dependencies": { "dependencies": {
"braces": { "is-plain-obj": {
"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": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
"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"
}
} }
} }
}, },
@@ -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": { "import-local": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", "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-redux": "^7.1.16",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1", "axios": "^0.21.1",
"http-proxy-middleware": "^2.0.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
@@ -50,6 +51,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari 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" content="Web site created using create-react-app"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <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" /> <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 rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700,900" rel="stylesheet">
<title>React App</title> <title>Flame</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!--
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> </body>
</html> </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)); store.dispatch<any>(setTheme(localStorage.theme));
} }
if (localStorage.customTitle) {
document.title = localStorage.customTitle;
}
const App = (): JSX.Element => { const App = (): JSX.Element => {
return ( return (
<Provider store={store}> <Provider store={store}>

View File

@@ -27,4 +27,16 @@
font-weight: 400; font-weight: 400;
font-size: 0.8em; font-size: 0.8em;
opacity: 1; 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 classes from './AppCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon'; import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser } from '../../../utility/iconParser';
import { App } from '../../../interfaces'; import { App } from '../../../interfaces';
@@ -11,22 +12,12 @@ interface ComponentProps {
} }
const AppCard = (props: ComponentProps): JSX.Element => { 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 => { const redirectHandler = (url: string): void => {
window.open(url); window.open(url);
} }
return ( 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}> <div className={classes.AppCardIcon}>
<Icon icon={iconParser(props.app.icon)} /> <Icon icon={iconParser(props.app.icon)} />
</div> </div>

View File

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

View File

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

View File

@@ -18,9 +18,18 @@
.Bookmarks a { .Bookmarks a {
line-height: 2; line-height: 2;
transition: all 0.25s; transition: all 0.25s;
display: flex;
} }
.BookmarkCard a:hover { .BookmarkCard a:hover {
text-decoration: underline; text-decoration: underline;
padding-left: 10px; 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 { Bookmark, Category } from '../../../interfaces';
import classes from './BookmarkCard.module.css'; import classes from './BookmarkCard.module.css';
import Icon from '../../UI/Icons/Icon/Icon';
import { iconParser } from '../../../utility/iconParser';
interface ComponentProps { interface ComponentProps {
category: Category; category: Category;
} }
@@ -13,8 +16,13 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => {
{props.category.bookmarks.map((bookmark: Bookmark) => ( {props.category.bookmarks.map((bookmark: Bookmark) => (
<a <a
href={`http://${bookmark.url}`} href={`http://${bookmark.url}`}
target='blank' target='_blank'
key={`bookmark-${bookmark.id}`}> key={`bookmark-${bookmark.id}`}>
{bookmark.icon && (
<div className={classes.BookmarkIcon}>
<Icon icon={iconParser(bookmark.icon)} />
</div>
)}
{bookmark.name} {bookmark.name}
</a> </a>
))} ))}

View File

@@ -29,9 +29,11 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
const [formData, setFormData] = useState<NewBookmark>({ const [formData, setFormData] = useState<NewBookmark>({
name: '', name: '',
url: '', url: '',
categoryId: -1 categoryId: -1,
icon: ''
}) })
// Load category data if provided for editing
useEffect(() => { useEffect(() => {
if (props.category) { if (props.category) {
setCategoryName({ name: props.category.name }); setCategoryName({ name: props.category.name });
@@ -40,18 +42,21 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
} }
}, [props.category]) }, [props.category])
// Load bookmark data if provided for editing
useEffect(() => { useEffect(() => {
if (props.bookmark) { if (props.bookmark) {
setFormData({ setFormData({
name: props.bookmark.name, name: props.bookmark.name,
url: props.bookmark.url, url: props.bookmark.url,
categoryId: props.bookmark.categoryId categoryId: props.bookmark.categoryId,
icon: props.bookmark.icon
}) })
} else { } else {
setFormData({ setFormData({
name: '', name: '',
url: '', url: '',
categoryId: -1 categoryId: -1,
icon: ''
}) })
} }
}, [props.bookmark]) }, [props.bookmark])
@@ -79,7 +84,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
setFormData({ setFormData({
name: '', name: '',
url: '', url: '',
categoryId: formData.categoryId categoryId: formData.categoryId,
icon: ''
}) })
} }
} else { } else {
@@ -94,7 +100,8 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
setFormData({ setFormData({
name: '', name: '',
url: '', url: '',
categoryId: -1 categoryId: -1,
icon: ''
}) })
} }
@@ -177,6 +184,7 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
value={formData.url} value={formData.url}
onChange={(e) => inputChangeHandler(e)} onChange={(e) => inputChangeHandler(e)}
/> />
<span>Only urls without http[s]:// are supported</span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>
<label htmlFor='categoryId'>Bookmark Category</label> <label htmlFor='categoryId'>Bookmark Category</label>
@@ -200,6 +208,25 @@ const BookmarkForm = (props: ComponentProps): JSX.Element => {
})} })}
</select> </select>
</InputGroup> </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> </Fragment>
) )
} }

View File

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

View File

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

View File

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

View File

@@ -81,7 +81,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<SectionHeadline title='Applications' link='/applications' /> <SectionHeadline title='Applications' link='/applications' />
{props.appsLoading {props.appsLoading
? <Spinner /> ? <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> <div className={classes.HomeSpace}></div>
@@ -89,7 +92,10 @@ const Home = (props: ComponentProps): JSX.Element => {
<SectionHeadline title='Bookmarks' link='/bookmarks' /> <SectionHeadline title='Bookmarks' link='/bookmarks' />
{props.categoriesLoading {props.categoriesLoading
? <Spinner /> ? <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}> <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 Headline from '../UI/Headlines/Headline/Headline';
import Themer from '../Themer/Themer'; import Themer from '../Themer/Themer';
import WeatherSettings from './WeatherSettings/WeatherSettings'; import WeatherSettings from './WeatherSettings/WeatherSettings';
import OtherSettings from './OtherSettings/OtherSettings';
const Settings = (): JSX.Element => { const Settings = (): JSX.Element => {
return ( return (
@@ -30,11 +31,19 @@ const Settings = (): JSX.Element => {
to='/settings/weather'> to='/settings/weather'>
Weather Weather
</NavLink> </NavLink>
<NavLink
className={classes.SettingsNavLink}
activeClassName={classes.SettingsNavLinkActive}
exact
to='/settings/other'>
Other
</NavLink>
</nav> </nav>
<section className={classes.SettingsContent}> <section className={classes.SettingsContent}>
<Switch> <Switch>
<Route exact path='/settings' component={Themer} /> <Route exact path='/settings' component={Themer} />
<Route path='/settings/weather' component={WeatherSettings} /> <Route path='/settings/weather' component={WeatherSettings} />
<Route path='/settings/other' component={OtherSettings} />
</Switch> </Switch>
</section> </section>
</div> </div>

View File

@@ -64,6 +64,15 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
const formSubmitHandler = (e: FormEvent) => { const formSubmitHandler = (e: FormEvent) => {
e.preventDefault(); 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) axios.put<ApiResponse<{}>>('/api/config', formData)
.then(() => { .then(() => {
props.createNotification({ props.createNotification({
@@ -111,6 +120,7 @@ const WeatherSettings = (props: ComponentProps): JSX.Element => {
target='blank'> target='blank'>
{' '}Weather API {' '}Weather API
</a> </a>
. Key is required for weather module to work.
</span> </span>
</InputGroup> </InputGroup>
<InputGroup> <InputGroup>

View File

@@ -12,8 +12,8 @@ interface ComponentProps {
const WeatherIcon = (props: ComponentProps): JSX.Element => { const WeatherIcon = (props: ComponentProps): JSX.Element => {
const icon = props.isDay const icon = props.isDay
? (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.day) ? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day)
: (new IconMapping).mapIcon(props.weatherStatusCode, TimeOfDay.night); : new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.night);
useEffect(() => { useEffect(() => {
const delay = setTimeout(() => { const delay = setTimeout(() => {
@@ -25,7 +25,7 @@ const WeatherIcon = (props: ComponentProps): JSX.Element => {
return () => { return () => {
clearTimeout(delay); clearTimeout(delay);
} }
}, [props.weatherStatusCode]); }, [props.weatherStatusCode, icon, props.theme.colors.accent]);
return <canvas id={`weather-icon`} width='50' height='50'></canvas> 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 // Open socket for data updates
useEffect(() => { 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) => { webSocketClient.onmessage = (e) => {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);

View File

@@ -4,10 +4,12 @@ export interface Bookmark extends Model {
name: string; name: string;
url: string; url: string;
categoryId: number; categoryId: number;
icon: string;
} }
export interface NewBookmark { export interface NewBookmark {
name: string; name: string;
url: string; url: string;
categoryId: number; 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 payload: res.data.data
}) })
} catch (err) { } catch (err) {
dispatch<GetAppsAction<string>>({ console.log(err);
type: ActionTypes.getAppsError,
payload: err.data.data
})
} }
} }

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 asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse'); const ErrorResponse = require('../utils/ErrorResponse');
const App = require('../models/App'); const App = require('../models/App');
const Config = require('../models/Config');
// @desc Create new app // @desc Create new app
// @route POST /api/apps // @route POST /api/apps
// @access Public // @access Public
exports.createApp = asyncWrapper(async (req, res, next) => { 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({ res.status(201).json({
success: true, success: true,

View File

@@ -2,12 +2,29 @@ const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse'); const ErrorResponse = require('../utils/ErrorResponse');
const Category = require('../models/Category'); const Category = require('../models/Category');
const Bookmark = require('../models/Bookmark'); const Bookmark = require('../models/Bookmark');
const Config = require('../models/Config');
// @desc Create new category // @desc Create new category
// @route POST /api/categories // @route POST /api/categories
// @access Public // @access Public
exports.createCategory = asyncWrapper(async (req, res, next) => { 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({ res.status(201).json({
success: true, success: true,

7
db.js
View File

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

View File

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

6
package-lock.json generated
View File

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

View File

@@ -24,7 +24,7 @@
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
"sequelize": "^6.6.2", "sequelize": "^6.6.2",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"ws": "^7.4.5" "ws": "^7.4.6"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^2.0.7" "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 { Op } = require('sequelize');
const Config = require('../models/Config'); const Config = require('../models/Config');
const { config } = require('./initialConfig.json');
const initConfig = async () => { const initConfig = async () => {
// Config keys
const keys = ['WEATHER_API_KEY', 'lat', 'long', 'isCelsius'];
const values = ['', 0, 0, true];
// Get config values // Get config values
const configPairs = await Config.findAll({ const configPairs = await Config.findAll({
where: { where: {
key: { 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); const configKeys = configPairs.map((pair) => pair.key);
// Create missing pairs // Create missing pairs
keys.forEach(async (key, idx) => { config.forEach(async ({ key, value}) => {
if (!configKeys.includes(key)) { if (!configKeys.includes(key)) {
await Config.create({ await Config.create({
key, key,
value: values[idx], value,
valueType: typeof values[idx] 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 schedule = require('node-schedule');
const getExternalWeather = require('./getExternalWeather'); const getExternalWeather = require('./getExternalWeather');
const clearWeatherData = require('./clearWeatherData');
const Sockets = require('../Sockets'); const Sockets = require('../Sockets');
// Update weather data every 15 minutes // 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 // Clear old weather data every 4 hours
const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 0 */4 * * *', async () => { const weatherCleanerJob = schedule.scheduleJob('clearWeather', '0 5 */4 * * *', async () => {
console.log('clean') clearWeatherData();
}) })