mirror of
https://github.com/pawelmalak/flame.git
synced 2025-07-18 19:19:36 +02:00
commit
91ab1c5ae4
27 changed files with 289 additions and 152 deletions
|
@ -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 . .
|
||||||
|
|
||||||
|
|
|
@ -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
130
client/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -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: ''
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { ApiResponse, Config, NewNotification } from '../../../interfaces';
|
||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
customTitle: string;
|
customTitle: string;
|
||||||
|
pinAppsByDefault: number;
|
||||||
|
pinCategoriesByDefault: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ComponentProps {
|
interface ComponentProps {
|
||||||
|
@ -17,12 +19,14 @@ interface ComponentProps {
|
||||||
|
|
||||||
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
const [formData, setFormData] = useState<FormState>({
|
const [formData, setFormData] = useState<FormState>({
|
||||||
customTitle: document.title
|
customTitle: document.title,
|
||||||
|
pinAppsByDefault: 0,
|
||||||
|
pinCategoriesByDefault: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// get initial config
|
// get initial config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle')
|
axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
let tmpFormData = { ...formData };
|
let tmpFormData = { ...formData };
|
||||||
|
|
||||||
|
@ -60,10 +64,16 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
document.title = formData.customTitle;
|
document.title = formData.customTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
const inputChangeHandler = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, isNumber?: boolean) => {
|
||||||
|
let value: string | number = e.target.value;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
value = parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
[e.target.name]: e.target.value
|
[e.target.name]: value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +90,30 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||||
onChange={(e) => inputChangeHandler(e)}
|
onChange={(e) => inputChangeHandler(e)}
|
||||||
/>
|
/>
|
||||||
</InputGroup>
|
</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>
|
<Button>Save changes</Button>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
15
client/src/setupProxy.js
Normal 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);
|
||||||
|
};
|
|
@ -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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
9
client/src/utility/iconParser.ts
Normal file
9
client/src/utility/iconParser.ts
Normal 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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
7
db.js
|
@ -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);
|
||||||
|
|
|
@ -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'
|
||||||
|
|
22
utils/clearWeatherData.js
Normal file
22
utils/clearWeatherData.js
Normal 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;
|
|
@ -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', 'customTitle'];
|
|
||||||
const values = ['', 0, 0, true, 'Flame'];
|
|
||||||
|
|
||||||
// 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
32
utils/initialConfig.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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();
|
||||||
})
|
})
|
Loading…
Add table
Add a link
Reference in a new issue