commit
91ab1c5ae4
27 changed files with 289 additions and 152 deletions
|
@ -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 . .
|
||||
|
||||
|
|
|
@ -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
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",
|
||||
"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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -201,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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,6 +45,7 @@ const Bookmarks = (props: ComponentProps): JSX.Element => {
|
|||
name: '',
|
||||
url: '',
|
||||
categoryId: -1,
|
||||
icon: '',
|
||||
id: -1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
|
|
|
@ -9,6 +9,8 @@ import { ApiResponse, Config, NewNotification } from '../../../interfaces';
|
|||
|
||||
interface FormState {
|
||||
customTitle: string;
|
||||
pinAppsByDefault: number;
|
||||
pinCategoriesByDefault: number;
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
|
@ -17,12 +19,14 @@ interface ComponentProps {
|
|||
|
||||
const OtherSettings = (props: ComponentProps): JSX.Element => {
|
||||
const [formData, setFormData] = useState<FormState>({
|
||||
customTitle: document.title
|
||||
customTitle: document.title,
|
||||
pinAppsByDefault: 0,
|
||||
pinCategoriesByDefault: 0
|
||||
})
|
||||
|
||||
// get initial config
|
||||
useEffect(() => {
|
||||
axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle')
|
||||
axios.get<ApiResponse<Config[]>>('/api/config?keys=customTitle,pinAppsByDefault,pinCategoriesByDefault')
|
||||
.then(data => {
|
||||
let tmpFormData = { ...formData };
|
||||
|
||||
|
@ -60,10 +64,16 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
|
|||
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({
|
||||
...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)}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
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
|
||||
})
|
||||
} catch (err) {
|
||||
dispatch<GetAppsAction<string>>({
|
||||
type: ActionTypes.getAppsError,
|
||||
payload: err.data.data
|
||||
})
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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 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,
|
||||
|
|
|
@ -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
7
db.js
|
@ -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);
|
||||
|
|
|
@ -13,6 +13,10 @@ const Bookmark = sequelize.define('Bookmark', {
|
|||
categoryId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
icon: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: ''
|
||||
}
|
||||
}, {
|
||||
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 Config = require('../models/Config');
|
||||
const { config } = require('./initialConfig.json');
|
||||
|
||||
const initConfig = async () => {
|
||||
// Config keys
|
||||
const keys = ['WEATHER_API_KEY', 'lat', 'long', 'isCelsius', 'customTitle'];
|
||||
const values = ['', 0, 0, true, 'Flame'];
|
||||
|
||||
// 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
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 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();
|
||||
})
|
Loading…
Add table
Reference in a new issue