Merge pull request #145 from pawelmalak/feature

Version 1.7.4
This commit is contained in:
pawelmalak 2021-11-08 15:44:53 +01:00 committed by GitHub
commit 08afaece2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1536 additions and 1114 deletions

166
.dev/bookmarks_importer.py Executable file
View file

@ -0,0 +1,166 @@
import sqlite3
from bs4 import BeautifulSoup
from PIL import Image, UnidentifiedImageError
from io import BytesIO
import re
import base64
from datetime import datetime, timezone
import os
import argparse
"""
Imports html bookmarks file into Flame.
Tested only on Firefox html exports so far.
Usage:
python3 bookmarks_importer.py --bookmarks <path to bookmarks file> --data <path to flame data dir>
"""
parser = argparse.ArgumentParser()
parser.add_argument('--bookmarks', type=str, required=True)
parser.add_argument('--data', type=str, required=True)
args = parser.parse_args()
bookmarks_path = args.bookmarks
data_path = args.data
created = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + datetime.now().astimezone().strftime(" %z")
updated = created
if data_path[-1] != '/':
data_path = data_path + '/'
def Base64toPNG(codec, name):
"""
Convert base64 encoded image to png file
Reference: https://github.com/python-pillow/Pillow/issues/3400#issuecomment-428104239
Parameters:
codec (str): icon in html bookmark format.e.g. 'data:image/png;base64,<image encoding>'
name (str): name for export file
Returns:
icon_name(str): name of png output E.g. 1636473849374--mybookmark.png
None: if image not produced successfully
"""
try:
unix_t = str(int(datetime.now(tz=timezone.utc).timestamp() * 1000))
icon_name = unix_t + '--' + re.sub(r'\W+', '', name).lower() + '.png'
image_path = data_path + 'uploads/' + icon_name
if os.path.exists(image_path):
return image_path
base64_data = re.sub('^data:image/.+;base64,', '', codec)
byte_data = base64.b64decode(base64_data)
image_data = BytesIO(byte_data)
img = Image.open(image_data)
img.save(image_path, "PNG")
return icon_name
except UnidentifiedImageError:
return None
def FlameBookmarkParser(bookmarks_path):
"""
Parses HTML bookmarks file
Reference: https://stackoverflow.com/questions/68621107/extracting-bookmarks-and-folder-hierarchy-from-google-chrome-with-beautifulsoup
Parameters:
bookmarks_path (str): path to bookmarks.html
Returns:
None
"""
soup = BeautifulSoup()
with open(bookmarks_path) as f:
soup = BeautifulSoup(f.read(), 'lxml')
dt = soup.find_all('dt')
folder_name =''
for i in dt:
n = i.find_next()
if n.name == 'h3':
folder_name = n.text
continue
else:
url = n.get("href")
website_name = n.text
icon = n.get("icon")
if icon != None:
icon_name = Base64toPNG(icon, website_name)
cat_id = AddFlameCategory(folder_name)
AddFlameBookmark(website_name, url, cat_id, icon_name)
def AddFlameCategory(cat_name):
"""
Parses HTML bookmarks file
Parameters:
cat_name (str): category name
Returns:
cat_id (int): primary key id of cat_name
"""
con = sqlite3.connect(data_path + 'db.sqlite')
cur = con.cursor()
count_sql = ("SELECT count(*) FROM categories WHERE name = ?;")
cur.execute(count_sql, [cat_name])
count = int(cur.fetchall()[0][0])
if count > 0:
getid_sql = ("SELECT id FROM categories WHERE name = ?;")
cur.execute(getid_sql, [cat_name])
cat_id = int(cur.fetchall()[0][0])
return cat_id
is_pinned = 1
insert_sql = "INSERT OR IGNORE INTO categories(name, isPinned, createdAt, updatedAt) VALUES (?, ?, ?, ?);"
cur.execute(insert_sql, (cat_name, is_pinned, created, updated))
con.commit()
getid_sql = ("SELECT id FROM categories WHERE name = ?;")
cur.execute(getid_sql, [cat_name])
cat_id = int(cur.fetchall()[0][0])
return cat_id
def AddFlameBookmark(website_name, url, cat_id, icon_name):
con = sqlite3.connect(data_path + 'db.sqlite')
cur = con.cursor()
if icon_name == None:
insert_sql = "INSERT OR IGNORE INTO bookmarks(name, url, categoryId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?);"
cur.execute(insert_sql, (website_name, url, cat_id, created, updated))
con.commit()
else:
insert_sql = "INSERT OR IGNORE INTO bookmarks(name, url, categoryId, icon, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?);"
cur.execute(insert_sql, (website_name, url, cat_id, icon_name, created, updated))
con.commit()
if __name__ == "__main__":
FlameBookmarkParser(bookmarks_path)

9
.dev/getMdi.js Normal file
View file

@ -0,0 +1,9 @@
// Script to get all icon names from materialdesignicons.com
const getMdi = () => {
const icons = document.querySelectorAll('#icons div span');
const names = [...icons].map((icon) => icon.textContent.replace('mdi-', ''));
const output = names.map((name) => ({ name }));
output.pop();
const json = JSON.stringify(output);
console.log(json);
};

2
.env
View file

@ -1,3 +1,3 @@
PORT=5005
NODE_ENV=development
VERSION=1.7.3
VERSION=1.7.4

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules
data
public
!client/public
build.sh

View file

@ -1,3 +1,10 @@
### v1.7.4 (2021-11-08)
- Added option to set custom greetings and date ([#103](https://github.com/pawelmalak/flame/issues/103))
- Fallback to web search if local search has zero results ([#129](https://github.com/pawelmalak/flame/issues/129))
- Added iOS "Add to homescreen" icon ([#131](https://github.com/pawelmalak/flame/issues/131))
- Added experimental script to import bookmarks ([#141](https://github.com/pawelmalak/flame/issues/141))
- Added 3 new themes
### v1.7.3 (2021-10-28)
- Fixed bug with custom CSS not updating

View file

@ -1,6 +1,4 @@
FROM node:14-alpine
RUN apk update && apk add --no-cache nano curl
FROM node:14 as builder
WORKDIR /app
@ -18,6 +16,12 @@ RUN mkdir -p ./public ./data \
&& mv ./client/build/* ./public \
&& rm -rf ./client
FROM node:14-alpine
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 5005
ENV NODE_ENV=production

View file

@ -1,13 +1,10 @@
FROM node:14-alpine
RUN apk update && apk add --no-cache nano curl
FROM node:14 as builder
WORKDIR /app
COPY package*.json ./
RUN apk --no-cache --virtual build-dependencies add python make g++ \
&& npm install --production
RUN npm install --production
COPY . .
@ -17,8 +14,13 @@ RUN mkdir -p ./public ./data \
&& npm run build \
&& cd .. \
&& mv ./client/build/* ./public \
&& rm -rf ./client \
&& apk del build-dependencies
&& rm -rf ./client
FROM node:14-alpine
COPY --from=builder /app /app
WORKDIR /app
EXPOSE 5005

View file

@ -1,15 +1,10 @@
# Flame
[![JS Badge](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black)](https://shields.io/)
[![TS Badge](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://shields.io/)
[![Node Badge](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white)](https://shields.io/)
[![React Badge](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://shields.io/)
![Homescreen screenshot](./.github/_home.png)
## Description
Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own appliaction hub in no time - no file editing necessary.
Flame is self-hosted startpage for your server. Its design is inspired (heavily) by [SUI](https://github.com/jeroenpardon/sui). Flame is very easy to setup and use. With built-in editors it allows you to setup your very own application hub in no time - no file editing necessary.
## Technology
@ -42,7 +37,15 @@ npm run dev
### With Docker (recommended)
[Docker Hub](https://hub.docker.com/r/pawelmalak/flame)
[Docker Hub link](https://hub.docker.com/r/pawelmalak/flame)
```sh
docker pull pawelmalak/flame:latest
# for ARM architecture (e.g. RaspberryPi)
docker pull pawelmalak/flame:multiarch
```
#### Building images
@ -96,13 +99,14 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/
- Applications
- Create, update, delete and organize applications using GUI
- Pin your favourite apps to homescreen
- Pin your favourite apps to the homescreen
![Homescreen screenshot](./.github/_apps.png)
- Bookmarks
- Create, update, delete and organize bookmarks and categories using GUI
- Pin your favourite categories to homescreen
- Pin your favourite categories to the homescreen
- Import html bookmarks (experimental)
![Homescreen screenshot](./.github/_bookmarks.png)
@ -111,7 +115,7 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/
- Get current temperature, cloud coverage and weather status with animated icons
- Themes
- Customize your page by choosing from 12 color themes
- Customize your page by choosing from 15 color themes
![Homescreen screenshot](./.github/_themes.png)
@ -125,23 +129,7 @@ To use search bar you need to type your search query with selected prefix. For e
> You can change where to open search results (same/new tab) in the settings
#### Supported search engines
| Name | Prefix | Search URL |
| ---------- | ------ | ----------------------------------- |
| Disroot | /ds | http://search.disroot.org/search?q= |
| DuckDuckGo | /d | https://duckduckgo.com/?q= |
| Google | /g | https://www.google.com/search?q= |
#### Supported services
| Name | Prefix | Search URL |
| ------------------ | ------ | --------------------------------------------- |
| IMDb | /im | https://www.imdb.com/find?q= |
| Reddit | /r | https://www.reddit.com/search?q= |
| Spotify | /sp | https://open.spotify.com/search/ |
| The Movie Database | /mv | https://www.themoviedb.org/search?query= |
| Youtube | /yt | https://www.youtube.com/results?search_query= |
For list of supported search engines, shortcuts and more about searching functionality visit [project wiki](https://github.com/pawelmalak/flame/wiki/Search-bar).
### Setting up weather module
@ -159,13 +147,13 @@ labels:
- flame.type=application # "app" works too
- flame.name=My container
- flame.url=https://example.com
- flame.icon=icon-name # Optional, default is "docker"
- flame.icon=icon-name # optional, default is "docker"
# - flame.icon=custom to make changes in app. ie: custom icon upload
```
And you must have activated the Docker sync option in the settings panel.
> "Use Docker API" option must be enabled for this to work. You can find it in Settings > Other > Docker section
You can set up different apps in the same label adding `;` between each one.
You can also set up different apps in the same label adding `;` between each one.
```yml
labels:
@ -208,13 +196,27 @@ metadata:
- flame.pawelmalak/type=application # "app" works too
- flame.pawelmalak/name=My container
- flame.pawelmalak/url=https://example.com
- flame.pawelmalak/icon=icon-name # Optional, default is "kubernetes"
- flame.pawelmalak/icon=icon-name # optional, default is "kubernetes"
```
And you must have activated the Kubernetes sync option in the settings panel.
> "Use Kubernetes Ingress API" option must be enabled for this to work. You can find it in Settings > Other > Kubernetes section
### Custom CSS
### Import HTML Bookmarks (Experimental)
> This is an experimental feature. Its behaviour might change in the future.
>
> Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS)
- Requirements
- python3
- pip packages: Pillow, beautifulsoup4
- Backup your `db.sqlite` before running script!
- Known Issues:
- generated icons are sometimes incorrect
```bash
pip3 install Pillow, beautifulsoup4
cd flame/.dev
python3 bookmarks_importer.py --bookmarks <path to bookmarks.html> --data <path to flame data folder>
```
### Custom CSS and themes
See project wiki for [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) and [Custom theme with CSS](https://github.com/pawelmalak/flame/wiki/Custom-theme-with-CSS).

View file

@ -1 +1 @@
REACT_APP_VERSION=1.7.3
REACT_APP_VERSION=1.7.4

434
client/package-lock.json generated
View file

@ -1806,9 +1806,9 @@
}
},
"@mdi/js": {
"version": "5.9.55",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-5.9.55.tgz",
"integrity": "sha512-BbeHMgeK2/vjdJIRnx12wvQ6s8xAYfvMmEAVsUx9b+7GiQGQ9Za8jpwp17dMKr9CgKRvemlAM4S7S3QOtEbp4A=="
"version": "6.4.95",
"resolved": "https://registry.npmjs.org/@mdi/js/-/js-6.4.95.tgz",
"integrity": "sha512-b1/P//1D2KOzta8YRGyoSLGsAlWyUHfxzVBhV4e/ppnjM4DfBgay/vWz7Eg5Ee80JZ4zsQz8h54X+KOahtBk5Q=="
},
"@mdi/react": {
"version": "1.5.0",
@ -2047,20 +2047,45 @@
}
},
"@testing-library/dom": {
"version": "7.30.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.30.4.tgz",
"integrity": "sha512-GObDVMaI4ARrZEXaRy4moolNAxWPKvEYNV/fa6Uc2eAzR/t4otS6A7EhrntPBIQLeehL9DbVhscvvv7gd6hWqA==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.0.tgz",
"integrity": "sha512-8Ay4UDiMlB5YWy+ZvCeRyFFofs53ebxrWnOFvCoM1HpMAX4cHyuSrCuIM9l2lVuUWUt+Gr3loz/nCwdrnG6ShQ==",
"requires": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^4.2.0",
"aria-query": "^4.2.2",
"aria-query": "^5.0.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.4",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.4.4",
"pretty-format": "^26.6.2"
"pretty-format": "^27.0.2"
},
"dependencies": {
"@jest/types": {
"version": "27.2.5",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz",
"integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^16.0.0",
"chalk": "^4.0.0"
}
},
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -2069,10 +2094,15 @@
"color-convert": "^2.0.1"
}
},
"aria-query": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg=="
},
"chalk": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -2096,6 +2126,29 @@
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"pretty-format": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz",
"integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==",
"requires": {
"@jest/types": "^27.2.5",
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
}
}
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -2107,9 +2160,9 @@
}
},
"@testing-library/jest-dom": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.12.0.tgz",
"integrity": "sha512-N9Y82b2Z3j6wzIoAqajlKVF1Zt7sOH0pPee0sUHXHc5cv2Fdn23r+vpWm0MBBoGJtPOly5+Bdx1lnc3CD+A+ow==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.15.0.tgz",
"integrity": "sha512-lOMuQidnL1tWHLEWIhL6UvSZC1Qt3OkNe1khvi2h6xFiqpe5O8arYs46OU0qyUGq0cSTbroQyMktYNXu3a7sAA==",
"requires": {
"@babel/runtime": "^7.9.2",
"@types/testing-library__jest-dom": "^5.9.1",
@ -2117,6 +2170,7 @@
"chalk": "^3.0.0",
"css": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.5.6",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
@ -2191,18 +2245,18 @@
}
},
"@testing-library/react": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz",
"integrity": "sha512-TXMCg0jT8xmuU8BkKMtp8l7Z50Ykew5WNX8UoIKTaLFwKkP2+1YDhOLA2Ga3wY4x29jyntk7EWfum0kjlYiSjQ==",
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.2.tgz",
"integrity": "sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g==",
"requires": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^7.28.1"
"@testing-library/dom": "^8.0.0"
}
},
"@testing-library/user-event": {
"version": "12.8.3",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz",
"integrity": "sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ==",
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz",
"integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==",
"requires": {
"@babel/runtime": "^7.12.5"
}
@ -2213,9 +2267,9 @@
"integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA=="
},
"@types/aria-query": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.1.tgz",
"integrity": "sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg=="
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig=="
},
"@types/babel__core": {
"version": "7.1.14",
@ -2286,9 +2340,9 @@
}
},
"@types/history": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz",
"integrity": "sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA=="
"version": "4.7.9",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz",
"integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ=="
},
"@types/hoist-non-react-statics": {
"version": "3.3.1",
@ -2305,9 +2359,9 @@
"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==",
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.7.tgz",
"integrity": "sha512-9hdj6iXH64tHSLTY+Vt2eYOGzSogC+JQ2H7bdPWkuh7KXP5qLllWx++t+K9Wk556c3dkDdPws/SpMRi0sdCT1w==",
"requires": {
"@types/node": "*"
}
@ -2334,12 +2388,126 @@
}
},
"@types/jest": {
"version": "26.0.23",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
"integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
"version": "27.0.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz",
"integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==",
"requires": {
"jest-diff": "^26.0.0",
"pretty-format": "^26.0.0"
"jest-diff": "^27.0.0",
"pretty-format": "^27.0.0"
},
"dependencies": {
"@jest/types": {
"version": "27.2.5",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz",
"integrity": "sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ==",
"requires": {
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^16.0.0",
"chalk": "^4.0.0"
}
},
"@types/yargs": {
"version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
"integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==",
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"diff-sequences": {
"version": "27.0.6",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz",
"integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest-diff": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.3.1.tgz",
"integrity": "sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==",
"requires": {
"chalk": "^4.0.0",
"diff-sequences": "^27.0.6",
"jest-get-type": "^27.3.1",
"pretty-format": "^27.3.1"
}
},
"jest-get-type": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.3.1.tgz",
"integrity": "sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg=="
},
"pretty-format": {
"version": "27.3.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz",
"integrity": "sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA==",
"requires": {
"@jest/types": "^27.2.5",
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"dependencies": {
"ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="
}
}
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"@types/json-schema": {
@ -2358,9 +2526,9 @@
"integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA=="
},
"@types/node": {
"version": "12.20.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.12.tgz",
"integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg=="
"version": "16.11.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
"integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w=="
},
"@types/normalize-package-data": {
"version": "2.4.0",
@ -2378,9 +2546,9 @@
"integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA=="
},
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
"version": "15.7.4",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
},
"@types/q": {
"version": "1.5.4",
@ -2388,35 +2556,43 @@
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
},
"@types/react": {
"version": "17.0.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.5.tgz",
"integrity": "sha512-bj4biDB9ZJmGAYTWSKJly6bMr4BLUiBrx9ujiJEoP9XIDY9CTaPGxE5QWN/1WjpPLzYF7/jRNnV2nNxNe970sw==",
"version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz",
"integrity": "sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"@types/react-autosuggest": {
"version": "10.1.5",
"resolved": "https://registry.npmjs.org/@types/react-autosuggest/-/react-autosuggest-10.1.5.tgz",
"integrity": "sha512-qfMzrp6Is0VYRF5a97Bv/+P2F9ZtFY4YE2825yyWV4VxCpvcfvQHEqGNkDFIPme7t3B2BpQ784QBllYAxemERQ==",
"requires": {
"@types/react": "*"
}
},
"@types/react-beautiful-dnd": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz",
"integrity": "sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz",
"integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==",
"requires": {
"@types/react": "*"
}
},
"@types/react-dom": {
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz",
"integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==",
"version": "17.0.11",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz",
"integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==",
"requires": {
"@types/react": "*"
}
},
"@types/react-redux": {
"version": "7.1.16",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz",
"integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==",
"version": "7.1.20",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.20.tgz",
"integrity": "sha512-q42es4c8iIeTgcnB+yJgRTTzftv3eYYvCZOh1Ckn2eX/3o5TdsQYKUWpLoLuGlcY/p+VAhV9IOEZJcWk/vfkXw==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@ -2425,9 +2601,9 @@
}
},
"@types/react-router": {
"version": "5.1.14",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.14.tgz",
"integrity": "sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw==",
"version": "5.1.17",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.17.tgz",
"integrity": "sha512-RNSXOyb3VyRs/EOGmjBhhGKTbnN6fHWvy5FNLzWfOWOGjgVUKqJZXfpKzLmgoU8h6Hj8mpALj/mbXQASOb92wQ==",
"requires": {
"@types/history": "*",
"@types/react": "*"
@ -2452,9 +2628,9 @@
}
},
"@types/scheduler": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz",
"integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA=="
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
},
"@types/source-list-map": {
"version": "0.1.2",
@ -2472,9 +2648,9 @@
"integrity": "sha512-0VBprVqfgFD7Ehb2vd8Lh9TG3jP98gvr8rgehQqzztZNI7o8zS8Ad4jyZneKELphpuE212D8J70LnSNQSyO6bQ=="
},
"@types/testing-library__jest-dom": {
"version": "5.9.5",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz",
"integrity": "sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ==",
"version": "5.14.1",
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.1.tgz",
"integrity": "sha512-Gk9vaXfbzc5zCXI9eYE9BI5BNHEp4D3FWjgqBE/ePGYElLAP+KvxBcsdkwfIVvezs605oiyd/VrpiHe3Oeg+Aw==",
"requires": {
"@types/jest": "*"
}
@ -3159,11 +3335,18 @@
"integrity": "sha512-1uIESzroqpaTzt9uX48HO+6gfnKu3RwvWdCcWSrX4csMInJfCo1yvKPNXCwXFRpJqRW25tiASb6No0YH57PXqg=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.4"
},
"dependencies": {
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
}
}
},
"axobject-query": {
@ -4906,9 +5089,9 @@
}
},
"csstype": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz",
"integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw=="
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.9.tgz",
"integrity": "sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw=="
},
"cyclist": {
"version": "1.0.1",
@ -5227,9 +5410,9 @@
}
},
"dom-accessibility-api": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz",
"integrity": "sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ=="
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz",
"integrity": "sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g=="
},
"dom-converter": {
"version": "0.2.0",
@ -5577,6 +5760,11 @@
"es6-symbol": "^3.1.1"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
@ -7481,9 +7669,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.0.tgz",
"integrity": "sha512-S+RN5njuyvYV760aiVKnyuTXqUMcSIvYOsHA891DOVQyrdZOwaXtBHpt9FUVPEDAsOvsPArZp6VXQLs44yvkow==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz",
"integrity": "sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg==",
"requires": {
"@types/http-proxy": "^1.17.5",
"http-proxy": "^1.18.1",
@ -12114,9 +12302,9 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
},
"prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.4.1.tgz",
"integrity": "sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==",
"dev": true
},
"pretty-bytes": {
@ -12407,6 +12595,18 @@
"whatwg-fetch": "^3.4.1"
}
},
"react-autosuggest": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-10.1.0.tgz",
"integrity": "sha512-/azBHmc6z/31s/lBf6irxPf/7eejQdR0IqnZUzjdSibtlS8+Rw/R79pgDAo6Ft5QqCUTyEQ+f0FhL+1olDQ8OA==",
"requires": {
"es6-promise": "^4.2.8",
"prop-types": "^15.7.2",
"react-themeable": "^1.1.0",
"section-iterator": "^2.0.0",
"shallow-equal": "^1.2.1"
}
},
"react-beautiful-dnd": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz",
@ -12548,16 +12748,31 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-redux": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.4.tgz",
"integrity": "sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==",
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz",
"integrity": "sha512-10RPdsz0UUrRL1NZE0ejTkucnclYSgXp5q+tB5SWx2qeG2ZJQJyymgAhwKy73yiL/13btfB6fPr+rgbMAaZIAQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"@types/react-redux": "^7.1.16",
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^16.13.1"
"react-is": "^17.0.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.0.tgz",
"integrity": "sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}
}
},
"react-refresh": {
@ -12677,6 +12892,21 @@
"workbox-webpack-plugin": "5.1.4"
}
},
"react-themeable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz",
"integrity": "sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4=",
"requires": {
"object-assign": "^3.0.0"
},
"dependencies": {
"object-assign": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz",
"integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I="
}
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -12793,9 +13023,9 @@
}
},
"redux": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz",
"integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==",
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz",
"integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==",
"requires": {
"@babel/runtime": "^7.9.2"
}
@ -12806,9 +13036,9 @@
"integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A=="
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.0.tgz",
"integrity": "sha512-/y6ZKQNU/0u8Bm7ROLq9Pt/7lU93cT0IucYMrubo89ENjxPa7i8pqLKu6V4X7/TvYovQ6x01unTeyeZ9lgXiTA=="
},
"regenerate": {
"version": "1.4.2",
@ -13511,6 +13741,11 @@
"ajv-keywords": "^3.5.2"
}
},
"section-iterator": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz",
"integrity": "sha1-v0RNev7rlK1Dw5rS+yYVFifMuio="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -13685,6 +13920,11 @@
"safe-buffer": "^5.0.1"
}
},
"shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -14926,9 +15166,9 @@
}
},
"typescript": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg=="
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA=="
},
"unbox-primitive": {
"version": "1.0.1",
@ -15525,9 +15765,9 @@
}
},
"web-vitals": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.2.tgz",
"integrity": "sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig=="
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.2.tgz",
"integrity": "sha512-nZnEH8dj+vJFqCRYdvYv0a59iLXsb8jJkt+xvXfwgnkyPdsSLtKNlYmtTDiHmTNGXeSXtpjTTUcNvFtrAk6VMQ=="
},
"webidl-conversions": {
"version": "6.1.0",

View file

@ -3,33 +3,35 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@mdi/js": "^5.9.55",
"@mdi/js": "^6.4.95",
"@mdi/react": "^1.5.0",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.23",
"@types/node": "^12.20.12",
"@types/react": "^17.0.5",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.3",
"@types/react-redux": "^7.1.16",
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.0.2",
"@types/node": "^16.11.6",
"@types/react": "^17.0.34",
"@types/react-autosuggest": "^10.1.5",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.20",
"@types/react-router-dom": "^5.1.7",
"axios": "^0.21.1",
"axios": "^0.24.0",
"external-svg-loader": "^1.3.4",
"http-proxy-middleware": "^2.0.0",
"http-proxy-middleware": "^2.0.1",
"react": "^17.0.2",
"react-autosuggest": "^10.1.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2",
"react-redux": "^7.2.4",
"react-redux": "^7.2.6",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"redux": "^4.1.0",
"redux": "^4.1.2",
"redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.3.0",
"redux-thunk": "^2.4.0",
"skycons-ts": "^0.2.0",
"typescript": "^4.2.4",
"web-vitals": "^1.1.2"
"typescript": "^4.4.4",
"web-vitals": "^2.1.2"
},
"scripts": {
"start": "react-scripts start",
@ -56,6 +58,6 @@
]
},
"devDependencies": {
"prettier": "^2.3.2"
"prettier": "^2.4.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -2,7 +2,51 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="%PUBLIC_URL%/icons/favicon.ico" />
<link
rel="apple-touch-icon"
href="%PUBLIC_URL%/icons/apple-touch-icon.png"
/>
<link
rel="apple-touch-icon"
sizes="57x57"
href="%PUBLIC_URL%/icons/apple-touch-icon-57x57.png"
/>
<link
rel="apple-touch-icon"
sizes="72x72"
href="%PUBLIC_URL%/icons/apple-touch-icon-72x72.png"
/>
<link
rel="apple-touch-icon"
sizes="76x76"
href="%PUBLIC_URL%/icons/apple-touch-icon-76x76.png"
/>
<link
rel="apple-touch-icon"
sizes="114x114"
href="%PUBLIC_URL%/icons/apple-touch-icon-114x114.png"
/>
<link
rel="apple-touch-icon"
sizes="120x120"
href="%PUBLIC_URL%/icons/apple-touch-icon-120x120.png"
/>
<link
rel="apple-touch-icon"
sizes="144x144"
href="%PUBLIC_URL%/icons/apple-touch-icon-144x144.png"
/>
<link
rel="apple-touch-icon"
sizes="152x152"
href="%PUBLIC_URL%/icons/apple-touch-icon-152x152.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/icons/apple-touch-icon-180x180.png"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"

View file

@ -0,0 +1,31 @@
.Header h1 {
color: var(--color-primary);
font-weight: 700;
font-size: 4em;
display: inline-block;
}
.Header p {
color: var(--color-primary);
font-weight: 300;
text-transform: uppercase;
height: 30px;
}
.HeaderMain {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2.5rem;
}
.SettingsLink {
visibility: visible;
color: var(--color-accent);
}
@media (min-width: 769px) {
.SettingsLink {
visibility: hidden;
}
}

View file

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { Config, GlobalState } from '../../../interfaces';
import WeatherWidget from '../../Widgets/WeatherWidget/WeatherWidget';
import { getDateTime } from './functions/getDateTime';
import { greeter } from './functions/greeter';
import classes from './Header.module.css';
interface Props {
config: Config;
}
const Header = (props: Props): JSX.Element => {
const [dateTime, setDateTime] = useState<string>(getDateTime());
const [greeting, setGreeting] = useState<string>(greeter());
useEffect(() => {
let dateTimeInterval: NodeJS.Timeout;
dateTimeInterval = setInterval(() => {
setDateTime(getDateTime());
setGreeting(greeter());
}, 1000);
return () => window.clearInterval(dateTimeInterval);
}, []);
return (
<header className={classes.Header}>
<p>{dateTime}</p>
<Link to="/settings" className={classes.SettingsLink}>
Go to Settings
</Link>
<span className={classes.HeaderMain}>
<h1>{greeting}</h1>
<WeatherWidget />
</span>
</header>
);
};
const mapStateToProps = (state: GlobalState) => {
return {
config: state.config.config,
};
};
export default connect(mapStateToProps)(Header);

View file

@ -1,5 +1,5 @@
export const dateTime = (): string => {
const days = [
export const getDateTime = (): string => {
const days = localStorage.getItem('daySchema')?.split(';') || [
'Sunday',
'Monday',
'Tuesday',
@ -8,7 +8,8 @@ export const dateTime = (): string => {
'Friday',
'Saturday',
];
const months = [
const months = localStorage.getItem('monthSchema')?.split(';') || [
'January',
'February',
'March',

View file

@ -0,0 +1,17 @@
export const greeter = (): string => {
const now = new Date().getHours();
let msg: string;
const greetingsSchemaRaw =
localStorage.getItem('greetingsSchema') ||
'Good evening!;Good afternoon!;Good morning!;Good night!';
const greetingsSchema = greetingsSchemaRaw.split(';');
if (now >= 18) msg = greetingsSchema[0];
else if (now >= 12) msg = greetingsSchema[1];
else if (now >= 6) msg = greetingsSchema[2];
else if (now >= 0) msg = greetingsSchema[3];
else msg = 'Hello!';
return msg;
};

View file

@ -1,24 +1,3 @@
.Header h1 {
color: var(--color-primary);
font-weight: 700;
font-size: 4em;
display: inline-block;
}
.Header p {
color: var(--color-primary);
font-weight: 300;
text-transform: uppercase;
height: 30px;
}
.HeaderMain {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2.5rem;
}
.SettingsButton {
width: 35px;
height: 35px;
@ -40,19 +19,10 @@
opacity: 1;
}
.SettingsLink {
visibility: visible;
color: var(--color-accent);
}
@media (min-width: 769px) {
.SettingsButton {
visibility: visible;
}
.SettingsLink {
visibility: hidden;
}
}
.HomeSpace {

View file

@ -21,12 +21,8 @@ import classes from './Home.module.css';
// Components
import AppGrid from '../Apps/AppGrid/AppGrid';
import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid';
import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget';
import SearchBar from '../SearchBar/SearchBar';
// Functions
import { greeter } from './functions/greeter';
import { dateTime } from './functions/dateTime';
import Header from './Header/Header';
interface ComponentProps {
getApps: Function;
@ -48,11 +44,6 @@ const Home = (props: ComponentProps): JSX.Element => {
categoriesLoading,
} = props;
const [header, setHeader] = useState({
dateTime: dateTime(),
greeting: greeter(),
});
// Local search query
const [localSearch, setLocalSearch] = useState<null | string>(null);
const [appSearchResult, setAppSearchResult] = useState<null | App[]>(null);
@ -74,23 +65,6 @@ const Home = (props: ComponentProps): JSX.Element => {
}
}, [getCategories]);
// Refresh greeter and time
useEffect(() => {
let interval: any;
// Start interval only when hideHeader is false
if (!props.config.hideHeader) {
interval = setInterval(() => {
setHeader({
dateTime: dateTime(),
greeting: greeter(),
});
}, 1000);
}
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (localSearch) {
// Search through apps
@ -126,20 +100,7 @@ const Home = (props: ComponentProps): JSX.Element => {
<div></div>
)}
{!props.config.hideHeader ? (
<header className={classes.Header}>
<p>{header.dateTime}</p>
<Link to="/settings" className={classes.SettingsLink}>
Go to Settings
</Link>
<span className={classes.HeaderMain}>
<h1>{header.greeting}</h1>
<WeatherWidget />
</span>
</header>
) : (
<div></div>
)}
{!props.config.hideHeader ? <Header /> : <div></div>}
{!props.config.hideApps ? (
<Fragment>

View file

@ -1,12 +0,0 @@
export const greeter = (): string => {
const now = new Date().getHours();
let msg: string;
if (now >= 18) msg = 'Good evening!';
else if (now >= 12) msg = 'Good afternoon!';
else if (now >= 6) msg = 'Good morning!';
else if (now >= 0) msg = 'Good night!';
else msg = 'Hello!';
return msg;
}

View file

@ -91,8 +91,18 @@ const SearchBar = (props: ComponentProps): JSX.Element => {
// Local query -> redirect if at least 1 result found
if (appSearchResult?.length) {
redirectUrl(appSearchResult[0].url, sameTab);
} else if (bookmarkSearchResult?.length) {
} else if (bookmarkSearchResult?.[0]?.bookmarks?.length) {
redirectUrl(bookmarkSearchResult[0].bookmarks[0].url, sameTab);
} else {
// no local results -> search the internet with the default search provider
let template = query.template;
if (query.prefix === 'l') {
template = 'https://duckduckgo.com/?q=';
}
const url = `${template}${search}`;
redirectUrl(url, sameTab);
}
} else {
// Valid query -> redirect to search results

View file

@ -81,6 +81,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<form onSubmit={(e) => formSubmitHandler(e)}>
{/* OTHER OPTIONS */}
<SettingsHeadline text="Miscellaneous" />
{/* PAGE TITLE */}
<InputGroup>
<label htmlFor="customTitle">Custom page title</label>
<input
@ -92,6 +93,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
{/* DATE FORMAT */}
<InputGroup>
<label htmlFor="useAmericanDate">Date formatting</label>
<select
@ -107,6 +110,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* BEAHVIOR OPTIONS */}
<SettingsHeadline text="App Behavior" />
{/* PIN APPS */}
<InputGroup>
<label htmlFor="pinAppsByDefault">
Pin new applications by default
@ -121,6 +125,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* PIN CATEGORIES */}
<InputGroup>
<label htmlFor="pinCategoriesByDefault">
Pin new categories by default
@ -135,6 +141,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* SORT TYPE */}
<InputGroup>
<label htmlFor="useOrdering">Sorting type</label>
<select
@ -148,6 +156,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value="orderId">Custom order</option>
</select>
</InputGroup>
{/* APPS OPPENING */}
<InputGroup>
<label htmlFor="appsSameTab">Open applications in the same tab</label>
<select
@ -160,6 +170,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* BOOKMARKS OPPENING */}
<InputGroup>
<label htmlFor="bookmarksSameTab">Open bookmarks in the same tab</label>
<select
@ -175,6 +187,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* MODULES OPTIONS */}
<SettingsHeadline text="Modules" />
{/* HIDE HEADER */}
<InputGroup>
<label htmlFor="hideHeader">Hide greeting and date</label>
<select
@ -187,6 +200,53 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* CUSTOM GREETINGS */}
<InputGroup>
<label htmlFor="greetingsSchema">Custom greetings</label>
<input
type="text"
id="greetingsSchema"
name="greetingsSchema"
placeholder="Good day;Hi;Bye!"
value={formData.greetingsSchema}
onChange={(e) => inputChangeHandler(e)}
/>
<span>
Greetings must be separated with semicolon. Only 4 messages can be
used
</span>
</InputGroup>
{/* CUSTOM DAYS */}
<InputGroup>
<label htmlFor="daySchema">Custom weekday names</label>
<input
type="text"
id="daySchema"
name="daySchema"
placeholder="Sunday;Monday;Tuesday"
value={formData.daySchema}
onChange={(e) => inputChangeHandler(e)}
/>
<span>Names must be separated with semicolon</span>
</InputGroup>
{/* CUSTOM MONTHS */}
<InputGroup>
<label htmlFor="monthSchema">Custom month names</label>
<input
type="text"
id="monthSchema"
name="monthSchema"
placeholder="January;February;March"
value={formData.monthSchema}
onChange={(e) => inputChangeHandler(e)}
/>
<span>Names must be separated with semicolon</span>
</InputGroup>
{/* HIDE APPS */}
<InputGroup>
<label htmlFor="hideApps">Hide applications</label>
<select
@ -199,6 +259,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* HIDE CATEGORIES */}
<InputGroup>
<label htmlFor="hideCategories">Hide categories</label>
<select
@ -214,6 +276,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* DOCKER SETTINGS */}
<SettingsHeadline text="Docker" />
{/* CUSTOM DOCKER SOCKET HOST */}
<InputGroup>
<label htmlFor="dockerHost">Docker Host</label>
<input
@ -225,6 +288,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
onChange={(e) => inputChangeHandler(e)}
/>
</InputGroup>
{/* USE DOCKER API */}
<InputGroup>
<label htmlFor="dockerApps">Use Docker API</label>
<select
@ -237,6 +302,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
<option value={0}>False</option>
</select>
</InputGroup>
{/* UNPIN DOCKER APPS */}
<InputGroup>
<label htmlFor="unpinStoppedApps">
Unpin stopped containers / other apps
@ -254,6 +321,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => {
{/* KUBERNETES SETTINGS */}
<SettingsHeadline text="Kubernetes" />
{/* USE KUBERNETES */}
<InputGroup>
<label htmlFor="kubernetesApps">Use Kubernetes Ingress API</label>
<select

View file

@ -95,6 +95,30 @@
"primary": "#4C432E",
"accent": "#AA9A73"
}
},
{
"name": "neon",
"colors": {
"background": "#091833",
"primary": "#EFFBFF",
"accent": "#ea00d9"
}
},
{
"name": "pumpkin",
"colors": {
"background": "#2d3436",
"primary": "#EFFBFF",
"accent": "#ffa500"
}
},
{
"name": "onedark",
"colors": {
"background": "#282c34",
"primary": "#dfd9d6",
"accent": "#98c379"
}
}
]
}

View file

@ -21,4 +21,7 @@ export interface Config {
unpinStoppedApps: boolean;
useAmericanDate: boolean;
disableAutofocus: boolean;
greetingsSchema: string;
daySchema: string;
monthSchema: string;
}

View file

@ -27,4 +27,7 @@ export interface OtherSettingsForm {
kubernetesApps: boolean;
unpinStoppedApps: boolean;
useAmericanDate: boolean;
greetingsSchema: string;
daySchema: string;
monthSchema: string;
}

View file

@ -3,6 +3,7 @@ import { Dispatch } from 'redux';
import { ActionTypes } from './actionTypes';
import { Config, ApiResponse, Query } from '../../interfaces';
import { CreateNotificationAction } from './notification';
import { storeUIConfig } from '../../utility';
export interface GetConfigAction {
type: ActionTypes.getConfig;
@ -22,7 +23,15 @@ export const getConfig = () => async (dispatch: Dispatch) => {
document.title = res.data.data.customTitle;
// Store settings for priority UI elements
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
const keys: (keyof Config)[] = [
'useAmericanDate',
'greetingsSchema',
'daySchema',
'monthSchema',
];
for (let key of keys) {
storeUIConfig(key, res.data.data);
}
} catch (err) {
console.log(err);
}
@ -51,7 +60,15 @@ export const updateConfig = (formData: any) => async (dispatch: Dispatch) => {
});
// Store settings for priority UI elements
localStorage.setItem('useAmericanDate', `${res.data.data.useAmericanDate}`);
const keys: (keyof Config)[] = [
'useAmericanDate',
'greetingsSchema',
'daySchema',
'monthSchema',
];
for (let key of keys) {
storeUIConfig(key, res.data.data);
}
} catch (err) {
console.log(err);
}

View file

@ -10,7 +10,7 @@ export interface State {
const initialState: State = {
loading: true,
config: configTemplate,
config: { ...configTemplate },
customQueries: [],
};

View file

@ -6,3 +6,4 @@ export * from './searchParser';
export * from './redirectUrl';
export * from './templateObjects';
export * from './inputHandler';
export * from './storeUIConfig';

View file

@ -0,0 +1,8 @@
import { Config } from '../interfaces';
export const storeUIConfig = <K extends keyof Config>(
key: K,
config: Config
) => {
localStorage.setItem(key, `${config[key]}`);
};

View file

@ -23,4 +23,8 @@ export const configTemplate: Config = {
unpinStoppedApps: false,
useAmericanDate: false,
disableAutofocus: false,
greetingsSchema: 'Good evening!;Good afternoon!;Good morning!;Good night!',
daySchema: 'Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday',
monthSchema:
'January;February;March;April;May;June;July;August;September;October;November;December',
};

View file

@ -15,6 +15,10 @@ export const otherSettingsTemplate: OtherSettingsForm = {
kubernetesApps: true,
unpinStoppedApps: true,
useAmericanDate: false,
greetingsSchema: 'Good evening!;Good afternoon!;Good morning!;Good night!',
daySchema: 'Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday',
monthSchema:
'January;February;March;April;May;June;July;August;September;October;November;December',
};
export const weatherSettingsTemplate: WeatherForm = {

View file

@ -1,57 +0,0 @@
const queries = require('../../src/utility/searchQueries.json');
const fs = require('fs');
const prettier = require('prettier');
/**
* @description CLI tool for adding new search engines/providers. It will ensure that prefix is unique and that all entries are sorted alphabetically
* @argumens name prefix template
* @example node cli-searchQueries.js "DuckDuckGo" "d" "https://duckduckgo.com/?q="
*/
// Get arguments
const args = process.argv.slice(2);
// Check arguments
if (args.length < 3) {
return console.log('Missing arguments');
} else if (args.length > 3) {
return console.log('Too many arguments provided');
}
// Construct new query object
const newQuery = {
name: args[0],
prefix: args[1],
template: args[2],
};
// Get old queries
let rawQueries = queries.queries;
let parsedQueries = '';
// Check if prefix is unique
const isUnique = !rawQueries.find((query) => query.prefix == newQuery.prefix);
if (!isUnique) {
return console.log('Prefix already exists');
}
// Add new query
rawQueries.push(newQuery);
// Sort alphabetically
rawQueries = rawQueries.sort((a, b) => {
const _a = a.name.toLowerCase();
const _b = b.name.toLowerCase();
if (_a < _b) return -1;
if (_a > _b) return 1;
return 0;
});
// Format JSON
parsedQueries = JSON.stringify(queries);
parsedQueries = prettier.format(parsedQueries, { parser: 'json' });
// Save file
fs.writeFileSync('../../src/utility/searchQueries.json', parsedQueries);

View file

@ -0,0 +1,28 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Category = require('../../models/Category');
const loadConfig = require('../../utils/loadConfig');
// @desc Create new category
// @route POST /api/categories
// @access Public
const createCategory = asyncWrapper(async (req, res, next) => {
const { pinCategoriesByDefault: pinCategories } = await loadConfig();
let category;
if (pinCategories) {
category = await Category.create({
...req.body,
isPinned: true,
});
} else {
category = await Category.create(req.body);
}
res.status(201).json({
success: true,
data: category,
});
});
module.exports = createCategory;

View file

@ -0,0 +1,45 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const Category = require('../../models/Category');
const Bookmark = require('../../models/Bookmark');
// @desc Delete category
// @route DELETE /api/categories/:id
// @access Public
const deleteCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
category.bookmarks.forEach(async (bookmark) => {
await Bookmark.destroy({
where: { id: bookmark.id },
});
});
await Category.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});
module.exports = deleteCategory;

View file

@ -0,0 +1,43 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Category = require('../../models/Category');
const Bookmark = require('../../models/Bookmark');
const { Sequelize } = require('sequelize');
const loadConfig = require('../../utils/loadConfig');
// @desc Get all categories
// @route GET /api/categories
// @access Public
const getAllCategories = asyncWrapper(async (req, res, next) => {
const { useOrdering: orderType } = await loadConfig();
let categories;
if (orderType == 'name') {
categories = await Category.findAll({
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
order: [[Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC']],
});
} else {
categories = await Category.findAll({
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
order: [[orderType, 'ASC']],
});
}
res.status(200).json({
success: true,
data: categories,
});
});
module.exports = getAllCategories;

View file

@ -0,0 +1,35 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const Category = require('../../models/Category');
const Bookmark = require('../../models/Bookmark');
// @desc Get single category
// @route GET /api/categories/:id
// @access Public
const getSingleCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
res.status(200).json({
success: true,
data: category,
});
});
module.exports = getSingleCategory;

View file

@ -0,0 +1,8 @@
module.exports = {
createCategory: require('./createCategory'),
getAllCategories: require('./getAllCategories'),
getSingleCategory: require('./getSingleCategory'),
updateCategory: require('./updateCategory'),
deleteCategory: require('./deleteCategory'),
reorderCategories: require('./reorderCategories'),
};

View file

@ -0,0 +1,22 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Category = require('../../models/Category');
// @desc Reorder categories
// @route PUT /api/categories/0/reorder
// @access Public
const reorderCategories = asyncWrapper(async (req, res, next) => {
req.body.categories.forEach(async ({ id, orderId }) => {
await Category.update(
{ orderId },
{
where: { id },
}
);
});
res.status(200).json({
success: true,
data: {},
});
});
module.exports = reorderCategories;

View file

@ -0,0 +1,30 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const ErrorResponse = require('../../utils/ErrorResponse');
const Category = require('../../models/Category');
// @desc Update category
// @route PUT /api/categories/:id
// @access Public
const updateCategory = asyncWrapper(async (req, res, next) => {
let category = await Category.findOne({
where: { id: req.params.id },
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
category = await category.update({ ...req.body });
res.status(200).json({
success: true,
data: category,
});
});
module.exports = updateCategory;

View file

@ -1,178 +0,0 @@
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');
const { Sequelize } = require('sequelize');
const loadConfig = require('../utils/loadConfig');
// @desc Create new category
// @route POST /api/categories
// @access Public
exports.createCategory = asyncWrapper(async (req, res, next) => {
const { pinCategoriesByDefault: pinCategories } = await loadConfig();
let category;
if (pinCategories) {
category = await Category.create({
...req.body,
isPinned: true,
});
} else {
category = await Category.create(req.body);
}
res.status(201).json({
success: true,
data: category,
});
});
// @desc Get all categories
// @route GET /api/categories
// @access Public
exports.getCategories = asyncWrapper(async (req, res, next) => {
const { useOrdering: orderType } = await loadConfig();
let categories;
if (orderType == 'name') {
categories = await Category.findAll({
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
order: [[Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC']],
});
} else {
categories = await Category.findAll({
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
order: [[orderType, 'ASC']],
});
}
res.status(200).json({
success: true,
data: categories,
});
});
// @desc Get single category
// @route GET /api/categories/:id
// @access Public
exports.getCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
res.status(200).json({
success: true,
data: category,
});
});
// @desc Update category
// @route PUT /api/categories/:id
// @access Public
exports.updateCategory = asyncWrapper(async (req, res, next) => {
let category = await Category.findOne({
where: { id: req.params.id },
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
category = await category.update({ ...req.body });
res.status(200).json({
success: true,
data: category,
});
});
// @desc Delete category
// @route DELETE /api/categories/:id
// @access Public
exports.deleteCategory = asyncWrapper(async (req, res, next) => {
const category = await Category.findOne({
where: { id: req.params.id },
include: [
{
model: Bookmark,
as: 'bookmarks',
},
],
});
if (!category) {
return next(
new ErrorResponse(
`Category with id of ${req.params.id} was not found`,
404
)
);
}
category.bookmarks.forEach(async (bookmark) => {
await Bookmark.destroy({
where: { id: bookmark.id },
});
});
await Category.destroy({
where: { id: req.params.id },
});
res.status(200).json({
success: true,
data: {},
});
});
// @desc Reorder categories
// @route PUT /api/categories/0/reorder
// @access Public
exports.reorderCategories = asyncWrapper(async (req, res, next) => {
req.body.categories.forEach(async ({ id, orderId }) => {
await Category.update(
{ orderId },
{
where: { id },
}
);
});
res.status(200).json({
success: true,
data: {},
});
});

View file

@ -0,0 +1,21 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
// @desc Add custom search query
// @route POST /api/queries
// @access Public
const addQuery = asyncWrapper(async (req, res, next) => {
const file = new File('data/customQueries.json');
let content = JSON.parse(file.read());
// Add new query
content.queries.push(req.body);
file.write(content, true);
res.status(201).json({
success: true,
data: req.body,
});
});
module.exports = addQuery;

View file

@ -0,0 +1,22 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
// @desc Delete query
// @route DELETE /api/queries/:prefix
// @access Public
const deleteQuery = asyncWrapper(async (req, res, next) => {
const file = new File('data/customQueries.json');
let content = JSON.parse(file.read());
content.queries = content.queries.filter(
(q) => q.prefix != req.params.prefix
);
file.write(content, true);
res.status(200).json({
success: true,
data: content.queries,
});
});
module.exports = deleteQuery;

View file

@ -0,0 +1,17 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
// @desc Get custom queries file
// @route GET /api/queries
// @access Public
const getQueries = asyncWrapper(async (req, res, next) => {
const file = new File('data/customQueries.json');
const content = JSON.parse(file.read());
res.status(200).json({
success: true,
data: content.queries,
});
});
module.exports = getQueries;

View file

@ -1,81 +1,6 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
const { join } = require('path');
const QUERIES_PATH = join(__dirname, '../../data/customQueries.json');
// @desc Add custom search query
// @route POST /api/queries
// @access Public
exports.addQuery = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
let content = JSON.parse(file.read());
// Add new query
content.queries.push(req.body);
file.write(content, true);
res.status(201).json({
success: true,
data: req.body,
});
});
// @desc Get custom queries file
// @route GET /api/queries
// @access Public
exports.getQueries = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
const content = JSON.parse(file.read());
res.status(200).json({
success: true,
data: content.queries,
});
});
// @desc Update query
// @route PUT /api/queries/:prefix
// @access Public
exports.updateQuery = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
let content = JSON.parse(file.read());
let queryIdx = content.queries.findIndex(
(q) => q.prefix == req.params.prefix
);
// query found
if (queryIdx > -1) {
content.queries = [
...content.queries.slice(0, queryIdx),
req.body,
...content.queries.slice(queryIdx + 1),
];
}
file.write(content, true);
res.status(200).json({
success: true,
data: content.queries,
});
});
// @desc Delete query
// @route DELETE /api/queries/:prefix
// @access Public
exports.deleteQuery = asyncWrapper(async (req, res, next) => {
const file = new File(QUERIES_PATH);
let content = JSON.parse(file.read());
content.queries = content.queries.filter(
(q) => q.prefix != req.params.prefix
);
file.write(content, true);
res.status(200).json({
success: true,
data: content.queries,
});
});
module.exports = {
addQuery: require('./addQuery'),
getQueries: require('./getQueries'),
updateQuery: require('./updateQuery'),
deleteQuery: require('./deleteQuery'),
};

View file

@ -0,0 +1,32 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const File = require('../../utils/File');
// @desc Update query
// @route PUT /api/queries/:prefix
// @access Public
const updateQuery = asyncWrapper(async (req, res, next) => {
const file = new File('data/customQueries.json');
let content = JSON.parse(file.read());
let queryIdx = content.queries.findIndex(
(q) => q.prefix == req.params.prefix
);
// query found
if (queryIdx > -1) {
content.queries = [
...content.queries.slice(0, queryIdx),
req.body,
...content.queries.slice(queryIdx + 1),
];
}
file.write(content, true);
res.status(200).json({
success: true,
data: content.queries,
});
});
module.exports = updateQuery;

View file

@ -1,31 +0,0 @@
const asyncWrapper = require('../middleware/asyncWrapper');
const ErrorResponse = require('../utils/ErrorResponse');
const Weather = require('../models/Weather');
const getExternalWeather = require('../utils/getExternalWeather');
// @desc Get latest weather status
// @route GET /api/weather
// @access Public
exports.getWeather = asyncWrapper(async (req, res, next) => {
const weather = await Weather.findAll({
order: [['createdAt', 'DESC']],
limit: 1,
});
res.status(200).json({
success: true,
data: weather,
});
});
// @desc Update weather
// @route GET /api/weather/update
// @access Public
exports.updateWeather = asyncWrapper(async (req, res, next) => {
const weather = await getExternalWeather();
res.status(200).json({
success: true,
data: weather,
});
});

View file

@ -0,0 +1,19 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const Weather = require('../../models/Weather');
// @desc Get latest weather status
// @route GET /api/weather
// @access Public
const getWeather = asyncWrapper(async (req, res, next) => {
const weather = await Weather.findAll({
order: [['createdAt', 'DESC']],
limit: 1,
});
res.status(200).json({
success: true,
data: weather,
});
});
module.exports = getWeather;

View file

@ -0,0 +1,4 @@
module.exports = {
getWeather: require('./getWather'),
updateWeather: require('./updateWeather'),
};

View file

@ -0,0 +1,16 @@
const asyncWrapper = require('../../middleware/asyncWrapper');
const getExternalWeather = require('../../utils/getExternalWeather');
// @desc Update weather
// @route GET /api/weather/update
// @access Public
const updateWeather = asyncWrapper(async (req, res, next) => {
const weather = await getExternalWeather();
res.status(200).json({
success: true,
data: weather,
});
});
module.exports = updateWeather;

734
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,21 +17,21 @@
"author": "",
"license": "ISC",
"dependencies": {
"@kubernetes/client-node": "^0.15.0",
"@types/express": "^4.17.11",
"axios": "^0.21.1",
"@kubernetes/client-node": "^0.15.1",
"@types/express": "^4.17.13",
"axios": "^0.24.0",
"colors": "^1.4.0",
"concurrently": "^6.0.2",
"dotenv": "^9.0.0",
"concurrently": "^6.3.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"multer": "^1.4.2",
"multer": "^1.4.3",
"node-schedule": "^2.0.0",
"sequelize": "^6.6.2",
"sequelize": "^6.9.0",
"sqlite3": "^5.0.2",
"umzug": "^2.3.0",
"ws": "^7.4.6"
"ws": "^8.2.3"
},
"devDependencies": {
"nodemon": "^2.0.7"
"nodemon": "^2.0.14"
}
}

View file

@ -3,26 +3,21 @@ const router = express.Router();
const {
createCategory,
getCategories,
getCategory,
getAllCategories,
getSingleCategory,
updateCategory,
deleteCategory,
reorderCategories
} = require('../controllers/category');
reorderCategories,
} = require('../controllers/categories');
router
.route('/')
.post(createCategory)
.get(getCategories);
router.route('/').post(createCategory).get(getAllCategories);
router
.route('/:id')
.get(getCategory)
.get(getSingleCategory)
.put(updateCategory)
.delete(deleteCategory);
router
.route('/0/reorder')
.put(reorderCategories);
router.route('/0/reorder').put(reorderCategories);
module.exports = router;

View file

@ -1,7 +1,7 @@
class ErrorResponse extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode
this.statusCode = statusCode;
}
}

View file

@ -2,23 +2,28 @@ const { Op } = require('sequelize');
const Weather = require('../models/Weather');
const Logger = require('./Logger');
const logger = new Logger();
const loadConfig = require('./loadConfig');
const clearWeatherData = async () => {
const { WEATHER_API_KEY: secret } = await loadConfig();
const weather = await Weather.findOne({
order: [[ 'createdAt', 'DESC' ]]
order: [['createdAt', 'DESC']],
});
if (weather) {
await Weather.destroy({
where: {
id: {
[Op.lt]: weather.id
}
}
})
[Op.lt]: weather.id,
},
},
});
}
if (secret) {
logger.log('Old weather data was deleted');
}
};
module.exports = clearWeatherData;

View file

@ -20,5 +20,8 @@
"kubernetesApps": false,
"unpinStoppedApps": false,
"useAmericanDate": false,
"disableAutofocus": false
"disableAutofocus": false,
"greetingsSchema": "Good evening!;Good afternoon!;Good morning!;Good night!",
"daySchema": "Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday",
"monthSchema": "January;February;March;April;May;June;July;August;September;October;November;December"
}