├── .dev
├── DEV_GUIDELINES.md
├── bookmarks_importer.py
├── build_dev.sh
├── build_latest.sh
├── build_multiarch.sh
└── getMdi.js
├── .docker
├── Dockerfile
├── Dockerfile.dev
├── Dockerfile.multiarch
└── docker-compose.yml
├── .dockerignore
├── .env
├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
├── apps.png
├── bookmarks.png
├── home.png
├── settings.png
└── themes.png
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── Socket.js
├── Sockets.js
├── api.js
├── client
├── .env
├── .gitignore
├── package-lock.json
├── package.json
├── public
│ ├── flame.css
│ ├── icons
│ │ ├── apple-touch-icon-114x114.png
│ │ ├── apple-touch-icon-120x120.png
│ │ ├── apple-touch-icon-144x144.png
│ │ ├── apple-touch-icon-152x152.png
│ │ ├── apple-touch-icon-180x180.png
│ │ ├── apple-touch-icon-57x57.png
│ │ ├── apple-touch-icon-72x72.png
│ │ ├── apple-touch-icon-76x76.png
│ │ ├── apple-touch-icon.png
│ │ └── favicon.ico
│ ├── index.html
│ └── robots.txt
├── src
│ ├── App.tsx
│ ├── assets
│ │ └── fonts
│ │ │ └── Roboto
│ │ │ ├── roboto-v29-latin-500.woff
│ │ │ ├── roboto-v29-latin-500.woff2
│ │ │ ├── roboto-v29-latin-700.woff
│ │ │ ├── roboto-v29-latin-700.woff2
│ │ │ ├── roboto-v29-latin-900.woff
│ │ │ ├── roboto-v29-latin-900.woff2
│ │ │ ├── roboto-v29-latin-regular.woff
│ │ │ └── roboto-v29-latin-regular.woff2
│ ├── components
│ │ ├── Actions
│ │ │ ├── TableActions.module.css
│ │ │ └── TableActions.tsx
│ │ ├── Apps
│ │ │ ├── AppCard
│ │ │ │ ├── AppCard.module.css
│ │ │ │ └── AppCard.tsx
│ │ │ ├── AppForm
│ │ │ │ ├── AppForm.module.css
│ │ │ │ └── AppForm.tsx
│ │ │ ├── AppGrid
│ │ │ │ ├── AppGrid.module.css
│ │ │ │ └── AppGrid.tsx
│ │ │ ├── AppTable
│ │ │ │ └── AppTable.tsx
│ │ │ ├── Apps.module.css
│ │ │ └── Apps.tsx
│ │ ├── Bookmarks
│ │ │ ├── BookmarkCard
│ │ │ │ ├── BookmarkCard.module.css
│ │ │ │ └── BookmarkCard.tsx
│ │ │ ├── BookmarkGrid
│ │ │ │ ├── BookmarkGrid.module.css
│ │ │ │ └── BookmarkGrid.tsx
│ │ │ ├── Bookmarks.module.css
│ │ │ ├── Bookmarks.tsx
│ │ │ ├── Form
│ │ │ │ ├── BookmarksForm.tsx
│ │ │ │ ├── CategoryForm.tsx
│ │ │ │ ├── Form.module.css
│ │ │ │ └── Form.tsx
│ │ │ └── Table
│ │ │ │ ├── BookmarksTable.tsx
│ │ │ │ ├── CategoryTable.tsx
│ │ │ │ └── Table.tsx
│ │ ├── Home
│ │ │ ├── Header
│ │ │ │ ├── Header.module.css
│ │ │ │ ├── Header.tsx
│ │ │ │ └── functions
│ │ │ │ │ ├── getDateTime.ts
│ │ │ │ │ └── greeter.ts
│ │ │ ├── Home.module.css
│ │ │ └── Home.tsx
│ │ ├── NotificationCenter
│ │ │ ├── NotificationCenter.module.css
│ │ │ └── NotificationCenter.tsx
│ │ ├── Routing
│ │ │ └── ProtectedRoute.tsx
│ │ ├── SearchBar
│ │ │ ├── SearchBar.module.css
│ │ │ └── SearchBar.tsx
│ │ ├── Settings
│ │ │ ├── AppDetails
│ │ │ │ ├── AppDetails.module.css
│ │ │ │ ├── AppDetails.tsx
│ │ │ │ └── AuthForm
│ │ │ │ │ └── AuthForm.tsx
│ │ │ ├── DockerSettings
│ │ │ │ └── DockerSettings.tsx
│ │ │ ├── GeneralSettings
│ │ │ │ ├── CustomQueries
│ │ │ │ │ ├── CustomQueries.tsx
│ │ │ │ │ └── QueriesForm.tsx
│ │ │ │ └── GeneralSettings.tsx
│ │ │ ├── Settings.module.css
│ │ │ ├── Settings.tsx
│ │ │ ├── StyleSettings
│ │ │ │ └── StyleSettings.tsx
│ │ │ ├── Themer
│ │ │ │ ├── ThemeBuilder
│ │ │ │ │ ├── ThemeBuilder.module.css
│ │ │ │ │ ├── ThemeBuilder.tsx
│ │ │ │ │ ├── ThemeCreator.module.css
│ │ │ │ │ ├── ThemeCreator.tsx
│ │ │ │ │ └── ThemeEditor.tsx
│ │ │ │ ├── ThemeGrid
│ │ │ │ │ ├── ThemeGrid.module.css
│ │ │ │ │ └── ThemeGrid.tsx
│ │ │ │ ├── ThemePreview
│ │ │ │ │ ├── ThemePreview.module.css
│ │ │ │ │ └── ThemePreview.tsx
│ │ │ │ └── Themer.tsx
│ │ │ ├── UISettings
│ │ │ │ └── UISettings.tsx
│ │ │ ├── WeatherSettings
│ │ │ │ └── WeatherSettings.tsx
│ │ │ └── settings.json
│ │ ├── UI
│ │ │ ├── Buttons
│ │ │ │ ├── ActionButton
│ │ │ │ │ ├── ActionButton.module.css
│ │ │ │ │ └── ActionButton.tsx
│ │ │ │ └── Button
│ │ │ │ │ ├── Button.module.css
│ │ │ │ │ └── Button.tsx
│ │ │ ├── Forms
│ │ │ │ ├── InputGroup
│ │ │ │ │ ├── InputGroup.module.css
│ │ │ │ │ └── InputGroup.tsx
│ │ │ │ └── ModalForm
│ │ │ │ │ ├── ModalForm.module.css
│ │ │ │ │ └── ModalForm.tsx
│ │ │ ├── Headlines
│ │ │ │ ├── Headline
│ │ │ │ │ ├── Headline.module.css
│ │ │ │ │ └── Headline.tsx
│ │ │ │ ├── SectionHeadline
│ │ │ │ │ ├── SectionHeadline.module.css
│ │ │ │ │ └── SectionHeadline.tsx
│ │ │ │ └── SettingsHeadline
│ │ │ │ │ ├── SettingsHeadline.module.css
│ │ │ │ │ └── SettingsHeadline.tsx
│ │ │ ├── Icons
│ │ │ │ ├── ActionIcons
│ │ │ │ │ ├── ActionIcons.module.css
│ │ │ │ │ └── ActionIcons.tsx
│ │ │ │ ├── Icon
│ │ │ │ │ ├── Icon.module.css
│ │ │ │ │ └── Icon.tsx
│ │ │ │ └── WeatherIcon
│ │ │ │ │ ├── IconMapping.ts
│ │ │ │ │ ├── WeatherIcon.tsx
│ │ │ │ │ └── WeatherMapping.json
│ │ │ ├── Layout
│ │ │ │ ├── Layout.module.css
│ │ │ │ └── Layout.tsx
│ │ │ ├── Modal
│ │ │ │ ├── Modal.module.css
│ │ │ │ └── Modal.tsx
│ │ │ ├── Notification
│ │ │ │ ├── Notification.module.css
│ │ │ │ └── Notification.tsx
│ │ │ ├── Spinner
│ │ │ │ ├── Spinner.module.css
│ │ │ │ └── Spinner.tsx
│ │ │ ├── Tables
│ │ │ │ ├── CompactTable
│ │ │ │ │ ├── CompactTable.module.css
│ │ │ │ │ └── CompactTable.tsx
│ │ │ │ └── Table
│ │ │ │ │ ├── Table.module.css
│ │ │ │ │ └── Table.tsx
│ │ │ ├── Text
│ │ │ │ └── Message
│ │ │ │ │ ├── Message.module.css
│ │ │ │ │ └── Message.tsx
│ │ │ └── index.ts
│ │ └── Widgets
│ │ │ └── WeatherWidget
│ │ │ ├── WeatherWidget.module.css
│ │ │ └── WeatherWidget.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── interfaces
│ │ ├── Api.ts
│ │ ├── App.ts
│ │ ├── Bookmark.ts
│ │ ├── Category.ts
│ │ ├── Config.ts
│ │ ├── Forms.ts
│ │ ├── Notification.ts
│ │ ├── Query.ts
│ │ ├── Route.ts
│ │ ├── SearchResult.ts
│ │ ├── Theme.ts
│ │ ├── Weather.ts
│ │ └── index.ts
│ ├── react-app-env.d.ts
│ ├── setupProxy.js
│ ├── store
│ │ ├── action-creators
│ │ │ ├── app.ts
│ │ │ ├── auth.ts
│ │ │ ├── bookmark.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ ├── notification.ts
│ │ │ └── theme.ts
│ │ ├── action-types
│ │ │ └── index.ts
│ │ ├── actions
│ │ │ ├── app.ts
│ │ │ ├── auth.ts
│ │ │ ├── bookmark.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ ├── notification.ts
│ │ │ └── theme.ts
│ │ ├── index.ts
│ │ ├── reducers
│ │ │ ├── app.ts
│ │ │ ├── auth.ts
│ │ │ ├── bookmark.ts
│ │ │ ├── config.ts
│ │ │ ├── index.ts
│ │ │ ├── notification.ts
│ │ │ └── theme.ts
│ │ └── store.ts
│ ├── types
│ │ ├── ConfigFormData.ts
│ │ ├── WeatherData.ts
│ │ └── index.ts
│ └── utility
│ │ ├── applyAuth.ts
│ │ ├── arrayPartition.ts
│ │ ├── checkVersion.ts
│ │ ├── decodeToken.ts
│ │ ├── escapeRegex.ts
│ │ ├── iconParser.ts
│ │ ├── index.ts
│ │ ├── inputHandler.ts
│ │ ├── parseTheme.ts
│ │ ├── parseTime.ts
│ │ ├── redirectUrl.ts
│ │ ├── searchParser.ts
│ │ ├── searchQueries.json
│ │ ├── sortData.ts
│ │ ├── storeUIConfig.ts
│ │ ├── templateObjects
│ │ ├── appTemplate.ts
│ │ ├── bookmarkTemplate.ts
│ │ ├── categoryTemplate.ts
│ │ ├── configTemplate.ts
│ │ ├── index.ts
│ │ ├── settingsTemplate.ts
│ │ └── weatherTemplate.ts
│ │ ├── urlParser.ts
│ │ └── validators.ts
└── tsconfig.json
├── controllers
├── apps
│ ├── createApp.js
│ ├── deleteApp.js
│ ├── docker
│ │ ├── index.js
│ │ ├── useDocker.js
│ │ └── useKubernetes.js
│ ├── getAllApps.js
│ ├── getSingleApp.js
│ ├── index.js
│ ├── reorderApps.js
│ └── updateApp.js
├── auth
│ ├── index.js
│ ├── login.js
│ └── validate.js
├── bookmarks
│ ├── createBookmark.js
│ ├── deleteBookmark.js
│ ├── getAllBookmarks.js
│ ├── getSingleBookmark.js
│ ├── index.js
│ ├── reorderBookmarks.js
│ └── updateBookmark.js
├── categories
│ ├── createCategory.js
│ ├── deleteCategory.js
│ ├── getAllCategories.js
│ ├── getSingleCategory.js
│ ├── index.js
│ ├── reorderCategories.js
│ └── updateCategory.js
├── config
│ ├── getCSS.js
│ ├── getConfig.js
│ ├── index.js
│ ├── updateCSS.js
│ └── updateConfig.js
├── queries
│ ├── addQuery.js
│ ├── deleteQuery.js
│ ├── getQueries.js
│ ├── index.js
│ └── updateQuery.js
├── themes
│ ├── addTheme.js
│ ├── deleteTheme.js
│ ├── getThemes.js
│ ├── index.js
│ └── updateTheme.js
└── weather
│ ├── getWather.js
│ ├── index.js
│ └── updateWeather.js
├── db
├── index.js
├── migrations
│ ├── 00_initial.js
│ ├── 01_new-config.js
│ ├── 02_resource-access.js
│ ├── 03_weather.js
│ ├── 04_bookmarks-order.js
│ └── 05_app-description.js
└── utils
│ ├── backupDb.js
│ └── slugify.js
├── k8s
├── base
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ ├── rbac.yaml
│ └── service.yaml
└── overlays
│ └── shokohsc
│ ├── deployment.yaml
│ ├── ingress.yaml
│ ├── kustomization.yaml
│ ├── namespace.yaml
│ ├── rbac.yaml
│ └── service.yaml
├── middleware
├── asyncWrapper.js
├── auth.js
├── errorHandler.js
├── index.js
├── multer.js
├── requireAuth.js
└── requireBody.js
├── models
├── App.js
├── Bookmark.js
├── Category.js
├── Config.js
├── Weather.js
└── associateModels.js
├── package-lock.json
├── package.json
├── routes
├── apps.js
├── auth.js
├── bookmark.js
├── category.js
├── config.js
├── queries.js
├── themes.js
└── weather.js
├── server.js
├── skaffold.yaml
└── utils
├── ErrorResponse.js
├── File.js
├── Logger.js
├── checkFileExists.js
├── clearWeatherData.js
├── getExternalWeather.js
├── init
├── createFile.js
├── index.js
├── initConfig.js
├── initDockerSecrets.js
├── initFiles.js
├── initialConfig.json
├── initialFiles.json
├── normalizeTheme.js
└── themes.json
├── jobs.js
├── loadConfig.js
└── signToken.js
/.dev/DEV_GUIDELINES.md:
--------------------------------------------------------------------------------
1 | ## Adding new config key
2 |
3 | 1. Edit utils/init/initialConfig.json
4 | 2. Edit client/src/interfaces/Config.ts
5 | 3. Edit client/src/utility/templateObjects/configTemplate.ts
6 |
7 | If config value will be used in a form:
8 |
9 | 4. Edit client/src/interfaces/Forms.ts
10 | 5. Edit client/src/utility/templateObjects/settingsTemplate.ts
--------------------------------------------------------------------------------
/.dev/build_dev.sh:
--------------------------------------------------------------------------------
1 | docker build -t flame:dev -f .docker/Dockerfile .
--------------------------------------------------------------------------------
/.dev/build_latest.sh:
--------------------------------------------------------------------------------
1 | docker build -t pawelmalak/flame -t "pawelmalak/flame:$1" -f .docker/Dockerfile . \
2 | && docker push pawelmalak/flame && docker push "pawelmalak/flame:$1"
--------------------------------------------------------------------------------
/.dev/build_multiarch.sh:
--------------------------------------------------------------------------------
1 | docker buildx build \
2 | --platform linux/arm/v7,linux/arm64,linux/amd64 \
3 | -f .docker/Dockerfile.multiarch \
4 | -t pawelmalak/flame:multiarch \
5 | -t "pawelmalak/flame:multiarch$1" \
6 | --push .
--------------------------------------------------------------------------------
/.dev/getMdi.js:
--------------------------------------------------------------------------------
1 | // Script to get all icon names from materialdesignicons.com
2 | const getMdi = () => {
3 | const icons = document.querySelectorAll('#icons div span');
4 | const names = [...icons].map((icon) => icon.textContent.replace('mdi-', ''));
5 | const output = names.map((name) => ({ name }));
6 | output.pop();
7 | const json = JSON.stringify(output);
8 | console.log(json);
9 | };
10 |
--------------------------------------------------------------------------------
/.docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16 as builder
2 |
3 | WORKDIR /app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install --production
8 |
9 | COPY . .
10 |
11 | RUN mkdir -p ./public ./data \
12 | && cd ./client \
13 | && npm install --production \
14 | && npm run build \
15 | && cd .. \
16 | && mv ./client/build/* ./public \
17 | && rm -rf ./client
18 |
19 | FROM node:16-alpine
20 |
21 | COPY --from=builder /app /app
22 |
23 | WORKDIR /app
24 |
25 | EXPOSE 5005
26 |
27 | ENV NODE_ENV=production
28 | ENV PASSWORD=flame_password
29 |
30 | CMD ["sh", "-c", "chown -R node /app/data && node server.js"]
--------------------------------------------------------------------------------
/.docker/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine as build-front
2 |
3 | RUN apk add --no-cache curl
4 |
5 | WORKDIR /app
6 |
7 | COPY ./client .
8 |
9 | RUN npm install --production \
10 | && npm run build
11 |
12 | FROM node:lts-alpine
13 |
14 | WORKDIR /app
15 |
16 | RUN mkdir -p ./public
17 |
18 | COPY --from=build-front /app/build/ ./public
19 |
20 | COPY package*.json ./
21 |
22 | RUN npm install
23 |
24 | COPY . .
25 |
26 | CMD ["npm", "run", "skaffold"]
--------------------------------------------------------------------------------
/.docker/Dockerfile.multiarch:
--------------------------------------------------------------------------------
1 | FROM node:16-alpine3.11 as builder
2 |
3 | WORKDIR /app
4 |
5 | COPY package*.json ./
6 |
7 | RUN apk --no-cache --virtual build-dependencies add python python3 make g++ \
8 | && npm install --production
9 |
10 | COPY . .
11 |
12 | RUN mkdir -p ./public ./data \
13 | && cd ./client \
14 | && npm install --production \
15 | && npm run build \
16 | && cd .. \
17 | && mv ./client/build/* ./public \
18 | && rm -rf ./client
19 |
20 | FROM node:16-alpine3.11
21 |
22 | COPY --from=builder /app /app
23 |
24 | WORKDIR /app
25 |
26 | EXPOSE 5005
27 |
28 | ENV NODE_ENV=production
29 | ENV PASSWORD=flame_password
30 |
31 | CMD ["sh", "-c", "chown -R node /app/data && node server.js"]
--------------------------------------------------------------------------------
/.docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.6'
2 |
3 | services:
4 | flame:
5 | image: pawelmalak/flame
6 | container_name: flame
7 | volumes:
8 | - /path/to/host/data:/app/data
9 | # - /var/run/docker.sock:/var/run/docker.sock # optional but required for Docker integration
10 | ports:
11 | - 5005:5005
12 | # secrets:
13 | # - password # optional but required for (1)
14 | environment:
15 | - PASSWORD=flame_password
16 | # - PASSWORD_FILE=/run/secrets/password # optional but required for (1)
17 | restart: unless-stopped
18 |
19 | # optional but required for Docker secrets (1)
20 | # secrets:
21 | # password:
22 | # file: /path/to/secrets/password
23 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .github
3 | public
4 | k8s
5 | skaffold.yaml
6 | data
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | PORT=5005
2 | NODE_ENV=development
3 | VERSION=2.3.1
4 | PASSWORD=flame_password
5 | SECRET=e02eb43d69953658c6d07311d6313f2d4467672cb881f96b29368ba1f3f4da4b
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a bug report
4 | title: "[BUG] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Deployment details:**
11 | - App version [e.g. v1.7.4]:
12 | - Platform [e.g. amd64, arm64, arm/v7]:
13 | - Docker image tag [e.g. latest, multiarch]:
14 |
15 | ---
16 |
17 | **Bug description:**
18 |
19 | A clear and concise description of what the bug is.
20 |
21 | ---
22 |
23 | **Steps to reproduce:**
24 |
25 | 1. Go to '...'
26 | 2. Click on '....'
27 | 3. Scroll down to '....'
28 |
--------------------------------------------------------------------------------
/.github/apps.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/.github/apps.png
--------------------------------------------------------------------------------
/.github/bookmarks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/.github/bookmarks.png
--------------------------------------------------------------------------------
/.github/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/.github/home.png
--------------------------------------------------------------------------------
/.github/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/.github/settings.png
--------------------------------------------------------------------------------
/.github/themes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/.github/themes.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | data
3 | public
4 | !client/public
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.md
2 | docker-compose.yml
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "singleQuote": true,
5 | "arrowParens": "always",
6 | "printWidth": 80,
7 | "trailingComma": "es5"
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Paweł Malak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Socket.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws');
2 | const Logger = require('./utils/Logger');
3 | const logger = new Logger();
4 |
5 | class Socket {
6 | constructor(server) {
7 | this.webSocketServer = new WebSocket.Server({ server })
8 |
9 | this.webSocketServer.on('listening', () => {
10 | logger.log('Socket: listen');
11 | })
12 |
13 | this.webSocketServer.on('connection', (webSocketClient) => {
14 | // console.log('Socket: new connection');
15 | })
16 | }
17 |
18 | send(msg) {
19 | this.webSocketServer.clients.forEach(client => client.send(msg));
20 | }
21 | }
22 |
23 | module.exports = Socket;
--------------------------------------------------------------------------------
/Sockets.js:
--------------------------------------------------------------------------------
1 | class Sockets {
2 | constructor() {
3 | this.sockets = [];
4 | }
5 |
6 | registerSocket(name, socket) {
7 | this.sockets.push({ name, socket });
8 | }
9 |
10 | getAllSockets() {
11 | return this.sockets;
12 | }
13 |
14 | getSocket(name) {
15 | const socket = this.sockets.find(socket => socket.name === name);
16 | return socket;
17 | }
18 | }
19 |
20 | module.exports = new Sockets();
--------------------------------------------------------------------------------
/api.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 | const express = require('express');
3 | const { errorHandler } = require('./middleware');
4 |
5 | const api = express();
6 |
7 | // Static files
8 | api.use(express.static(join(__dirname, 'public')));
9 | api.use('/uploads', express.static(join(__dirname, 'data/uploads')));
10 | api.get(/^\/(?!api)/, (req, res) => {
11 | res.sendFile(join(__dirname, 'public/index.html'));
12 | });
13 |
14 | // Body parser
15 | api.use(express.json());
16 |
17 | // Link controllers with routes
18 | api.use('/api/apps', require('./routes/apps'));
19 | api.use('/api/config', require('./routes/config'));
20 | api.use('/api/weather', require('./routes/weather'));
21 | api.use('/api/categories', require('./routes/category'));
22 | api.use('/api/bookmarks', require('./routes/bookmark'));
23 | api.use('/api/queries', require('./routes/queries'));
24 | api.use('/api/auth', require('./routes/auth'));
25 | api.use('/api/themes', require('./routes/themes'));
26 |
27 | // Custom error handler
28 | api.use(errorHandler);
29 |
30 | module.exports = api;
31 |
--------------------------------------------------------------------------------
/client/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_VERSION=2.3.1
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@mdi/js": "^6.4.95",
7 | "@mdi/react": "^1.5.0",
8 | "@testing-library/jest-dom": "^5.15.0",
9 | "@testing-library/react": "^12.1.2",
10 | "@testing-library/user-event": "^13.5.0",
11 | "@types/jest": "^27.0.2",
12 | "@types/node": "^16.11.6",
13 | "@types/react": "^17.0.34",
14 | "@types/react-beautiful-dnd": "^13.1.2",
15 | "@types/react-dom": "^17.0.11",
16 | "@types/react-redux": "^7.1.20",
17 | "@types/react-router-dom": "^5.1.7",
18 | "axios": "^0.24.0",
19 | "external-svg-loader": "^1.3.4",
20 | "http-proxy-middleware": "^2.0.1",
21 | "jwt-decode": "^3.1.2",
22 | "react": "^17.0.2",
23 | "react-beautiful-dnd": "^13.1.0",
24 | "react-dom": "^17.0.2",
25 | "react-redux": "^7.2.6",
26 | "react-router-dom": "^5.2.0",
27 | "react-scripts": "^5.0.1",
28 | "redux": "^4.1.2",
29 | "redux-devtools-extension": "^2.13.9",
30 | "redux-thunk": "^2.4.0",
31 | "skycons-ts": "^0.2.0",
32 | "typescript": "^4.4.4",
33 | "web-vitals": "^2.1.2"
34 | },
35 | "scripts": {
36 | "start": "react-scripts start",
37 | "build": "react-scripts build",
38 | "test": "react-scripts test",
39 | "eject": "react-scripts eject"
40 | },
41 | "eslintConfig": {
42 | "extends": [
43 | "react-app",
44 | "react-app/jest"
45 | ]
46 | },
47 | "browserslist": {
48 | "production": [
49 | ">0.2%",
50 | "not dead",
51 | "not op_mini all"
52 | ],
53 | "development": [
54 | "last 1 chrome version",
55 | "last 1 firefox version",
56 | "last 1 safari version"
57 | ]
58 | },
59 | "devDependencies": {
60 | "prettier": "^2.4.1"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/client/public/flame.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/flame.css
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/client/public/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/public/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/public/icons/favicon.ico
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
51 |
55 |
56 | Flame
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-500.woff2
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-700.woff2
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-900.woff2
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff
--------------------------------------------------------------------------------
/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pawelmalak/flame/3c347c854c4c55456785ff026a703422d8f02f62/client/src/assets/fonts/Roboto/roboto-v29-latin-regular.woff2
--------------------------------------------------------------------------------
/client/src/components/Actions/TableActions.module.css:
--------------------------------------------------------------------------------
1 | .TableActions {
2 | display: flex;
3 | align-items: center;
4 | }
5 |
6 | .TableAction {
7 | width: 22px;
8 | }
9 |
10 | .TableAction:hover {
11 | cursor: pointer;
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/components/Actions/TableActions.tsx:
--------------------------------------------------------------------------------
1 | import { Icon } from '../UI';
2 | import classes from './TableActions.module.css';
3 |
4 | interface Entity {
5 | id: number;
6 | name: string;
7 | isPinned?: boolean;
8 | isPublic: boolean;
9 | }
10 |
11 | interface Props {
12 | entity: Entity;
13 | deleteHandler: (id: number, name: string) => void;
14 | updateHandler: (id: number) => void;
15 | pinHanlder?: (id: number) => void;
16 | changeVisibilty: (id: number) => void;
17 | showPin?: boolean;
18 | }
19 |
20 | export const TableActions = (props: Props): JSX.Element => {
21 | const {
22 | entity,
23 | deleteHandler,
24 | updateHandler,
25 | pinHanlder,
26 | changeVisibilty,
27 | showPin = true,
28 | } = props;
29 |
30 | const _pinHandler = pinHanlder || function () {};
31 |
32 | return (
33 |
34 | {/* DELETE */}
35 | deleteHandler(entity.id, entity.name)}
38 | tabIndex={0}
39 | >
40 |
41 |
42 |
43 | {/* UPDATE */}
44 | updateHandler(entity.id)}
47 | tabIndex={0}
48 | >
49 |
50 |
51 |
52 | {/* PIN */}
53 | {showPin && (
54 | _pinHandler(entity.id)}
57 | tabIndex={0}
58 | >
59 | {entity.isPinned ? (
60 |
61 | ) : (
62 |
63 | )}
64 |
65 | )}
66 |
67 | {/* VISIBILITY */}
68 | changeVisibilty(entity.id)}
71 | tabIndex={0}
72 | >
73 | {entity.isPublic ? (
74 |
75 | ) : (
76 |
77 | )}
78 |
79 | |
80 | );
81 | };
82 |
--------------------------------------------------------------------------------
/client/src/components/Apps/AppCard/AppCard.module.css:
--------------------------------------------------------------------------------
1 | .AppCard {
2 | width: 100%;
3 | display: flex;
4 | align-items: center;
5 | margin-bottom: 20px;
6 | }
7 |
8 | .AppCardIcon {
9 | width: 35px;
10 | height: 35px;
11 | margin-right: 0.5em;
12 | }
13 |
14 | .AppCardDetails {
15 | text-transform: uppercase;
16 | }
17 |
18 | .AppCardDetails h5 {
19 | font-size: 1em;
20 | font-weight: 500;
21 | color: var(--color-primary);
22 | margin-bottom: -4px;
23 | }
24 |
25 | .AppCardDetails span {
26 | color: var(--color-accent);
27 | font-weight: 400;
28 | font-size: 0.8em;
29 | opacity: 1;
30 | }
31 |
32 | @media (min-width: 500px) {
33 | .AppCard {
34 | padding: 2px;
35 | border-radius: 4px;
36 | transition: all 0.1s;
37 | }
38 |
39 | .AppCard:hover {
40 | background-color: rgba(0, 0, 0, 0.2);
41 | }
42 | }
43 |
44 | .CustomIcon {
45 | width: 90%;
46 | height: 90%;
47 | margin-top: 2px;
48 | margin-left: 2px;
49 | object-fit: contain;
50 | }
51 |
--------------------------------------------------------------------------------
/client/src/components/Apps/AppCard/AppCard.tsx:
--------------------------------------------------------------------------------
1 | import classes from './AppCard.module.css';
2 | import { Icon } from '../../UI';
3 | import { iconParser, isImage, isSvg, isUrl, urlParser } from '../../../utility';
4 |
5 | import { App } from '../../../interfaces';
6 | import { useSelector } from 'react-redux';
7 | import { State } from '../../../store/reducers';
8 |
9 | interface Props {
10 | app: App;
11 | }
12 |
13 | export const AppCard = ({ app }: Props): JSX.Element => {
14 | const { config } = useSelector((state: State) => state.config);
15 |
16 | const [displayUrl, redirectUrl] = urlParser(app.url);
17 |
18 | let iconEl: JSX.Element;
19 | const { icon } = app;
20 |
21 | if (isImage(icon)) {
22 | const source = isUrl(icon) ? icon : `/uploads/${icon}`;
23 |
24 | iconEl = (
25 |
30 | );
31 | } else if (isSvg(icon)) {
32 | const source = isUrl(icon) ? icon : `/uploads/${icon}`;
33 |
34 | iconEl = (
35 |
36 |
41 |
42 | );
43 | } else {
44 | iconEl = ;
45 | }
46 |
47 | return (
48 |
54 | {iconEl}
55 |
56 |
{app.name}
57 | {!app.description.length ? displayUrl : app.description}
58 |
59 |
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/client/src/components/Apps/AppForm/AppForm.module.css:
--------------------------------------------------------------------------------
1 | .Switch {
2 | text-decoration: underline;
3 | }
4 |
5 | .Switch:hover {
6 | cursor: pointer;
7 | }
--------------------------------------------------------------------------------
/client/src/components/Apps/AppGrid/AppGrid.module.css:
--------------------------------------------------------------------------------
1 | .AppGrid {
2 | display: grid;
3 | grid-template-columns: repeat(1, 1fr);
4 | }
5 |
6 | @media (min-width: 430px) {
7 | .AppGrid {
8 | grid-template-columns: repeat(2, 1fr);
9 | }
10 | }
11 |
12 | @media (min-width: 670px) {
13 | .AppGrid {
14 | grid-template-columns: repeat(3, 1fr);
15 | }
16 | }
17 |
18 | @media (min-width: 900px) {
19 | .AppGrid {
20 | grid-template-columns: repeat(4, 1fr);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/components/Apps/AppGrid/AppGrid.tsx:
--------------------------------------------------------------------------------
1 | import classes from './AppGrid.module.css';
2 | import { Link } from 'react-router-dom';
3 | import { App } from '../../../interfaces/App';
4 |
5 | import { AppCard } from '../AppCard/AppCard';
6 | import { Message } from '../../UI';
7 |
8 | interface Props {
9 | apps: App[];
10 | totalApps?: number;
11 | searching: boolean;
12 | }
13 |
14 | export const AppGrid = (props: Props): JSX.Element => {
15 | let apps: JSX.Element;
16 |
17 | if (props.searching || props.apps.length) {
18 | if (!props.apps.length) {
19 | apps = No apps match your search criteria;
20 | } else {
21 | apps = (
22 |
23 | {props.apps.map((app: App): JSX.Element => {
24 | return
;
25 | })}
26 |
27 | );
28 | }
29 | } else {
30 | if (props.totalApps) {
31 | apps = (
32 |
33 | There are no pinned applications. You can pin them from the{' '}
34 | /applications menu
35 |
36 | );
37 | } else {
38 | apps = (
39 |
40 | You don't have any applications. You can add a new one from{' '}
41 | /applications menu
42 |
43 | );
44 | }
45 | }
46 |
47 | return apps;
48 | };
49 |
--------------------------------------------------------------------------------
/client/src/components/Apps/Apps.module.css:
--------------------------------------------------------------------------------
1 | .ActionsContainer {
2 | display: flex;
3 | align-items: center;
4 | }
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.module.css:
--------------------------------------------------------------------------------
1 | .BookmarkCard {
2 | margin-bottom: 30px;
3 | }
4 |
5 | .BookmarkCard h3 {
6 | color: var(--color-accent);
7 | margin-bottom: 10px;
8 | font-size: 16px;
9 | font-weight: 400;
10 | text-transform: uppercase;
11 | }
12 |
13 | .BookmarkHeader:hover {
14 | cursor: pointer;
15 | }
16 |
17 | .Bookmarks {
18 | display: flex;
19 | flex-direction: column;
20 | }
21 |
22 | .Bookmarks a {
23 | line-height: 2;
24 | transition: all 0.25s;
25 | display: flex;
26 | }
27 |
28 | .BookmarkCard a:hover {
29 | text-decoration: underline;
30 | padding-left: 10px;
31 | }
32 |
33 | .BookmarkIcon {
34 | width: 20px;
35 | height: 20px;
36 | display: flex;
37 | margin-top: 3px;
38 | margin-right: 2px;
39 | justify-content: center;
40 | align-items: center;
41 | }
42 |
43 | .BookmarkIconSvg {
44 | width: 80%;
45 | height: 80%;
46 | margin-top: 2px;
47 | margin-left: 2px;
48 | object-fit: contain;
49 | }
50 |
51 | .CustomIcon {
52 | width: 90%;
53 | height: 90%;
54 | margin-top: 2px;
55 | object-fit: contain;
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.module.css:
--------------------------------------------------------------------------------
1 | .BookmarkGrid {
2 | display: grid;
3 | grid-template-columns: repeat(1, 1fr);
4 | }
5 |
6 | @media (min-width: 430px) {
7 | .BookmarkGrid {
8 | grid-template-columns: repeat(2, 1fr);
9 | }
10 | }
11 |
12 | @media (min-width: 670px) {
13 | .BookmarkGrid {
14 | grid-template-columns: repeat(3, 1fr);
15 | }
16 | }
17 |
18 | @media (min-width: 900px) {
19 | .BookmarkGrid {
20 | grid-template-columns: repeat(4, 1fr);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/BookmarkGrid/BookmarkGrid.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import classes from './BookmarkGrid.module.css';
4 |
5 | import { Category } from '../../../interfaces';
6 |
7 | import { BookmarkCard } from '../BookmarkCard/BookmarkCard';
8 | import { Message } from '../../UI';
9 |
10 | interface Props {
11 | categories: Category[];
12 | totalCategories?: number;
13 | searching: boolean;
14 | fromHomepage?: boolean;
15 | }
16 |
17 | export const BookmarkGrid = (props: Props): JSX.Element => {
18 | const {
19 | categories,
20 | totalCategories,
21 | searching,
22 | fromHomepage = false,
23 | } = props;
24 |
25 | let bookmarks: JSX.Element;
26 |
27 | if (categories.length) {
28 | if (searching && !categories[0].bookmarks.length) {
29 | bookmarks = No bookmarks match your search criteria;
30 | } else {
31 | bookmarks = (
32 |
33 | {categories.map(
34 | (category: Category): JSX.Element => (
35 |
40 | )
41 | )}
42 |
43 | );
44 | }
45 | } else {
46 | if (totalCategories) {
47 | bookmarks = (
48 |
49 | There are no pinned categories. You can pin them from the{' '}
50 | /bookmarks menu
51 |
52 | );
53 | } else {
54 | bookmarks = (
55 |
56 | You don't have any bookmarks. You can add a new one from{' '}
57 | /bookmarks menu
58 |
59 | );
60 | }
61 | }
62 |
63 | return bookmarks;
64 | };
65 |
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/Bookmarks.module.css:
--------------------------------------------------------------------------------
1 | .ActionsContainer {
2 | display: flex;
3 | align-items: center;
4 | }
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/Form/Form.module.css:
--------------------------------------------------------------------------------
1 | .Switch {
2 | text-decoration: underline;
3 | }
4 |
5 | .Switch:hover {
6 | cursor: pointer;
7 | }
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/Form/Form.tsx:
--------------------------------------------------------------------------------
1 | // Typescript
2 | import { ContentType } from '../Bookmarks';
3 |
4 | // Utils
5 | import { CategoryForm } from './CategoryForm';
6 | import { BookmarksForm } from './BookmarksForm';
7 | import { Fragment } from 'react';
8 | import { useSelector } from 'react-redux';
9 | import { State } from '../../../store/reducers';
10 | import { bookmarkTemplate, categoryTemplate } from '../../../utility';
11 |
12 | interface Props {
13 | modalHandler: () => void;
14 | contentType: ContentType;
15 | inUpdate?: boolean;
16 | }
17 |
18 | export const Form = (props: Props): JSX.Element => {
19 | const { categoryInEdit, bookmarkInEdit } = useSelector(
20 | (state: State) => state.bookmarks
21 | );
22 |
23 | const { modalHandler, contentType, inUpdate } = props;
24 |
25 | return (
26 |
27 | {!inUpdate ? (
28 | // form: add new
29 |
30 | {contentType === ContentType.category ? (
31 |
32 | ) : (
33 |
34 | )}
35 |
36 | ) : (
37 | // form: update
38 |
39 | {contentType === ContentType.category ? (
40 |
44 | ) : (
45 |
49 | )}
50 |
51 | )}
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/client/src/components/Bookmarks/Table/Table.tsx:
--------------------------------------------------------------------------------
1 | import { Category, Bookmark } from '../../../interfaces';
2 | import { ContentType } from '../Bookmarks';
3 | import { BookmarksTable } from './BookmarksTable';
4 | import { CategoryTable } from './CategoryTable';
5 |
6 | interface Props {
7 | contentType: ContentType;
8 | openFormForUpdating: (data: Category | Bookmark) => void;
9 | }
10 |
11 | export const Table = (props: Props): JSX.Element => {
12 | const tableEl =
13 | props.contentType === ContentType.category ? (
14 |
15 | ) : (
16 |
17 | );
18 |
19 | return tableEl;
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/components/Home/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .Header h1 {
2 | color: var(--color-primary);
3 | font-weight: 700;
4 | font-size: 4em;
5 | display: inline-block;
6 | }
7 |
8 | .Header p {
9 | color: var(--color-primary);
10 | font-weight: 300;
11 | text-transform: uppercase;
12 | height: 30px;
13 | }
14 |
15 | .HeaderMain {
16 | display: flex;
17 | justify-content: space-between;
18 | align-items: center;
19 | margin-bottom: 2.5rem;
20 | }
21 |
22 | .SettingsLink {
23 | visibility: visible;
24 | color: var(--color-accent);
25 | }
26 |
27 | @media (min-width: 769px) {
28 | .SettingsLink {
29 | visibility: hidden;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/components/Home/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | // Redux
5 | import { useSelector } from 'react-redux';
6 | import { State } from '../../../store/reducers';
7 |
8 | // CSS
9 | import classes from './Header.module.css';
10 |
11 | // Components
12 | import { WeatherWidget } from '../../Widgets/WeatherWidget/WeatherWidget';
13 |
14 | // Utils
15 | import { getDateTime } from './functions/getDateTime';
16 | import { greeter } from './functions/greeter';
17 |
18 | export const Header = (): JSX.Element => {
19 | const { hideHeader, hideDate, showTime } = useSelector(
20 | (state: State) => state.config.config
21 | );
22 |
23 | const [dateTime, setDateTime] = useState(getDateTime());
24 | const [greeting, setGreeting] = useState(greeter());
25 |
26 | useEffect(() => {
27 | let dateTimeInterval: NodeJS.Timeout;
28 |
29 | dateTimeInterval = setInterval(() => {
30 | setDateTime(getDateTime());
31 | setGreeting(greeter());
32 | }, 1000);
33 |
34 | return () => window.clearInterval(dateTimeInterval);
35 | }, []);
36 |
37 | return (
38 |
39 | {(!hideDate || showTime) && {dateTime}
}
40 |
41 |
42 | Go to Settings
43 |
44 |
45 | {!hideHeader && (
46 |
47 | {greeting}
48 |
49 |
50 | )}
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/client/src/components/Home/Header/functions/getDateTime.ts:
--------------------------------------------------------------------------------
1 | import { parseTime } from '../../../../utility';
2 |
3 | export const getDateTime = (): string => {
4 | const days = localStorage.getItem('daySchema')?.split(';') || [
5 | 'Sunday',
6 | 'Monday',
7 | 'Tuesday',
8 | 'Wednesday',
9 | 'Thursday',
10 | 'Friday',
11 | 'Saturday',
12 | ];
13 |
14 | const months = localStorage.getItem('monthSchema')?.split(';') || [
15 | 'January',
16 | 'February',
17 | 'March',
18 | 'April',
19 | 'May',
20 | 'June',
21 | 'July',
22 | 'August',
23 | 'September',
24 | 'October',
25 | 'November',
26 | 'December',
27 | ];
28 |
29 | const now = new Date();
30 |
31 | const useAmericanDate = localStorage.useAmericanDate === 'true';
32 | const showTime = localStorage.showTime === 'true';
33 | const hideDate = localStorage.hideDate === 'true';
34 |
35 | // Date
36 | let dateEl = '';
37 |
38 | if (!hideDate) {
39 | if (!useAmericanDate) {
40 | dateEl = `${days[now.getDay()]}, ${now.getDate()} ${
41 | months[now.getMonth()]
42 | } ${now.getFullYear()}`;
43 | } else {
44 | dateEl = `${days[now.getDay()]}, ${
45 | months[now.getMonth()]
46 | } ${now.getDate()} ${now.getFullYear()}`;
47 | }
48 | }
49 |
50 | // Time
51 | const p = parseTime;
52 | let timeEl = '';
53 |
54 | if (showTime) {
55 | const time = `${p(now.getHours())}:${p(now.getMinutes())}:${p(
56 | now.getSeconds()
57 | )}`;
58 |
59 | timeEl = time;
60 | }
61 |
62 | // Separator
63 | let separator = '';
64 |
65 | if (!hideDate && showTime) {
66 | separator = ' - ';
67 | }
68 |
69 | // Output
70 | return `${dateEl}${separator}${timeEl}`;
71 | };
72 |
--------------------------------------------------------------------------------
/client/src/components/Home/Header/functions/greeter.ts:
--------------------------------------------------------------------------------
1 | export const greeter = (): string => {
2 | const now = new Date().getHours();
3 | let msg: string;
4 |
5 | const greetingsSchemaRaw =
6 | localStorage.getItem('greetingsSchema') ||
7 | 'Good evening!;Good afternoon!;Good morning!;Good night!';
8 | const greetingsSchema = greetingsSchemaRaw.split(';');
9 |
10 | if (now >= 18) msg = greetingsSchema[0];
11 | else if (now >= 12) msg = greetingsSchema[1];
12 | else if (now >= 6) msg = greetingsSchema[2];
13 | else if (now >= 0) msg = greetingsSchema[3];
14 | else msg = 'Hello!';
15 |
16 | return msg;
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/Home/Home.module.css:
--------------------------------------------------------------------------------
1 | .SettingsButton {
2 | width: 35px;
3 | height: 35px;
4 | background-color: var(--color-accent);
5 | border-radius: 50%;
6 | position: fixed;
7 | bottom: var(--spacing-ui);
8 | left: var(--spacing-ui);
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | opacity: 0.25;
13 | transition: all 0.3s;
14 | visibility: hidden;
15 | }
16 |
17 | .SettingsButton:hover {
18 | cursor: pointer;
19 | opacity: 1;
20 | }
21 |
22 | @media (min-width: 769px) {
23 | .SettingsButton {
24 | visibility: visible;
25 | }
26 | }
27 |
28 | .HomeSpace {
29 | height: 20px;
30 | }
31 |
--------------------------------------------------------------------------------
/client/src/components/NotificationCenter/NotificationCenter.module.css:
--------------------------------------------------------------------------------
1 | .NotificationCenter {
2 | position: fixed;
3 | right: var(--spacing-ui);
4 | bottom: var(--spacing-ui);
5 | max-width: 300px;
6 | z-index: 500;
7 | color: white;
8 | }
--------------------------------------------------------------------------------
/client/src/components/NotificationCenter/NotificationCenter.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Notification as NotificationInterface } from '../../interfaces';
3 |
4 | import classes from './NotificationCenter.module.css';
5 |
6 | import { Notification } from '../UI';
7 | import { State } from '../../store/reducers';
8 |
9 | export const NotificationCenter = (): JSX.Element => {
10 | const { notifications } = useSelector((state: State) => state.notification);
11 |
12 | return (
13 |
17 | {notifications.map((notification: NotificationInterface) => {
18 | return (
19 |
26 | );
27 | })}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/client/src/components/Routing/ProtectedRoute.tsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Redirect, Route, RouteProps } from 'react-router';
3 | import { State } from '../../store/reducers';
4 |
5 | export const ProtectedRoute = ({ ...rest }: RouteProps) => {
6 | const { isAuthenticated } = useSelector((state: State) => state.auth);
7 |
8 | if (isAuthenticated) {
9 | return ;
10 | } else {
11 | return ;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/client/src/components/SearchBar/SearchBar.module.css:
--------------------------------------------------------------------------------
1 | .SearchBar {
2 | width: 100%;
3 | padding: 10px 0;
4 | color: var(--color-primary);
5 | /* font-size: 20px; */
6 | margin-bottom: 20px;
7 | background-color: transparent;
8 | border: none;
9 | border-bottom: 2px solid var(--color-accent);
10 | opacity: 0.5;
11 | transition: all 0.2s;
12 | border-radius: 0px;
13 | }
14 |
15 | .SearchBar:focus {
16 | opacity: 1;
17 | outline: none;
18 | }
--------------------------------------------------------------------------------
/client/src/components/Settings/AppDetails/AppDetails.module.css:
--------------------------------------------------------------------------------
1 | .text {
2 | color: var(--color-primary);
3 | margin-bottom: 15px;
4 | }
5 |
6 | .text a,
7 | .text span {
8 | color: var(--color-accent);
9 | }
10 |
11 | .separator {
12 | margin: 30px 0;
13 | border: 1px solid var(--color-primary);
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/components/Settings/AppDetails/AppDetails.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 |
3 | // UI
4 | import { Button, SettingsHeadline } from '../../UI';
5 | import { AuthForm } from './AuthForm/AuthForm';
6 | import classes from './AppDetails.module.css';
7 |
8 | // Store
9 | import { useSelector } from 'react-redux';
10 | import { State } from '../../../store/reducers';
11 |
12 | // Other
13 | import { checkVersion } from '../../../utility';
14 |
15 | export const AppDetails = (): JSX.Element => {
16 | const { isAuthenticated } = useSelector((state: State) => state.auth);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | {isAuthenticated && (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 | Flame
36 | {' '}
37 | version {process.env.REACT_APP_VERSION}
38 |
39 |
40 |
41 | See changelog{' '}
42 |
47 | here
48 |
49 |
50 |
51 |
52 |
53 |
54 | )}
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Settings.module.css:
--------------------------------------------------------------------------------
1 | .Settings {
2 | width: 100%;
3 | display: grid;
4 | grid-template-columns: 1fr;
5 | }
6 |
7 | .SettingsNav {
8 | display: flex;
9 | flex-direction: column;
10 | }
11 |
12 | .SettingsNavLink {
13 | padding-left: 7px;
14 | border-left: 3px solid transparent;
15 | display: flex;
16 | align-items: center;
17 | height: 40px;
18 | transition: all 0.3s;
19 | }
20 |
21 | .SettingsNavLink:hover,
22 | .SettingsNavLink:focus {
23 | border-left: 3px solid var(--color-primary);
24 | }
25 |
26 | .SettingsNavLinkActive {
27 | border-left: 3px solid var(--color-primary);
28 | }
29 |
30 | @media (min-width: 480px) {
31 | .Settings {
32 | grid-template-columns: 1fr 2fr;
33 | }
34 | }
35 |
36 | @media (min-width: 500px) {
37 | .Settings {
38 | grid-template-columns: 1fr 3fr;
39 | }
40 | }
--------------------------------------------------------------------------------
/client/src/components/Settings/StyleSettings/StyleSettings.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, ChangeEvent, FormEvent } from 'react';
2 | import axios from 'axios';
3 |
4 | // Redux
5 | import { useDispatch } from 'react-redux';
6 | import { bindActionCreators } from 'redux';
7 | import { actionCreators } from '../../../store';
8 |
9 | // Typescript
10 | import { ApiResponse } from '../../../interfaces';
11 |
12 | // Other
13 | import { InputGroup, Button } from '../../UI';
14 | import { applyAuth } from '../../../utility';
15 |
16 | export const StyleSettings = (): JSX.Element => {
17 | const dispatch = useDispatch();
18 | const { createNotification } = bindActionCreators(actionCreators, dispatch);
19 |
20 | const [customStyles, setCustomStyles] = useState('');
21 |
22 | useEffect(() => {
23 | axios
24 | .get>('/api/config/0/css')
25 | .then((data) => setCustomStyles(data.data.data))
26 | .catch((err) => console.log(err.response));
27 | }, []);
28 |
29 | const inputChangeHandler = (e: ChangeEvent) => {
30 | e.preventDefault();
31 | setCustomStyles(e.target.value);
32 | };
33 |
34 | const formSubmitHandler = (e: FormEvent) => {
35 | e.preventDefault();
36 |
37 | axios
38 | .put>(
39 | '/api/config/0/css',
40 | { styles: customStyles },
41 | { headers: applyAuth() }
42 | )
43 | .then(() => {
44 | createNotification({
45 | title: 'Success',
46 | message: 'CSS saved. Reload page to see changes',
47 | });
48 | })
49 | .catch((err) => console.log(err.response));
50 | };
51 |
52 | return (
53 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.module.css:
--------------------------------------------------------------------------------
1 | .ThemeBuilder {
2 | margin-bottom: 30px;
3 | }
4 |
5 | .Buttons button:not(:last-child) {
6 | margin-right: 10px;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.module.css:
--------------------------------------------------------------------------------
1 | .ColorsContainer {
2 | display: grid;
3 | grid-template-columns: repeat(3, 1fr);
4 | grid-gap: 10px;
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 |
3 | // Redux
4 | import { useSelector, useDispatch } from 'react-redux';
5 | import { bindActionCreators } from 'redux';
6 | import { Theme } from '../../../../interfaces';
7 | import { actionCreators } from '../../../../store';
8 | import { State } from '../../../../store/reducers';
9 |
10 | // Other
11 | import { ActionIcons, CompactTable, Icon, ModalForm } from '../../../UI';
12 |
13 | interface Props {
14 | modalHandler: () => void;
15 | }
16 |
17 | export const ThemeEditor = (props: Props): JSX.Element => {
18 | const {
19 | theme: { userThemes },
20 | } = useSelector((state: State) => state);
21 |
22 | const { deleteTheme, editTheme } = bindActionCreators(
23 | actionCreators,
24 | useDispatch()
25 | );
26 |
27 | const updateHandler = (theme: Theme) => {
28 | props.modalHandler();
29 | editTheme(theme);
30 | };
31 |
32 | const deleteHandler = (theme: Theme) => {
33 | if (window.confirm(`Are you sure you want to delete this theme?`)) {
34 | deleteTheme(theme.name);
35 | }
36 | };
37 |
38 | return (
39 | {}} modalHandler={props.modalHandler}>
40 |
41 | {userThemes.map((t, idx) => (
42 |
43 | {t.name}
44 |
45 | updateHandler(t)}>
46 |
47 |
48 | deleteHandler(t)}>
49 |
50 |
51 |
52 |
53 | ))}
54 |
55 |
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.module.css:
--------------------------------------------------------------------------------
1 | .ThemerGrid {
2 | width: 100%;
3 | display: grid;
4 | grid-template-columns: 1fr;
5 | grid-auto-rows: 100px;
6 | }
7 |
8 | @media (min-width: 340px) {
9 | .ThemerGrid {
10 | grid-template-columns: 1fr 1fr;
11 | }
12 | }
13 |
14 | @media (min-width: 680px) {
15 | .ThemerGrid {
16 | grid-template-columns: 1fr 1fr 1fr;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx:
--------------------------------------------------------------------------------
1 | // Components
2 | import { ThemePreview } from '../ThemePreview/ThemePreview';
3 |
4 | // Other
5 | import { Theme } from '../../../../interfaces';
6 | import classes from './ThemeGrid.module.css';
7 |
8 | interface Props {
9 | themes: Theme[];
10 | }
11 |
12 | export const ThemeGrid = ({ themes }: Props): JSX.Element => {
13 | return (
14 |
15 | {themes.map(
16 | (theme: Theme, idx: number): JSX.Element => (
17 |
18 | )
19 | )}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemePreview/ThemePreview.module.css:
--------------------------------------------------------------------------------
1 | .ThemePreview {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | align-items: flex-start;
7 | }
8 |
9 | .ThemePreview:hover {
10 | cursor: pointer;
11 | }
12 |
13 | .ThemePreview p {
14 | text-transform: capitalize;
15 | margin: 8px 0;
16 | color: var(--color-primary);
17 | }
18 |
19 | .ColorsPreview {
20 | display: flex;
21 | border: 1px solid silver;
22 | }
23 |
24 | .ColorPreview {
25 | width: 50px;
26 | height: 50px;
27 | }
28 |
29 | @media (min-width: 340px) {
30 | .ColorPreview {
31 | width: 40px;
32 | height: 40px;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx:
--------------------------------------------------------------------------------
1 | // Redux
2 | import { useDispatch } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import { actionCreators } from '../../../../store';
5 |
6 | // Other
7 | import { Theme } from '../../../../interfaces/Theme';
8 | import classes from './ThemePreview.module.css';
9 |
10 | interface Props {
11 | theme: Theme;
12 | }
13 |
14 | export const ThemePreview = ({
15 | theme: { colors, name },
16 | }: Props): JSX.Element => {
17 | const { setTheme } = bindActionCreators(actionCreators, useDispatch());
18 |
19 | return (
20 | setTheme(colors)}>
21 |
35 |
{name}
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/client/src/components/Settings/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "routes": [
3 | {
4 | "name": "Theme",
5 | "dest": "/settings",
6 | "authRequired": false
7 | },
8 | {
9 | "name": "General",
10 | "dest": "/settings/general",
11 | "authRequired": true
12 | },
13 | {
14 | "name": "Interface",
15 | "dest": "/settings/interface",
16 | "authRequired": true
17 | },
18 | {
19 | "name": "Weather",
20 | "dest": "/settings/weather",
21 | "authRequired": true
22 | },
23 | {
24 | "name": "Docker",
25 | "dest": "/settings/docker",
26 | "authRequired": true
27 | },
28 | {
29 | "name": "CSS",
30 | "dest": "/settings/css",
31 | "authRequired": true
32 | },
33 | {
34 | "name": "App",
35 | "dest": "/settings/app",
36 | "authRequired": false
37 | }
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/client/src/components/UI/Buttons/ActionButton/ActionButton.module.css:
--------------------------------------------------------------------------------
1 | .ActionButton {
2 | /* background-color: var(--color-accent); */
3 | border: 1.5px solid var(--color-accent);
4 | border-radius: 4px;
5 | color: var(--color-primary);
6 | padding: 5px 15px;
7 | transition: all 0.3s;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | max-width: 250px;
12 | margin-right: 10px;
13 | margin-bottom: 20px;
14 | }
15 |
16 | .ActionButton:hover {
17 | background-color: var(--color-accent);
18 | color: var(--color-background);
19 | cursor: pointer;
20 | }
21 |
22 | .ActionButtonIcon {
23 | --size: 20px;
24 | width: var(--size);
25 | height: var(--size);
26 | /* display: flex; */
27 | }
28 |
29 | .ActionButtonName {
30 | display: flex;
31 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Buttons/ActionButton/ActionButton.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from 'react';
2 | import { Link } from 'react-router-dom';
3 |
4 | import classes from './ActionButton.module.css';
5 | import { Icon } from '../..';
6 |
7 | interface Props {
8 | name: string;
9 | icon: string;
10 | link?: string;
11 | handler?: () => void;
12 | }
13 |
14 | export const ActionButton = (props: Props): JSX.Element => {
15 | const body = (
16 |
17 |
18 |
19 |
20 | {props.name}
21 |
22 | );
23 |
24 | if (props.link) {
25 | return (
26 |
27 | {body}
28 |
29 | );
30 | } else if (props.handler) {
31 | return (
32 | {
36 | if (e.key === 'Enter' && props.handler) props.handler();
37 | }}
38 | tabIndex={0}
39 | >
40 | {body}
41 |
42 | );
43 | } else {
44 | return {body}
;
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/client/src/components/UI/Buttons/Button/Button.module.css:
--------------------------------------------------------------------------------
1 | .Button {
2 | padding: 8px 15px;
3 | border: 1px solid var(--color-accent);
4 | background-color: var(--color-background);
5 | color: var(--color-primary);
6 | border-radius: 4px;
7 | }
8 |
9 | .Button:hover {
10 | cursor: pointer;
11 | background-color: var(--color-accent);
12 | color: var(--color-background);
13 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Buttons/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import classes from './Button.module.css';
3 |
4 | interface Props {
5 | children: ReactNode;
6 | click?: any;
7 | }
8 |
9 | export const Button = (props: Props): JSX.Element => {
10 | const { children, click } = props;
11 |
12 | return (
13 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/components/UI/Forms/InputGroup/InputGroup.module.css:
--------------------------------------------------------------------------------
1 | .InputGroup {
2 | margin-bottom: 15px;
3 | }
4 |
5 | .InputGroup label,
6 | .InputGroup span,
7 | .InputGroup input,
8 | .InputGroup textarea {
9 | display: block;
10 | }
11 |
12 | .InputGroup input,
13 | .InputGroup select,
14 | .InputGroup textarea {
15 | margin: 8px 0;
16 | width: 100%;
17 | border: none;
18 | border-radius: 4px;
19 | padding: 10px;
20 | background-color: var(--color-primary);
21 | color: var(--color-background);
22 | }
23 |
24 | .InputGroup span {
25 | font-size: 12px;
26 | color: var(--color-primary);
27 | }
28 |
29 | .InputGroup span a {
30 | color: var(--color-accent);
31 | }
32 |
33 | .InputGroup label {
34 | color: var(--color-primary);
35 | }
36 |
37 | .InputGroup textarea {
38 | resize: none;
39 | height: 50vh;
40 | }
41 |
42 | .InputGroup input[type='color'] {
43 | margin: 0;
44 | padding: 0;
45 | background-color: transparent;
46 | }
47 |
48 | .InputGroup input[type='color']:hover {
49 | cursor: pointer;
50 | }
51 |
--------------------------------------------------------------------------------
/client/src/components/UI/Forms/InputGroup/InputGroup.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import classes from './InputGroup.module.css';
3 |
4 | interface Props {
5 | children: ReactNode;
6 | }
7 |
8 | export const InputGroup = (props: Props): JSX.Element => {
9 | return {props.children}
;
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/components/UI/Forms/ModalForm/ModalForm.module.css:
--------------------------------------------------------------------------------
1 | .ModalForm {
2 | background-color: var(--color-background);
3 | color: var(--color-primary);
4 | border-radius: 6px;
5 | width: 95%;
6 | position: relative;
7 | padding: 50px 50px;
8 | }
9 |
10 | .ModalFormIcon {
11 | width: 40px;
12 | position: absolute;
13 | right: 5px;
14 | top: 5px;
15 | }
16 |
17 | .ModalFormIcon:hover {
18 | cursor: pointer;
19 | }
20 |
21 | @media (min-width: 430px) {
22 | .ModalForm {
23 | width: 90%;
24 | }
25 | }
26 |
27 | @media (min-width: 800px) {
28 | .ModalForm {
29 | width: 70%;
30 | }
31 | }
32 |
33 | @media (min-width: 1000px) {
34 | .ModalForm {
35 | width: 60%;
36 | }
37 | }
38 |
39 | @media (min-width: 1400px) {
40 | .ModalForm {
41 | width: 40%;
42 | }
43 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Forms/ModalForm/ModalForm.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, SyntheticEvent } from 'react';
2 |
3 | import classes from './ModalForm.module.css';
4 | import { Icon } from '../..';
5 |
6 | interface ComponentProps {
7 | children: ReactNode;
8 | modalHandler?: () => void;
9 | formHandler: (e: SyntheticEvent) => void;
10 | }
11 |
12 | export const ModalForm = (props: ComponentProps): JSX.Element => {
13 | const _modalHandler = (): void => {
14 | if (props.modalHandler) {
15 | props.modalHandler();
16 | }
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/Headline/Headline.module.css:
--------------------------------------------------------------------------------
1 | .HeadlineTitle {
2 | color: var(--color-primary);
3 | font-weight: 700;
4 | }
5 |
6 | .HeadlineSubtitle {
7 | color: var(--color-primary);
8 | margin-bottom: 20px;
9 | font-weight: 400;
10 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/Headline/Headline.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment, ReactNode } from 'react';
2 | import classes from './Headline.module.css';
3 |
4 | interface Props {
5 | title: string;
6 | subtitle?: ReactNode;
7 | }
8 |
9 | export const Headline = (props: Props): JSX.Element => {
10 | return (
11 |
12 | {props.title}
13 | {props.subtitle && (
14 | {props.subtitle}
15 | )}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.module.css:
--------------------------------------------------------------------------------
1 | .SectionHeadline {
2 | text-transform: uppercase;
3 | color: var(--color-primary);
4 | font-weight: 900;
5 | font-size: 20px;
6 | margin-bottom: 16px;
7 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/SectionHeadline/SectionHeadline.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | import classes from './SectionHeadline.module.css';
4 |
5 | interface Props {
6 | title: string;
7 | link: string;
8 | }
9 |
10 | export const SectionHeadline = (props: Props): JSX.Element => {
11 | return (
12 |
13 | {props.title}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.module.css:
--------------------------------------------------------------------------------
1 | .SettingsHeadline {
2 | color: var(--color-primary);
3 | padding-bottom: 3px;
4 | margin-bottom: 10px;
5 | font-size: 20px;
6 | font-weight: 500;
7 | border-bottom: 2px solid var(--color-accent);
8 | display: inline-block;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/Headlines/SettingsHeadline/SettingsHeadline.tsx:
--------------------------------------------------------------------------------
1 | import classes from './SettingsHeadline.module.css';
2 |
3 | interface Props {
4 | text: string;
5 | }
6 |
7 | export const SettingsHeadline = (props: Props): JSX.Element => {
8 | return {props.text}
;
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/Icons/ActionIcons/ActionIcons.module.css:
--------------------------------------------------------------------------------
1 | .ActionIcons {
2 | display: flex;
3 | }
4 |
5 | .ActionIcons svg {
6 | width: 20px;
7 | }
8 |
9 | .ActionIcons svg:hover {
10 | cursor: pointer;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/UI/Icons/ActionIcons/ActionIcons.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import styles from './ActionIcons.module.css';
3 |
4 | interface Props {
5 | children: ReactNode;
6 | }
7 |
8 | export const ActionIcons = ({ children }: Props): JSX.Element => {
9 | return {children};
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/components/UI/Icons/Icon/Icon.module.css:
--------------------------------------------------------------------------------
1 | .Icon {
2 | color: var(--color-primary);
3 | width: 90%;
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/components/UI/Icons/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import classes from './Icon.module.css';
2 |
3 | import { Icon as MDIcon } from '@mdi/react';
4 |
5 | interface Props {
6 | icon: string;
7 | color?: string;
8 | }
9 |
10 | export const Icon = (props: Props): JSX.Element => {
11 | const MDIcons = require('@mdi/js');
12 | let iconPath = MDIcons[props.icon];
13 |
14 | if (!iconPath) {
15 | console.log(`Icon ${props.icon} not found`);
16 | iconPath = MDIcons.mdiCancel;
17 | }
18 |
19 | return (
20 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { Skycons } from 'skycons-ts';
4 | import { State } from '../../../../store/reducers';
5 | import { IconMapping, TimeOfDay } from './IconMapping';
6 |
7 | interface Props {
8 | weatherStatusCode: number;
9 | isDay: number;
10 | }
11 |
12 | export const WeatherIcon = (props: Props): JSX.Element => {
13 | const { activeTheme } = useSelector((state: State) => state.theme);
14 |
15 | const icon = props.isDay
16 | ? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day)
17 | : new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.night);
18 |
19 | useEffect(() => {
20 | const delay = setTimeout(() => {
21 | const skycons = new Skycons({ color: activeTheme.colors.accent });
22 | skycons.add(`weather-icon`, icon);
23 | skycons.play();
24 | }, 1);
25 |
26 | return () => {
27 | clearTimeout(delay);
28 | };
29 | }, [props.weatherStatusCode, icon, activeTheme.colors.accent]);
30 |
31 | return ;
32 | };
33 |
--------------------------------------------------------------------------------
/client/src/components/UI/Layout/Layout.module.css:
--------------------------------------------------------------------------------
1 | .Container {
2 | width: 100%;
3 | padding: 20px;
4 | margin: 0 auto;
5 | }
6 |
7 | /* .Container {
8 | width: 60%;
9 | margin: 0 auto;
10 | padding: 20px;
11 | padding-top: 20px;
12 | } */
13 |
14 | /* 320px — 480px: Mobile devices.
15 | 481px — 768px: iPads, Tablets.
16 | 769px — 1024px: Small screens, laptops.
17 | 1025px — 1200px: Desktops, large screens.
18 | 1201px and more — Extra large screens, TV. */
19 |
20 | @media (min-width: 769px) {
21 | .Container {
22 | /* padding: 25px 40px; */
23 | width: 90%;
24 | }
25 | }
26 |
27 | @media (min-width: 1201px) {
28 | .Container {
29 | padding: 50px 250px;
30 | }
31 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Layout/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import classes from './Layout.module.css';
3 |
4 | interface ComponentProps {
5 | children: ReactNode;
6 | }
7 |
8 | export const Container = (props: ComponentProps): JSX.Element => {
9 | return {props.children}
;
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/components/UI/Modal/Modal.module.css:
--------------------------------------------------------------------------------
1 | .Modal {
2 | width: 100%;
3 | height: 100%;
4 | position: fixed;
5 | top: 0;
6 | left: 0;
7 | z-index: 100;
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | .ModalClose {
14 | background-color: rgba(0, 0, 0, 0);
15 | visibility: hidden;
16 | }
17 |
18 | .ModalOpen {
19 | background-color: rgba(0, 0, 0, 0.7);
20 | visibility: visible;
21 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Modal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import { MouseEvent, ReactNode, useRef } from 'react';
2 |
3 | import classes from './Modal.module.css';
4 |
5 | interface Props {
6 | isOpen: boolean;
7 | setIsOpen: Function;
8 | children: ReactNode;
9 | cb?: Function;
10 | }
11 |
12 | export const Modal = ({
13 | isOpen,
14 | setIsOpen,
15 | children,
16 | cb,
17 | }: Props): JSX.Element => {
18 | const modalRef = useRef(null);
19 | const modalClasses = [
20 | classes.Modal,
21 | isOpen ? classes.ModalOpen : classes.ModalClose,
22 | ].join(' ');
23 |
24 | const clickHandler = (e: MouseEvent) => {
25 | if (e.target === modalRef.current) {
26 | setIsOpen(false);
27 |
28 | if (cb) cb();
29 | }
30 | };
31 |
32 | return (
33 |
34 | {children}
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/client/src/components/UI/Notification/Notification.module.css:
--------------------------------------------------------------------------------
1 | .Notification {
2 | width: 300px;
3 | background-color: var(--color-background);
4 | border: 1px solid var(--color-primary);
5 | color: var(--color-primary);
6 | border-radius: 4px;
7 | padding: 15px 10px;
8 | transition: all 0.25s;
9 | margin-bottom: 10px;
10 | }
11 |
12 | .Notification:hover {
13 | background-color: var(--color-primary);
14 | color: var(--color-background);
15 | cursor: pointer;
16 | }
17 |
18 | .Notification:last-child {
19 | margin-bottom: 0;
20 | }
21 |
22 | .NotificationOpen {
23 | animation: slideIn 0.3s;
24 | }
25 |
26 | .NotificationClose {
27 | animation: slideOut 0.3s;
28 | transform: translateX(600px);
29 | }
30 |
31 | @keyframes slideIn {
32 | from {
33 | transform: translateX(600px);
34 | }
35 | to {
36 | transform: translateX(0);
37 | }
38 | }
39 |
40 | @keyframes slideOut {
41 | from {
42 | transform: translateX(0);
43 | }
44 | to {
45 | transform: translateX(600px);
46 | }
47 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Notification/Notification.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { bindActionCreators } from 'redux';
4 | import { actionCreators } from '../../../store';
5 |
6 | import classes from './Notification.module.css';
7 |
8 | interface Props {
9 | title: string;
10 | message: string;
11 | id: number;
12 | url: string | null;
13 | }
14 |
15 | export const Notification = (props: Props): JSX.Element => {
16 | const dispatch = useDispatch();
17 | const { clearNotification } = bindActionCreators(actionCreators, dispatch);
18 |
19 | const [isOpen, setIsOpen] = useState(true);
20 | const elementClasses = [
21 | classes.Notification,
22 | isOpen ? classes.NotificationOpen : classes.NotificationClose,
23 | ].join(' ');
24 |
25 | useEffect(() => {
26 | const closeNotification = setTimeout(() => {
27 | setIsOpen(false);
28 | }, 3500);
29 |
30 | const clearNotificationTimeout = setTimeout(() => {
31 | clearNotification(props.id);
32 | }, 3600);
33 |
34 | return () => {
35 | window.clearTimeout(closeNotification);
36 | window.clearTimeout(clearNotificationTimeout);
37 | };
38 | }, []);
39 |
40 | const clickHandler = () => {
41 | if (props.url) {
42 | window.open(props.url, '_blank');
43 | }
44 | };
45 |
46 | return (
47 |
48 |
{props.title}
49 |
{props.message}
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/client/src/components/UI/Spinner/Spinner.module.css:
--------------------------------------------------------------------------------
1 | .Spinner,
2 | .Spinner:before,
3 | .Spinner:after {
4 | background: var(--color-primary);
5 | animation: load1 1s infinite ease-in-out;
6 | width: 1em;
7 | height: 4em;
8 | }
9 |
10 | .Spinner {
11 | color: var(--color-primary);
12 | text-indent: -9999em;
13 | margin: 88px auto;
14 | position: relative;
15 | font-size: 11px;
16 | transform: translateZ(0);
17 | animation-delay: -0.16s;
18 | }
19 |
20 | .Spinner:before,
21 | .Spinner:after {
22 | position: absolute;
23 | top: 0;
24 | content: '';
25 | }
26 |
27 | .Spinner:before {
28 | left: -1.5em;
29 | animation-delay: -0.32s;
30 | }
31 |
32 | .Spinner:after {
33 | left: 1.5em;
34 | }
35 |
36 | @keyframes load1 {
37 | 0%,
38 | 80%,
39 | 100% {
40 | box-shadow: 0 0;
41 | height: 4em;
42 | }
43 | 40% {
44 | box-shadow: 0 -2em;
45 | height: 5em;
46 | }
47 | }
48 |
49 | .SpinnerWrapper {
50 | height: 150px;
51 | display: flex;
52 | justify-content: center;
53 | align-items: center;
54 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import classes from './Spinner.module.css';
2 |
3 | export const Spinner = (): JSX.Element => {
4 | return (
5 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/Tables/CompactTable/CompactTable.module.css:
--------------------------------------------------------------------------------
1 | .CompactTable {
2 | display: grid;
3 | }
4 |
5 | .CompactTable span {
6 | color: var(--color-primary);
7 | }
8 |
9 | .CompactTable span:last-child {
10 | margin-bottom: 10px;
11 | }
12 |
13 | .Separator {
14 | border-bottom: 1px solid var(--color-primary);
15 | margin: 10px 0;
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/components/UI/Tables/CompactTable/CompactTable.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import classes from './CompactTable.module.css';
3 |
4 | interface Props {
5 | headers: string[];
6 | children?: ReactNode;
7 | }
8 |
9 | export const CompactTable = ({ headers, children }: Props): JSX.Element => {
10 | return (
11 |
15 | {headers.map((h, idx) => (
16 |
{h}
17 | ))}
18 |
19 |
23 |
24 | {children}
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/client/src/components/UI/Tables/Table/Table.module.css:
--------------------------------------------------------------------------------
1 | .TableContainer {
2 | width: 100%;
3 | }
4 |
5 | .Table {
6 | border-collapse: collapse;
7 | width: 100%;
8 | text-align: left;
9 | font-size: 16px;
10 | color: var(--color-primary);
11 | table-layout: fixed;
12 | }
13 |
14 | .Table th,
15 | .Table td {
16 | padding: 10px;
17 | overflow: hidden;
18 | text-overflow: ellipsis;
19 | }
20 |
21 | /* Head */
22 | .Table th {
23 | --header-radius: 4px;
24 | background-color: var(--color-primary);
25 | color: var(--color-background);
26 | }
27 |
28 | .Table th:first-child {
29 | border-top-left-radius: var(--header-radius);
30 | border-bottom-left-radius: var(--header-radius);
31 | }
32 |
33 | .Table th:last-child {
34 | border-top-right-radius: var(--header-radius);
35 | border-bottom-right-radius: var(--header-radius);
36 | }
37 |
38 | /* Body */
39 | .Table td {
40 | transition: all 0.2s;
41 | }
--------------------------------------------------------------------------------
/client/src/components/UI/Tables/Table/Table.tsx:
--------------------------------------------------------------------------------
1 | import classes from './Table.module.css';
2 |
3 | interface Props {
4 | children: React.ReactNode;
5 | headers: string[];
6 | innerRef?: any;
7 | }
8 |
9 | export const Table = (props: Props): JSX.Element => {
10 | return (
11 |
12 |
13 |
14 |
15 | {props.headers.map(
16 | (header: string, index: number): JSX.Element => (
17 | {header} |
18 | )
19 | )}
20 |
21 |
22 | {props.children}
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/client/src/components/UI/Text/Message/Message.module.css:
--------------------------------------------------------------------------------
1 | .message {
2 | color: var(--color-primary);
3 | }
4 |
5 | .message a {
6 | color: var(--color-accent);
7 | font-weight: 600;
8 | }
9 |
10 | .messageCenter {
11 | width: 100%;
12 | display: flex;
13 | justify-content: center;
14 | align-items: baseline;
15 | color: var(--color-primary);
16 | margin-bottom: 20px;
17 | }
18 |
19 | .messageCenter a,
20 | .messageCenter span {
21 | color: var(--color-accent);
22 | }
23 |
24 | .messageCenter a:hover {
25 | cursor: pointer;
26 | }
27 |
--------------------------------------------------------------------------------
/client/src/components/UI/Text/Message/Message.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import classes from './Message.module.css';
4 |
5 | interface Props {
6 | children: ReactNode;
7 | isPrimary?: boolean;
8 | }
9 |
10 | export const Message = ({ children, isPrimary = true }: Props): JSX.Element => {
11 | const style = isPrimary ? classes.message : classes.messageCenter;
12 |
13 | return {children}
;
14 | };
15 |
--------------------------------------------------------------------------------
/client/src/components/UI/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Tables/Table/Table';
2 | export * from './Tables/CompactTable/CompactTable';
3 | export * from './Spinner/Spinner';
4 | export * from './Notification/Notification';
5 | export * from './Modal/Modal';
6 | export * from './Layout/Layout';
7 | export * from './Icons/Icon/Icon';
8 | export * from './Icons/WeatherIcon/WeatherIcon';
9 | export * from './Icons/ActionIcons/ActionIcons';
10 | export * from './Headlines/Headline/Headline';
11 | export * from './Headlines/SectionHeadline/SectionHeadline';
12 | export * from './Headlines/SettingsHeadline/SettingsHeadline';
13 | export * from './Forms/InputGroup/InputGroup';
14 | export * from './Forms/ModalForm/ModalForm';
15 | export * from './Buttons/ActionButton/ActionButton';
16 | export * from './Buttons/Button/Button';
17 | export * from './Text/Message/Message';
18 |
--------------------------------------------------------------------------------
/client/src/components/Widgets/WeatherWidget/WeatherWidget.module.css:
--------------------------------------------------------------------------------
1 | .WeatherWidget {
2 | display: flex;
3 | visibility: hidden;
4 | }
5 |
6 | .WeatherDetails {
7 | display: flex;
8 | flex-direction: column;
9 | justify-content: center;
10 | font-size: 16px;
11 | color: var(--color-primary);
12 | margin-left: 10px;
13 | font-weight: 500;
14 | }
15 |
16 | .WeatherDetails span:first-child {
17 | border-bottom: 1px solid var(--color-primary);
18 | padding-bottom: 5px;
19 | }
20 |
21 | .WeatherDetails span:last-child {
22 | padding-top: 5px;
23 | }
24 |
25 | @media (min-width: 600px) {
26 | .WeatherWidget {
27 | visibility: visible;
28 | }
29 | }
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: local(''),
6 | url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff2') format('woff2'),
7 | url('./assets/fonts/Roboto/roboto-v29-latin-regular.woff') format('woff');
8 | }
9 |
10 | @font-face {
11 | font-family: 'Roboto';
12 | font-style: normal;
13 | font-weight: 500;
14 | src: local(''),
15 | url('./assets/fonts/Roboto/roboto-v29-latin-500.woff2') format('woff2'),
16 | url('./assets/fonts/Roboto/roboto-v29-latin-500.woff') format('woff');
17 | }
18 |
19 | @font-face {
20 | font-family: 'Roboto';
21 | font-style: normal;
22 | font-weight: 900;
23 | src: local(''),
24 | url('./assets/fonts/Roboto/roboto-v29-latin-900.woff2') format('woff2'),
25 | url('./assets/fonts/Roboto/roboto-v29-latin-900.woff') format('woff');
26 | }
27 |
28 | @font-face {
29 | font-family: 'Roboto';
30 | font-style: normal;
31 | font-weight: 700;
32 | src: local(''),
33 | url('./assets/fonts/Roboto/roboto-v29-latin-700.woff2') format('woff2'),
34 | url('./assets/fonts/Roboto/roboto-v29-latin-700.woff') format('woff');
35 | }
36 |
37 | * {
38 | margin: 0;
39 | padding: 0;
40 | box-sizing: border-box;
41 | }
42 |
43 | body {
44 | --color-background: #242b33;
45 | --color-primary: #effbff;
46 | --color-accent: #6ee2ff;
47 | --spacing-ui: 10px;
48 |
49 | background-color: var(--color-background);
50 | transition: background-color 0.3s;
51 | font-family: Roboto, sans-serif;
52 | font-size: 14px;
53 | }
54 |
55 | a {
56 | color: var(--color-primary);
57 | text-decoration: none;
58 | }
59 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 |
5 | import { Provider } from 'react-redux';
6 | import { store } from './store/store';
7 |
8 | import { App } from './App';
9 |
10 | ReactDOM.render(
11 |
12 |
13 |
14 |
15 | ,
16 | document.getElementById('root')
17 | );
18 |
--------------------------------------------------------------------------------
/client/src/interfaces/Api.ts:
--------------------------------------------------------------------------------
1 | export interface Model {
2 | id: number;
3 | createdAt: Date;
4 | updatedAt: Date;
5 | }
6 |
7 | export interface ApiResponse {
8 | success: boolean;
9 | data: T;
10 | }
11 |
12 | export interface Token {
13 | app: string;
14 | exp: number;
15 | iat: number;
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/interfaces/App.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '.';
2 |
3 | export interface NewApp {
4 | name: string;
5 | url: string;
6 | icon: string;
7 | isPublic: boolean;
8 | description: string;
9 | }
10 |
11 | export interface App extends Model, NewApp {
12 | orderId: number;
13 | isPinned: boolean;
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/interfaces/Bookmark.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '.';
2 |
3 | export interface NewBookmark {
4 | name: string;
5 | url: string;
6 | categoryId: number;
7 | icon: string;
8 | isPublic: boolean;
9 | }
10 |
11 | export interface Bookmark extends Model, NewBookmark {
12 | orderId: number;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/interfaces/Category.ts:
--------------------------------------------------------------------------------
1 | import { Model, Bookmark } from '.';
2 |
3 | export interface NewCategory {
4 | name: string;
5 | isPublic: boolean;
6 | }
7 |
8 | export interface Category extends Model, NewCategory {
9 | isPinned: boolean;
10 | orderId: number;
11 | bookmarks: Bookmark[];
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/interfaces/Config.ts:
--------------------------------------------------------------------------------
1 | import { WeatherData } from '../types';
2 |
3 | export interface Config {
4 | WEATHER_API_KEY: string;
5 | lat: number;
6 | long: number;
7 | isCelsius: boolean;
8 | customTitle: string;
9 | pinAppsByDefault: boolean;
10 | pinCategoriesByDefault: boolean;
11 | hideHeader: boolean;
12 | useOrdering: string;
13 | appsSameTab: boolean;
14 | bookmarksSameTab: boolean;
15 | searchSameTab: boolean;
16 | hideApps: boolean;
17 | hideCategories: boolean;
18 | hideSearch: boolean;
19 | defaultSearchProvider: string;
20 | secondarySearchProvider: string;
21 | dockerApps: boolean;
22 | dockerHost: string;
23 | kubernetesApps: boolean;
24 | unpinStoppedApps: boolean;
25 | useAmericanDate: boolean;
26 | disableAutofocus: boolean;
27 | greetingsSchema: string;
28 | daySchema: string;
29 | monthSchema: string;
30 | showTime: boolean;
31 | defaultTheme: string;
32 | isKilometer: boolean;
33 | weatherData: WeatherData;
34 | hideDate: boolean;
35 | }
36 |
--------------------------------------------------------------------------------
/client/src/interfaces/Forms.ts:
--------------------------------------------------------------------------------
1 | import { WeatherData } from '../types';
2 |
3 | export interface WeatherForm {
4 | WEATHER_API_KEY: string;
5 | lat: number;
6 | long: number;
7 | isCelsius: boolean;
8 | weatherData: WeatherData;
9 | }
10 |
11 | export interface GeneralForm {
12 | defaultSearchProvider: string;
13 | secondarySearchProvider: string;
14 | searchSameTab: boolean;
15 | pinAppsByDefault: boolean;
16 | pinCategoriesByDefault: boolean;
17 | useOrdering: string;
18 | appsSameTab: boolean;
19 | bookmarksSameTab: boolean;
20 | }
21 |
22 | export interface UISettingsForm {
23 | customTitle: string;
24 | hideHeader: boolean;
25 | hideApps: boolean;
26 | hideCategories: boolean;
27 | useAmericanDate: boolean;
28 | greetingsSchema: string;
29 | daySchema: string;
30 | monthSchema: string;
31 | showTime: boolean;
32 | hideDate: boolean;
33 | hideSearch: boolean;
34 | disableAutofocus: boolean;
35 | }
36 |
37 | export interface DockerSettingsForm {
38 | dockerApps: boolean;
39 | dockerHost: string;
40 | kubernetesApps: boolean;
41 | unpinStoppedApps: boolean;
42 | }
43 |
44 | export interface ThemeSettingsForm {
45 | defaultTheme: string;
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/interfaces/Notification.ts:
--------------------------------------------------------------------------------
1 | export interface NewNotification {
2 | title: string;
3 | message: string;
4 | url?: string;
5 | }
6 |
7 | export interface Notification extends NewNotification {
8 | id: number;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/interfaces/Query.ts:
--------------------------------------------------------------------------------
1 | export interface Query {
2 | name: string;
3 | prefix: string;
4 | template: string;
5 | }
--------------------------------------------------------------------------------
/client/src/interfaces/Route.ts:
--------------------------------------------------------------------------------
1 | export interface Route {
2 | name: string;
3 | dest: string;
4 | authRequired: boolean;
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/interfaces/SearchResult.ts:
--------------------------------------------------------------------------------
1 | import { Query } from './Query';
2 |
3 | export interface SearchResult {
4 | isLocal: boolean;
5 | isURL: boolean;
6 | sameTab: boolean;
7 | encodedURL: string;
8 | primarySearch: Query;
9 | secondarySearch: Query;
10 | rawQuery: string;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/interfaces/Theme.ts:
--------------------------------------------------------------------------------
1 | export interface ThemeColors {
2 | background: string;
3 | primary: string;
4 | accent: string;
5 | }
6 |
7 | export interface Theme {
8 | name: string;
9 | colors: ThemeColors;
10 | isCustom: boolean;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/interfaces/Weather.ts:
--------------------------------------------------------------------------------
1 | import { Model } from '.';
2 |
3 | export interface Weather extends Model {
4 | externalLastUpdate: string;
5 | tempC: number;
6 | tempF: number;
7 | isDay: number;
8 | cloud: number;
9 | conditionText: string;
10 | conditionCode: number;
11 | humidity: number;
12 | windK: number;
13 | windM: number;
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './App';
2 | export * from './Theme';
3 | export * from './Api';
4 | export * from './Weather';
5 | export * from './Bookmark';
6 | export * from './Category';
7 | export * from './Notification';
8 | export * from './Config';
9 | export * from './Forms';
10 | export * from './Query';
11 | export * from './SearchResult';
12 | export * from './Route';
13 |
--------------------------------------------------------------------------------
/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const { createProxyMiddleware } = require('http-proxy-middleware');
2 |
3 | module.exports = function (app) {
4 | const apiProxy = createProxyMiddleware('/api', {
5 | target: 'http://localhost:5005'
6 | })
7 |
8 | const assetsProxy = createProxyMiddleware('/uploads', {
9 | target: 'http://localhost:5005'
10 | })
11 |
12 | const wsProxy = createProxyMiddleware('/socket', {
13 | target: 'http://localhost:5005',
14 | ws: true
15 | })
16 |
17 | app.use(apiProxy);
18 | app.use(assetsProxy);
19 | app.use(wsProxy);
20 | };
--------------------------------------------------------------------------------
/client/src/store/action-creators/auth.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux';
2 | import { ApiResponse } from '../../interfaces';
3 | import { ActionType } from '../action-types';
4 | import {
5 | AuthErrorAction,
6 | AutoLoginAction,
7 | LoginAction,
8 | LogoutAction,
9 | } from '../actions/auth';
10 | import axios, { AxiosError } from 'axios';
11 | import { getApps, getCategories } from '.';
12 |
13 | export const login =
14 | (formData: { password: string; duration: string }) =>
15 | async (dispatch: Dispatch) => {
16 | try {
17 | const res = await axios.post>(
18 | '/api/auth',
19 | formData
20 | );
21 |
22 | localStorage.setItem('token', res.data.data.token);
23 |
24 | dispatch({
25 | type: ActionType.login,
26 | payload: res.data.data.token,
27 | });
28 |
29 | dispatch(getApps());
30 | dispatch(getCategories());
31 | } catch (err) {
32 | dispatch(authError(err, true));
33 | }
34 | };
35 |
36 | export const logout = () => (dispatch: Dispatch) => {
37 | localStorage.removeItem('token');
38 |
39 | dispatch({
40 | type: ActionType.logout,
41 | });
42 |
43 | dispatch(getApps());
44 | dispatch(getCategories());
45 | };
46 |
47 | export const autoLogin = () => async (dispatch: Dispatch) => {
48 | const token: string = localStorage.token;
49 |
50 | try {
51 | await axios.post>(
52 | '/api/auth/validate',
53 | { token }
54 | );
55 |
56 | dispatch({
57 | type: ActionType.autoLogin,
58 | payload: token,
59 | });
60 |
61 | dispatch(getApps());
62 | dispatch(getCategories());
63 | } catch (err) {
64 | dispatch(authError(err, false));
65 | }
66 | };
67 |
68 | export const authError =
69 | (error: unknown, showNotification: boolean) =>
70 | (dispatch: Dispatch) => {
71 | const apiError = error as AxiosError;
72 |
73 | if (showNotification) {
74 | dispatch({
75 | type: ActionType.createNotification,
76 | payload: {
77 | title: 'Error',
78 | message: apiError.response?.data.error,
79 | },
80 | });
81 | }
82 |
83 | dispatch(getApps());
84 | dispatch(getCategories());
85 | };
86 |
--------------------------------------------------------------------------------
/client/src/store/action-creators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './theme';
2 | export * from './config';
3 | export * from './notification';
4 | export * from './app';
5 | export * from './bookmark';
6 | export * from './auth';
7 |
--------------------------------------------------------------------------------
/client/src/store/action-creators/notification.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux';
2 | import { NewNotification } from '../../interfaces';
3 | import { ActionType } from '../action-types';
4 | import {
5 | CreateNotificationAction,
6 | ClearNotificationAction,
7 | } from '../actions/notification';
8 |
9 | export const createNotification =
10 | (notification: NewNotification) =>
11 | (dispatch: Dispatch) => {
12 | dispatch({
13 | type: ActionType.createNotification,
14 | payload: notification,
15 | });
16 | };
17 |
18 | export const clearNotification =
19 | (id: number) => (dispatch: Dispatch) => {
20 | dispatch({
21 | type: ActionType.clearNotification,
22 | payload: id,
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/client/src/store/action-types/index.ts:
--------------------------------------------------------------------------------
1 | export enum ActionType {
2 | // THEME
3 | setTheme = 'SET_THEME',
4 | fetchThemes = 'FETCH_THEMES',
5 | addTheme = 'ADD_THEME',
6 | deleteTheme = 'DELETE_THEME',
7 | updateTheme = 'UPDATE_THEME',
8 | editTheme = 'EDIT_THEME',
9 | // CONFIG
10 | getConfig = 'GET_CONFIG',
11 | updateConfig = 'UPDATE_CONFIG',
12 | // QUERIES
13 | addQuery = 'ADD_QUERY',
14 | deleteQuery = 'DELETE_QUERY',
15 | fetchQueries = 'FETCH_QUERIES',
16 | updateQuery = 'UPDATE_QUERY',
17 | // NOTIFICATIONS
18 | createNotification = 'CREATE_NOTIFICATION',
19 | clearNotification = 'CLEAR_NOTIFICATION',
20 | // APPS
21 | getApps = 'GET_APPS',
22 | getAppsSuccess = 'GET_APPS_SUCCESS',
23 | getAppsError = 'GET_APPS_ERROR',
24 | pinApp = 'PIN_APP',
25 | addApp = 'ADD_APP',
26 | addAppSuccess = 'ADD_APP_SUCCESS',
27 | deleteApp = 'DELETE_APP',
28 | updateApp = 'UPDATE_APP',
29 | reorderApps = 'REORDER_APPS',
30 | sortApps = 'SORT_APPS',
31 | setEditApp = 'SET_EDIT_APP',
32 | // CATEGORES
33 | getCategories = 'GET_CATEGORIES',
34 | getCategoriesSuccess = 'GET_CATEGORIES_SUCCESS',
35 | getCategoriesError = 'GET_CATEGORIES_ERROR',
36 | addCategory = 'ADD_CATEGORY',
37 | pinCategory = 'PIN_CATEGORY',
38 | deleteCategory = 'DELETE_CATEGORY',
39 | updateCategory = 'UPDATE_CATEGORY',
40 | sortCategories = 'SORT_CATEGORIES',
41 | reorderCategories = 'REORDER_CATEGORIES',
42 | setEditCategory = 'SET_EDIT_CATEGORY',
43 | // BOOKMARKS
44 | addBookmark = 'ADD_BOOKMARK',
45 | deleteBookmark = 'DELETE_BOOKMARK',
46 | updateBookmark = 'UPDATE_BOOKMARK',
47 | setEditBookmark = 'SET_EDIT_BOOKMARK',
48 | reorderBookmarks = 'REORDER_BOOKMARKS',
49 | sortBookmarks = 'SORT_BOOKMARKS',
50 | // AUTH
51 | login = 'LOGIN',
52 | logout = 'LOGOUT',
53 | autoLogin = 'AUTO_LOGIN',
54 | authError = 'AUTH_ERROR',
55 | }
56 |
--------------------------------------------------------------------------------
/client/src/store/actions/app.ts:
--------------------------------------------------------------------------------
1 | import { ActionType } from '../action-types';
2 | import { App } from '../../interfaces';
3 |
4 | export interface GetAppsAction {
5 | type:
6 | | ActionType.getApps
7 | | ActionType.getAppsSuccess
8 | | ActionType.getAppsError;
9 | payload: T;
10 | }
11 | export interface PinAppAction {
12 | type: ActionType.pinApp;
13 | payload: App;
14 | }
15 |
16 | export interface AddAppAction {
17 | type: ActionType.addAppSuccess;
18 | payload: App;
19 | }
20 | export interface DeleteAppAction {
21 | type: ActionType.deleteApp;
22 | payload: number;
23 | }
24 |
25 | export interface UpdateAppAction {
26 | type: ActionType.updateApp;
27 | payload: App;
28 | }
29 |
30 | export interface ReorderAppsAction {
31 | type: ActionType.reorderApps;
32 | payload: App[];
33 | }
34 |
35 | export interface SortAppsAction {
36 | type: ActionType.sortApps;
37 | payload: string;
38 | }
39 |
40 | export interface SetEditAppAction {
41 | type: ActionType.setEditApp;
42 | payload: App | null;
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/store/actions/auth.ts:
--------------------------------------------------------------------------------
1 | import { ActionType } from '../action-types';
2 |
3 | export interface LoginAction {
4 | type: ActionType.login;
5 | payload: string;
6 | }
7 |
8 | export interface LogoutAction {
9 | type: ActionType.logout;
10 | }
11 |
12 | export interface AutoLoginAction {
13 | type: ActionType.autoLogin;
14 | payload: string;
15 | }
16 |
17 | export interface AuthErrorAction {
18 | type: ActionType.authError;
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/store/actions/bookmark.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark, Category } from '../../interfaces';
2 | import { ActionType } from '../action-types';
3 |
4 | export interface GetCategoriesAction {
5 | type:
6 | | ActionType.getCategories
7 | | ActionType.getCategoriesSuccess
8 | | ActionType.getCategoriesError;
9 | payload: T;
10 | }
11 |
12 | export interface AddCategoryAction {
13 | type: ActionType.addCategory;
14 | payload: Category;
15 | }
16 |
17 | export interface AddBookmarkAction {
18 | type: ActionType.addBookmark;
19 | payload: Bookmark;
20 | }
21 |
22 | export interface PinCategoryAction {
23 | type: ActionType.pinCategory;
24 | payload: Category;
25 | }
26 |
27 | export interface DeleteCategoryAction {
28 | type: ActionType.deleteCategory;
29 | payload: number;
30 | }
31 |
32 | export interface UpdateCategoryAction {
33 | type: ActionType.updateCategory;
34 | payload: Category;
35 | }
36 |
37 | export interface DeleteBookmarkAction {
38 | type: ActionType.deleteBookmark;
39 | payload: {
40 | bookmarkId: number;
41 | categoryId: number;
42 | };
43 | }
44 |
45 | export interface UpdateBookmarkAction {
46 | type: ActionType.updateBookmark;
47 | payload: Bookmark;
48 | }
49 |
50 | export interface SortCategoriesAction {
51 | type: ActionType.sortCategories;
52 | payload: string;
53 | }
54 |
55 | export interface ReorderCategoriesAction {
56 | type: ActionType.reorderCategories;
57 | payload: Category[];
58 | }
59 |
60 | export interface SetEditCategoryAction {
61 | type: ActionType.setEditCategory;
62 | payload: Category | null;
63 | }
64 |
65 | export interface SetEditBookmarkAction {
66 | type: ActionType.setEditBookmark;
67 | payload: Bookmark | null;
68 | }
69 |
70 | export interface ReorderBookmarksAction {
71 | type: ActionType.reorderBookmarks;
72 | payload: {
73 | bookmarks: Bookmark[];
74 | categoryId: number;
75 | };
76 | }
77 |
78 | export interface SortBookmarksAction {
79 | type: ActionType.sortBookmarks;
80 | payload: {
81 | orderType: string;
82 | categoryId: number;
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/client/src/store/actions/config.ts:
--------------------------------------------------------------------------------
1 | import { ActionType } from '../action-types';
2 | import { Config, Query } from '../../interfaces';
3 |
4 | export interface GetConfigAction {
5 | type: ActionType.getConfig;
6 | payload: Config;
7 | }
8 |
9 | export interface UpdateConfigAction {
10 | type: ActionType.updateConfig;
11 | payload: Config;
12 | }
13 |
14 | export interface FetchQueriesAction {
15 | type: ActionType.fetchQueries;
16 | payload: Query[];
17 | }
18 |
19 | export interface AddQueryAction {
20 | type: ActionType.addQuery;
21 | payload: Query;
22 | }
23 |
24 | export interface DeleteQueryAction {
25 | type: ActionType.deleteQuery;
26 | payload: Query[];
27 | }
28 |
29 | export interface UpdateQueryAction {
30 | type: ActionType.updateQuery;
31 | payload: Query[];
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/store/actions/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from '../../interfaces';
2 |
3 | import {
4 | AddThemeAction,
5 | DeleteThemeAction,
6 | EditThemeAction,
7 | FetchThemesAction,
8 | SetThemeAction,
9 | UpdateThemeAction,
10 | } from './theme';
11 |
12 | import {
13 | AddQueryAction,
14 | DeleteQueryAction,
15 | FetchQueriesAction,
16 | GetConfigAction,
17 | UpdateConfigAction,
18 | UpdateQueryAction,
19 | } from './config';
20 |
21 | import {
22 | ClearNotificationAction,
23 | CreateNotificationAction,
24 | } from './notification';
25 |
26 | import {
27 | GetAppsAction,
28 | PinAppAction,
29 | AddAppAction,
30 | DeleteAppAction,
31 | UpdateAppAction,
32 | ReorderAppsAction,
33 | SortAppsAction,
34 | SetEditAppAction,
35 | } from './app';
36 |
37 | import {
38 | GetCategoriesAction,
39 | AddCategoryAction,
40 | PinCategoryAction,
41 | DeleteCategoryAction,
42 | UpdateCategoryAction,
43 | SortCategoriesAction,
44 | ReorderCategoriesAction,
45 | AddBookmarkAction,
46 | DeleteBookmarkAction,
47 | UpdateBookmarkAction,
48 | SetEditCategoryAction,
49 | SetEditBookmarkAction,
50 | ReorderBookmarksAction,
51 | SortBookmarksAction,
52 | } from './bookmark';
53 |
54 | import {
55 | AuthErrorAction,
56 | AutoLoginAction,
57 | LoginAction,
58 | LogoutAction,
59 | } from './auth';
60 |
61 | export type Action =
62 | // Theme
63 | | SetThemeAction
64 | | FetchThemesAction
65 | | AddThemeAction
66 | | DeleteThemeAction
67 | | UpdateThemeAction
68 | | EditThemeAction
69 | // Config
70 | | GetConfigAction
71 | | UpdateConfigAction
72 | | AddQueryAction
73 | | DeleteQueryAction
74 | | FetchQueriesAction
75 | | UpdateQueryAction
76 | // Notifications
77 | | CreateNotificationAction
78 | | ClearNotificationAction
79 | // Apps
80 | | GetAppsAction
81 | | PinAppAction
82 | | AddAppAction
83 | | DeleteAppAction
84 | | UpdateAppAction
85 | | ReorderAppsAction
86 | | SortAppsAction
87 | | SetEditAppAction
88 | // Categories
89 | | GetCategoriesAction
90 | | AddCategoryAction
91 | | PinCategoryAction
92 | | DeleteCategoryAction
93 | | UpdateCategoryAction
94 | | SortCategoriesAction
95 | | ReorderCategoriesAction
96 | | SetEditCategoryAction
97 | // Bookmarks
98 | | AddBookmarkAction
99 | | DeleteBookmarkAction
100 | | UpdateBookmarkAction
101 | | SetEditBookmarkAction
102 | | ReorderBookmarksAction
103 | | SortBookmarksAction
104 | // Auth
105 | | LoginAction
106 | | LogoutAction
107 | | AutoLoginAction
108 | | AuthErrorAction;
109 |
--------------------------------------------------------------------------------
/client/src/store/actions/notification.ts:
--------------------------------------------------------------------------------
1 | import { ActionType } from '../action-types';
2 | import { NewNotification } from '../../interfaces';
3 |
4 | export interface CreateNotificationAction {
5 | type: ActionType.createNotification;
6 | payload: NewNotification;
7 | }
8 |
9 | export interface ClearNotificationAction {
10 | type: ActionType.clearNotification;
11 | payload: number;
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/store/actions/theme.ts:
--------------------------------------------------------------------------------
1 | import { ActionType } from '../action-types';
2 | import { Theme, ThemeColors } from '../../interfaces';
3 |
4 | export interface SetThemeAction {
5 | type: ActionType.setTheme;
6 | payload: ThemeColors;
7 | }
8 |
9 | export interface FetchThemesAction {
10 | type: ActionType.fetchThemes;
11 | payload: Theme[];
12 | }
13 |
14 | export interface AddThemeAction {
15 | type: ActionType.addTheme;
16 | payload: Theme;
17 | }
18 |
19 | export interface DeleteThemeAction {
20 | type: ActionType.deleteTheme;
21 | payload: Theme[];
22 | }
23 |
24 | export interface UpdateThemeAction {
25 | type: ActionType.updateTheme;
26 | payload: Theme[];
27 | }
28 |
29 | export interface EditThemeAction {
30 | type: ActionType.editTheme;
31 | payload: Theme | null;
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/store/index.ts:
--------------------------------------------------------------------------------
1 | export * from './store';
2 | export * as actionCreators from './action-creators';
3 |
--------------------------------------------------------------------------------
/client/src/store/reducers/auth.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../actions';
2 | import { ActionType } from '../action-types';
3 |
4 | interface AuthState {
5 | isAuthenticated: boolean;
6 | token: string | null;
7 | }
8 |
9 | const initialState: AuthState = {
10 | isAuthenticated: false,
11 | token: null,
12 | };
13 |
14 | export const authReducer = (
15 | state: AuthState = initialState,
16 | action: Action
17 | ): AuthState => {
18 | switch (action.type) {
19 | case ActionType.login:
20 | return {
21 | ...state,
22 | token: action.payload,
23 | isAuthenticated: true,
24 | };
25 |
26 | case ActionType.logout:
27 | return {
28 | ...state,
29 | token: null,
30 | isAuthenticated: false,
31 | };
32 |
33 | case ActionType.autoLogin:
34 | return {
35 | ...state,
36 | token: action.payload,
37 | isAuthenticated: true,
38 | };
39 |
40 | case ActionType.authError:
41 | return {
42 | ...state,
43 | token: null,
44 | isAuthenticated: false,
45 | };
46 |
47 | default:
48 | return state;
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/client/src/store/reducers/config.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../actions';
2 | import { ActionType } from '../action-types';
3 | import { Config, Query } from '../../interfaces';
4 | import { configTemplate } from '../../utility';
5 |
6 | interface ConfigState {
7 | loading: boolean;
8 | config: Config;
9 | customQueries: Query[];
10 | }
11 |
12 | const initialState: ConfigState = {
13 | loading: true,
14 | config: { ...configTemplate },
15 | customQueries: [],
16 | };
17 |
18 | export const configReducer = (
19 | state: ConfigState = initialState,
20 | action: Action
21 | ): ConfigState => {
22 | switch (action.type) {
23 | case ActionType.getConfig:
24 | return {
25 | ...state,
26 | loading: false,
27 | config: action.payload,
28 | };
29 |
30 | case ActionType.updateConfig:
31 | return {
32 | ...state,
33 | config: action.payload,
34 | };
35 |
36 | case ActionType.fetchQueries:
37 | return {
38 | ...state,
39 | customQueries: action.payload,
40 | };
41 |
42 | case ActionType.addQuery:
43 | return {
44 | ...state,
45 | customQueries: [...state.customQueries, action.payload],
46 | };
47 |
48 | case ActionType.deleteQuery:
49 | return {
50 | ...state,
51 | customQueries: action.payload,
52 | };
53 |
54 | case ActionType.updateQuery:
55 | return {
56 | ...state,
57 | customQueries: action.payload,
58 | };
59 |
60 | default:
61 | return state;
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/client/src/store/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 |
3 | import { themeReducer } from './theme';
4 | import { configReducer } from './config';
5 | import { notificationReducer } from './notification';
6 | import { appsReducer } from './app';
7 | import { bookmarksReducer } from './bookmark';
8 | import { authReducer } from './auth';
9 |
10 | export const reducers = combineReducers({
11 | theme: themeReducer,
12 | config: configReducer,
13 | notification: notificationReducer,
14 | apps: appsReducer,
15 | bookmarks: bookmarksReducer,
16 | auth: authReducer,
17 | });
18 |
19 | export type State = ReturnType;
20 |
--------------------------------------------------------------------------------
/client/src/store/reducers/notification.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../actions';
2 | import { ActionType } from '../action-types';
3 | import { Notification } from '../../interfaces';
4 |
5 | export interface NotificationState {
6 | notifications: Notification[];
7 | idCounter: number;
8 | }
9 |
10 | const initialState: NotificationState = {
11 | notifications: [],
12 | idCounter: 0,
13 | };
14 |
15 | export const notificationReducer = (
16 | state: NotificationState = initialState,
17 | action: Action
18 | ): NotificationState => {
19 | switch (action.type) {
20 | case ActionType.createNotification:
21 | return {
22 | ...state,
23 | notifications: [
24 | ...state.notifications,
25 | {
26 | ...action.payload,
27 | id: state.idCounter,
28 | },
29 | ],
30 | idCounter: state.idCounter + 1,
31 | };
32 |
33 | case ActionType.clearNotification:
34 | return {
35 | ...state,
36 | notifications: [...state.notifications].filter(
37 | (notification) => notification.id !== action.payload
38 | ),
39 | };
40 | default:
41 | return state;
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/client/src/store/reducers/theme.ts:
--------------------------------------------------------------------------------
1 | import { Action } from '../actions';
2 | import { ActionType } from '../action-types';
3 | import { Theme } from '../../interfaces/Theme';
4 | import { arrayPartition, parsePABToTheme } from '../../utility';
5 |
6 | interface ThemeState {
7 | activeTheme: Theme;
8 | themes: Theme[];
9 | userThemes: Theme[];
10 | themeInEdit: Theme | null;
11 | }
12 |
13 | const savedTheme = localStorage.theme
14 | ? parsePABToTheme(localStorage.theme)
15 | : parsePABToTheme('#effbff;#6ee2ff;#242b33');
16 |
17 | const initialState: ThemeState = {
18 | activeTheme: {
19 | name: 'main',
20 | isCustom: false,
21 | colors: {
22 | ...savedTheme,
23 | },
24 | },
25 | themes: [],
26 | userThemes: [],
27 | themeInEdit: null,
28 | };
29 |
30 | export const themeReducer = (
31 | state: ThemeState = initialState,
32 | action: Action
33 | ): ThemeState => {
34 | switch (action.type) {
35 | case ActionType.setTheme: {
36 | return {
37 | ...state,
38 | activeTheme: {
39 | ...state.activeTheme,
40 | colors: action.payload,
41 | },
42 | };
43 | }
44 |
45 | case ActionType.fetchThemes: {
46 | const [themes, userThemes] = arrayPartition(
47 | action.payload,
48 | (e) => !e.isCustom
49 | );
50 |
51 | return {
52 | ...state,
53 | themes,
54 | userThemes,
55 | };
56 | }
57 |
58 | case ActionType.addTheme: {
59 | return {
60 | ...state,
61 | userThemes: [...state.userThemes, action.payload],
62 | };
63 | }
64 |
65 | case ActionType.deleteTheme: {
66 | return {
67 | ...state,
68 | userThemes: action.payload,
69 | };
70 | }
71 |
72 | case ActionType.editTheme: {
73 | return {
74 | ...state,
75 | themeInEdit: action.payload,
76 | };
77 | }
78 |
79 | case ActionType.updateTheme: {
80 | return {
81 | ...state,
82 | userThemes: action.payload,
83 | };
84 | }
85 |
86 | default:
87 | return state;
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/client/src/store/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import { composeWithDevTools } from 'redux-devtools-extension';
3 | import thunk from 'redux-thunk';
4 | import { reducers } from './reducers';
5 |
6 | export const store = createStore(
7 | reducers,
8 | {},
9 | composeWithDevTools(applyMiddleware(thunk))
10 | );
11 |
--------------------------------------------------------------------------------
/client/src/types/ConfigFormData.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DockerSettingsForm,
3 | UISettingsForm,
4 | GeneralForm,
5 | ThemeSettingsForm,
6 | WeatherForm,
7 | } from '../interfaces';
8 |
9 | export type ConfigFormData =
10 | | WeatherForm
11 | | GeneralForm
12 | | DockerSettingsForm
13 | | UISettingsForm
14 | | ThemeSettingsForm;
15 |
--------------------------------------------------------------------------------
/client/src/types/WeatherData.ts:
--------------------------------------------------------------------------------
1 | export type WeatherData = 'cloud' | 'humidity';
2 |
--------------------------------------------------------------------------------
/client/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ConfigFormData';
2 | export * from './WeatherData';
3 |
--------------------------------------------------------------------------------
/client/src/utility/applyAuth.ts:
--------------------------------------------------------------------------------
1 | export const applyAuth = () => {
2 | const token = localStorage.getItem('token') || '';
3 | return { 'Authorization-Flame': `Bearer ${token}` };
4 | };
5 |
--------------------------------------------------------------------------------
/client/src/utility/arrayPartition.ts:
--------------------------------------------------------------------------------
1 | export const arrayPartition = (
2 | arr: T[],
3 | isValid: (e: T) => boolean
4 | ): T[][] => {
5 | let pass: T[] = [];
6 | let fail: T[] = [];
7 |
8 | arr.forEach((e) => (isValid(e) ? pass : fail).push(e));
9 |
10 | return [pass, fail];
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/utility/checkVersion.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { store } from '../store/store';
3 | import { createNotification } from '../store/action-creators';
4 |
5 | export const checkVersion = async (isForced: boolean = false) => {
6 | try {
7 | const res = await axios.get(
8 | 'https://raw.githubusercontent.com/pawelmalak/flame/master/client/.env'
9 | );
10 |
11 | const githubVersion = res.data
12 | .split('\n')
13 | .map((pair) => pair.split('='))[0][1];
14 |
15 | if (githubVersion !== process.env.REACT_APP_VERSION) {
16 | store.dispatch(
17 | createNotification({
18 | title: 'Info',
19 | message: 'New version is available!',
20 | url: 'https://github.com/pawelmalak/flame/blob/master/CHANGELOG.md',
21 | })
22 | );
23 | } else if (isForced) {
24 | store.dispatch(
25 | createNotification({
26 | title: 'Info',
27 | message: 'You are using the latest version!',
28 | })
29 | );
30 | }
31 | } catch (err) {
32 | console.log(err);
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/client/src/utility/decodeToken.ts:
--------------------------------------------------------------------------------
1 | import jwtDecode from 'jwt-decode';
2 | import { parseTime } from '.';
3 | import { Token } from '../interfaces';
4 |
5 | export const decodeToken = (token: string): Token => {
6 | const decoded = jwtDecode(token) as Token;
7 |
8 | return decoded;
9 | };
10 |
11 | export const parseTokenExpire = (expiresIn: number): string => {
12 | const d = new Date(expiresIn * 1000);
13 | const p = parseTime;
14 |
15 | const useAmericanDate = localStorage.useAmericanDate === 'true';
16 | const time = `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
17 |
18 | if (useAmericanDate) {
19 | return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()} ${time}`;
20 | } else {
21 | return `${d.getDate()}/${d.getMonth() + 1}/${d.getFullYear()} ${time}`;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/utility/escapeRegex.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * https://stackoverflow.com/a/30851002/16957052
3 | */
4 | export const escapeRegex = (exp: string) => {
5 | return exp.replace(/[-[\]{}()*+!<=:?.\/\\^$|#\s,]/g, '\\$&');
6 | };
7 |
--------------------------------------------------------------------------------
/client/src/utility/iconParser.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Parse Material Desgin icon name to be used with mdi/js
3 | * @param mdiName Dash separated icon name from MDI, e.g. alert-box-outline
4 | * @returns Parsed icon name to be used with mdi/js, e.g mdiAlertBoxOutline
5 | */
6 | export const iconParser = (mdiName: string): string => {
7 | let parsedName = mdiName
8 | .split('-')
9 | .map((word: string) => `${word[0].toUpperCase()}${word.slice(1)}`)
10 | .join('');
11 | parsedName = `mdi${parsedName}`;
12 |
13 | return parsedName;
14 | }
--------------------------------------------------------------------------------
/client/src/utility/index.ts:
--------------------------------------------------------------------------------
1 | export * from './iconParser';
2 | export * from './urlParser';
3 | export * from './checkVersion';
4 | export * from './sortData';
5 | export * from './searchParser';
6 | export * from './redirectUrl';
7 | export * from './templateObjects';
8 | export * from './inputHandler';
9 | export * from './storeUIConfig';
10 | export * from './validators';
11 | export * from './parseTime';
12 | export * from './decodeToken';
13 | export * from './applyAuth';
14 | export * from './escapeRegex';
15 | export * from './parseTheme';
16 | export * from './arrayPartition';
17 |
--------------------------------------------------------------------------------
/client/src/utility/inputHandler.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, SetStateAction } from 'react';
2 |
3 | type Event = ChangeEvent;
4 |
5 | interface Options {
6 | isNumber?: boolean;
7 | isBool?: boolean;
8 | }
9 |
10 | interface Params {
11 | e: Event;
12 | options?: Options;
13 | setStateHandler: (v: SetStateAction) => void;
14 | state: T;
15 | }
16 |
17 | export const inputHandler = (params: Params): void => {
18 | const { e, options, setStateHandler, state } = params;
19 |
20 | const rawValue = e.target.value;
21 | let value: string | number | boolean = e.target.value;
22 |
23 | if (options) {
24 | const { isNumber = false, isBool = false } = options;
25 |
26 | if (isNumber) {
27 | value = parseFloat(rawValue);
28 | }
29 |
30 | if (isBool) {
31 | value = !!parseInt(rawValue);
32 | }
33 | }
34 |
35 | setStateHandler({
36 | ...state,
37 | [e.target.name]: value,
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/client/src/utility/parseTheme.ts:
--------------------------------------------------------------------------------
1 | import { ThemeColors } from '../interfaces';
2 |
3 | // parse theme in PAB (primary;accent;background) format to theme colors object
4 | export const parsePABToTheme = (themeStr: string): ThemeColors => {
5 | const [primary, accent, background] = themeStr.split(';');
6 |
7 | return {
8 | primary,
9 | accent,
10 | background,
11 | };
12 | };
13 |
14 | export const parseThemeToPAB = ({
15 | primary: p,
16 | accent: a,
17 | background: b,
18 | }: ThemeColors): string => {
19 | return `${p};${a};${b}`;
20 | };
21 |
--------------------------------------------------------------------------------
/client/src/utility/parseTime.ts:
--------------------------------------------------------------------------------
1 | export const parseTime = (time: number, ms = false) => {
2 | if (ms) {
3 | if (time >= 10 && time < 100) {
4 | return `0${time}`;
5 | } else if (time < 10) {
6 | return `00${time}`;
7 | }
8 | }
9 |
10 | return time < 10 ? `0${time}` : time.toString();
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/utility/redirectUrl.ts:
--------------------------------------------------------------------------------
1 | import { urlParser } from '.';
2 |
3 | export const redirectUrl = (url: string, sameTab: boolean) => {
4 | const parsedUrl = urlParser(url)[1];
5 |
6 | if (sameTab) {
7 | document.location.assign(parsedUrl);
8 | } else {
9 | window.open(parsedUrl);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/client/src/utility/searchParser.ts:
--------------------------------------------------------------------------------
1 | import searchQueries from './searchQueries.json';
2 | import { SearchResult } from '../interfaces';
3 | import { store } from '../store/store';
4 | import { isUrlOrIp } from '.';
5 |
6 | export const searchParser = (searchQuery: string): SearchResult => {
7 | const queries = searchQueries.queries;
8 |
9 | const result: SearchResult = {
10 | isLocal: false,
11 | isURL: false,
12 | sameTab: false,
13 | encodedURL: '',
14 | primarySearch: {
15 | name: '',
16 | prefix: '',
17 | template: '',
18 | },
19 | secondarySearch: {
20 | name: '',
21 | prefix: '',
22 | template: '',
23 | },
24 | rawQuery: searchQuery,
25 | };
26 |
27 | const { customQueries, config } = store.getState().config;
28 |
29 | // Check if url or ip was passed
30 | result.isURL = isUrlOrIp(searchQuery);
31 |
32 | // Match prefix and query
33 | const splitQuery = searchQuery.match(/^\/([a-z]+)[ ](.+)$/i);
34 |
35 | // Extract prefix
36 | const prefix = splitQuery ? splitQuery[1] : config.defaultSearchProvider;
37 |
38 | // Encode url
39 | const encodedURL = splitQuery
40 | ? encodeURIComponent(splitQuery[2])
41 | : encodeURIComponent(searchQuery);
42 |
43 | // Find primary search engine template
44 | const findProvider = (prefix: string) => {
45 | return [...queries, ...customQueries].find((q) => q.prefix === prefix);
46 | };
47 |
48 | const primarySearch = findProvider(prefix);
49 | const secondarySearch = findProvider(config.secondarySearchProvider);
50 |
51 | // If search providers were found
52 | if (primarySearch) {
53 | result.primarySearch = primarySearch;
54 | result.encodedURL = encodedURL;
55 |
56 | if (prefix === 'l') {
57 | result.isLocal = true;
58 | }
59 |
60 | result.sameTab = config.searchSameTab;
61 |
62 | if (secondarySearch) {
63 | result.secondarySearch = secondarySearch;
64 | }
65 |
66 | return result;
67 | }
68 |
69 | return result;
70 | };
71 |
--------------------------------------------------------------------------------
/client/src/utility/searchQueries.json:
--------------------------------------------------------------------------------
1 | {
2 | "queries": [
3 | {
4 | "name": "Deezer",
5 | "prefix": "dz",
6 | "template": "https://www.deezer.com/search/"
7 | },
8 | {
9 | "name": "Disroot",
10 | "prefix": "ds",
11 | "template": "http://search.disroot.org/search?q="
12 | },
13 | {
14 | "name": "DuckDuckGo",
15 | "prefix": "d",
16 | "template": "https://duckduckgo.com/?q="
17 | },
18 | {
19 | "name": "Google",
20 | "prefix": "g",
21 | "template": "https://www.google.com/search?q="
22 | },
23 | {
24 | "name": "IMDb",
25 | "prefix": "im",
26 | "template": "https://www.imdb.com/find?q="
27 | },
28 | {
29 | "name": "Local search",
30 | "prefix": "l",
31 | "template": "#"
32 | },
33 | {
34 | "name": "Reddit",
35 | "prefix": "r",
36 | "template": "https://www.reddit.com/search?q="
37 | },
38 | {
39 | "name": "Spotify",
40 | "prefix": "sp",
41 | "template": "https://open.spotify.com/search/"
42 | },
43 | {
44 | "name": "The Movie Database",
45 | "prefix": "mv",
46 | "template": "https://www.themoviedb.org/search?query="
47 | },
48 | {
49 | "name": "Tidal",
50 | "prefix": "td",
51 | "template": "https://listen.tidal.com/search?q="
52 | },
53 | {
54 | "name": "Wikipedia",
55 | "prefix": "w",
56 | "template": "https://en.wikipedia.org/w/index.php?search="
57 | },
58 | {
59 | "name": "YouTube",
60 | "prefix": "yt",
61 | "template": "https://www.youtube.com/results?search_query="
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/client/src/utility/sortData.ts:
--------------------------------------------------------------------------------
1 | interface Data {
2 | name: string;
3 | orderId: number;
4 | createdAt: Date;
5 | }
6 |
7 | export const sortData = (array: T[], field: string): T[] => {
8 | const sortedData = array.slice();
9 |
10 | if (field === 'name') {
11 | sortedData.sort((a: T, b: T) => {
12 | return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' })
13 | })
14 | } else if (field === 'orderId') {
15 | sortedData.sort((a: T, b: T) => {
16 | if (a.orderId < b.orderId) { return -1 }
17 | if (a.orderId > b.orderId) { return 1 }
18 | return 0;
19 | })
20 | } else {
21 | sortedData.sort((a: T, b: T) => {
22 | if (a.createdAt < b.createdAt) { return -1 }
23 | if (a.createdAt > b.createdAt) { return 1 }
24 | return 0;
25 | })
26 | }
27 |
28 | return sortedData;
29 | }
--------------------------------------------------------------------------------
/client/src/utility/storeUIConfig.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../interfaces';
2 |
3 | export const storeUIConfig = (
4 | key: K,
5 | config: Config
6 | ) => {
7 | localStorage.setItem(key, `${config[key]}`);
8 | };
9 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/appTemplate.ts:
--------------------------------------------------------------------------------
1 | import { App, NewApp } from '../../interfaces';
2 |
3 | export const newAppTemplate: NewApp = {
4 | name: '',
5 | url: '',
6 | icon: '',
7 | isPublic: true,
8 | description: '',
9 | };
10 |
11 | export const appTemplate: App = {
12 | ...newAppTemplate,
13 | isPinned: false,
14 | orderId: 0,
15 | id: -1,
16 | createdAt: new Date(),
17 | updatedAt: new Date(),
18 | };
19 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/bookmarkTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Bookmark, NewBookmark } from '../../interfaces';
2 |
3 | export const newBookmarkTemplate: NewBookmark = {
4 | name: '',
5 | url: '',
6 | categoryId: -1,
7 | icon: '',
8 | isPublic: true,
9 | };
10 |
11 | export const bookmarkTemplate: Bookmark = {
12 | ...newBookmarkTemplate,
13 | id: -1,
14 | createdAt: new Date(),
15 | updatedAt: new Date(),
16 | orderId: 0,
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/categoryTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Category, NewCategory } from '../../interfaces';
2 |
3 | export const newCategoryTemplate: NewCategory = {
4 | name: '',
5 | isPublic: true,
6 | };
7 |
8 | export const categoryTemplate: Category = {
9 | ...newCategoryTemplate,
10 | id: -1,
11 | isPinned: false,
12 | orderId: 0,
13 | bookmarks: [],
14 | createdAt: new Date(),
15 | updatedAt: new Date(),
16 | };
17 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/configTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../../interfaces';
2 |
3 | export const configTemplate: Config = {
4 | WEATHER_API_KEY: '',
5 | lat: 0,
6 | long: 0,
7 | isCelsius: true,
8 | customTitle: 'Flame',
9 | pinAppsByDefault: true,
10 | pinCategoriesByDefault: true,
11 | hideHeader: false,
12 | useOrdering: 'createdAt',
13 | appsSameTab: false,
14 | bookmarksSameTab: false,
15 | searchSameTab: false,
16 | hideApps: false,
17 | hideCategories: false,
18 | hideSearch: false,
19 | defaultSearchProvider: 'l',
20 | secondarySearchProvider: 'd',
21 | dockerApps: false,
22 | dockerHost: 'localhost',
23 | kubernetesApps: false,
24 | unpinStoppedApps: false,
25 | useAmericanDate: false,
26 | disableAutofocus: false,
27 | greetingsSchema: 'Good evening!;Good afternoon!;Good morning!;Good night!',
28 | daySchema: 'Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday',
29 | monthSchema:
30 | 'January;February;March;April;May;June;July;August;September;October;November;December',
31 | showTime: false,
32 | defaultTheme: 'tron',
33 | isKilometer: true,
34 | weatherData: 'cloud',
35 | hideDate: false,
36 | };
37 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/index.ts:
--------------------------------------------------------------------------------
1 | export * from './configTemplate';
2 | export * from './settingsTemplate';
3 | export * from './appTemplate';
4 | export * from './categoryTemplate';
5 | export * from './bookmarkTemplate';
6 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/settingsTemplate.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DockerSettingsForm,
3 | UISettingsForm,
4 | GeneralForm,
5 | ThemeSettingsForm,
6 | WeatherForm,
7 | } from '../../interfaces';
8 |
9 | export const uiSettingsTemplate: UISettingsForm = {
10 | customTitle: document.title,
11 | hideHeader: false,
12 | hideApps: false,
13 | hideCategories: false,
14 | useAmericanDate: false,
15 | greetingsSchema: 'Good evening!;Good afternoon!;Good morning!;Good night!',
16 | daySchema: 'Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday',
17 | monthSchema:
18 | 'January;February;March;April;May;June;July;August;September;October;November;December',
19 | showTime: false,
20 | hideDate: false,
21 | hideSearch: false,
22 | disableAutofocus: false,
23 | };
24 |
25 | export const weatherSettingsTemplate: WeatherForm = {
26 | WEATHER_API_KEY: '',
27 | lat: 0,
28 | long: 0,
29 | isCelsius: true,
30 | weatherData: 'cloud',
31 | };
32 |
33 | export const generalSettingsTemplate: GeneralForm = {
34 | searchSameTab: false,
35 | defaultSearchProvider: 'l',
36 | secondarySearchProvider: 'd',
37 | pinAppsByDefault: true,
38 | pinCategoriesByDefault: true,
39 | useOrdering: 'createdAt',
40 | appsSameTab: false,
41 | bookmarksSameTab: false,
42 | };
43 |
44 | export const dockerSettingsTemplate: DockerSettingsForm = {
45 | dockerApps: true,
46 | dockerHost: 'localhost',
47 | kubernetesApps: true,
48 | unpinStoppedApps: true,
49 | };
50 |
51 | export const themeSettingsTemplate: ThemeSettingsForm = {
52 | defaultTheme: 'tron',
53 | };
54 |
--------------------------------------------------------------------------------
/client/src/utility/templateObjects/weatherTemplate.ts:
--------------------------------------------------------------------------------
1 | import { Weather } from '../../interfaces';
2 |
3 | export const weatherTemplate: Weather = {
4 | externalLastUpdate: '',
5 | tempC: 0,
6 | tempF: 0,
7 | isDay: 1,
8 | cloud: 0,
9 | conditionText: '',
10 | conditionCode: 1000,
11 | id: -1,
12 | createdAt: new Date(),
13 | updatedAt: new Date(),
14 | humidity: 0,
15 | windK: 0,
16 | windM: 0,
17 | };
18 |
--------------------------------------------------------------------------------
/client/src/utility/urlParser.ts:
--------------------------------------------------------------------------------
1 | const hasProtocol = (url: string): boolean => /^\w+:\/\//.test(url);
2 | const isSteamUrl = (url: string): boolean => /^steam:\/\//.test(url);
3 | const isWebUrl = (url: string): boolean => /^https?:\/\//.test(url);
4 |
5 | export const urlParser = (url: string): string[] => {
6 | if (!hasProtocol(url)) {
7 | // No protocol -> apply http:// prefix
8 | url = `http://${url}`;
9 | }
10 |
11 | // Create simplified url to display as text
12 | let displayUrl: string;
13 | if (isSteamUrl(url)) {
14 | displayUrl = 'Run Steam App';
15 | } else if (isWebUrl(url)) {
16 | displayUrl = url
17 | .replace(/https?:\/\//, '')
18 | .replace('www.', '')
19 | .replace(/\/$/, '');
20 | } else displayUrl = url;
21 |
22 | return [displayUrl, url];
23 | };
24 |
--------------------------------------------------------------------------------
/client/src/utility/validators.ts:
--------------------------------------------------------------------------------
1 | export const isUrlOrIp = (data: string): boolean => {
2 | const regex =
3 | /^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|^((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/i;
4 |
5 | return regex.test(data);
6 | };
7 |
8 | export const isUrl = (data: string): boolean => {
9 | const regex =
10 | /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/i;
11 |
12 | return regex.test(data);
13 | };
14 |
15 | export const isImage = (data: string): boolean => {
16 | const regex = /.(jpeg|jpg|png|ico)$/i;
17 |
18 | return regex.test(data);
19 | };
20 |
21 | export const isSvg = (data: string): boolean => {
22 | const regex = /.(svg)$/i;
23 |
24 | return regex.test(data);
25 | };
26 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/controllers/apps/createApp.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 | const loadConfig = require('../../utils/loadConfig');
4 |
5 | // @desc Create new app
6 | // @route POST /api/apps
7 | // @access Public
8 | const createApp = asyncWrapper(async (req, res, next) => {
9 | const { pinAppsByDefault } = await loadConfig();
10 |
11 | let body = { ...req.body };
12 |
13 | if (body.icon) {
14 | body.icon = body.icon.trim();
15 | }
16 |
17 | if (req.file) {
18 | body.icon = req.file.filename;
19 | }
20 |
21 | const app = await App.create({
22 | ...body,
23 | isPinned: pinAppsByDefault,
24 | });
25 |
26 | res.status(201).json({
27 | success: true,
28 | data: app,
29 | });
30 | });
31 |
32 | module.exports = createApp;
33 |
--------------------------------------------------------------------------------
/controllers/apps/deleteApp.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 |
4 | // @desc Delete app
5 | // @route DELETE /api/apps/:id
6 | // @access Public
7 | const deleteApp = asyncWrapper(async (req, res, next) => {
8 | await App.destroy({
9 | where: { id: req.params.id },
10 | });
11 |
12 | res.status(200).json({
13 | success: true,
14 | data: {},
15 | });
16 | });
17 |
18 | module.exports = deleteApp;
19 |
--------------------------------------------------------------------------------
/controllers/apps/docker/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | useKubernetes: require('./useKubernetes'),
3 | useDocker: require('./useDocker'),
4 | };
5 |
--------------------------------------------------------------------------------
/controllers/apps/docker/useKubernetes.js:
--------------------------------------------------------------------------------
1 | const App = require('../../../models/App');
2 | const k8s = require('@kubernetes/client-node');
3 | const Logger = require('../../../utils/Logger');
4 | const logger = new Logger();
5 | const loadConfig = require('../../../utils/loadConfig');
6 |
7 | const useKubernetes = async (apps) => {
8 | const { useOrdering: orderType, unpinStoppedApps } = await loadConfig();
9 |
10 | let ingresses = null;
11 |
12 | try {
13 | const kc = new k8s.KubeConfig();
14 | kc.loadFromCluster();
15 | const k8sNetworkingV1Api = kc.makeApiClient(k8s.NetworkingV1Api);
16 | await k8sNetworkingV1Api.listIngressForAllNamespaces().then((res) => {
17 | ingresses = res.body.items;
18 | });
19 | } catch {
20 | logger.log("Can't connect to the Kubernetes API", 'ERROR');
21 | }
22 |
23 | if (ingresses) {
24 | apps = await App.findAll({
25 | order: [[orderType, 'ASC']],
26 | });
27 |
28 | ingresses = ingresses.filter(
29 | (e) => Object.keys(e.metadata.annotations).length !== 0
30 | );
31 |
32 | const kubernetesApps = [];
33 |
34 | for (const ingress of ingresses) {
35 | const annotations = ingress.metadata.annotations;
36 |
37 | if (
38 | 'flame.pawelmalak/name' in annotations &&
39 | 'flame.pawelmalak/url' in annotations &&
40 | /^app/.test(annotations['flame.pawelmalak/type'])
41 | ) {
42 | kubernetesApps.push({
43 | name: annotations['flame.pawelmalak/name'],
44 | url: annotations['flame.pawelmalak/url'],
45 | icon: annotations['flame.pawelmalak/icon'] || 'kubernetes',
46 | });
47 | }
48 | }
49 |
50 | if (unpinStoppedApps) {
51 | for (const app of apps) {
52 | await app.update({ isPinned: false });
53 | }
54 | }
55 |
56 | for (const item of kubernetesApps) {
57 | if (apps.some((app) => app.name === item.name)) {
58 | const app = apps.find((a) => a.name === item.name);
59 | await app.update({ ...item, isPinned: true });
60 | } else {
61 | await App.create({
62 | ...item,
63 | isPinned: true,
64 | });
65 | }
66 | }
67 | }
68 | };
69 |
70 | module.exports = useKubernetes;
71 |
--------------------------------------------------------------------------------
/controllers/apps/getAllApps.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 | const { Sequelize } = require('sequelize');
4 | const loadConfig = require('../../utils/loadConfig');
5 |
6 | const { useKubernetes, useDocker } = require('./docker');
7 |
8 | // @desc Get all apps
9 | // @route GET /api/apps
10 | // @access Public
11 | const getAllApps = asyncWrapper(async (req, res, next) => {
12 | const {
13 | useOrdering: orderType,
14 | dockerApps: useDockerAPI,
15 | kubernetesApps: useKubernetesAPI,
16 | } = await loadConfig();
17 |
18 | let apps;
19 |
20 | if (useDockerAPI) {
21 | await useDocker(apps);
22 | }
23 |
24 | if (useKubernetesAPI) {
25 | await useKubernetes(apps);
26 | }
27 |
28 | // apps visibility
29 | const where = req.isAuthenticated ? {} : { isPublic: true };
30 |
31 | const order =
32 | orderType == 'name'
33 | ? [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']]
34 | : [[orderType, 'ASC']];
35 |
36 | apps = await App.findAll({
37 | order,
38 | where,
39 | });
40 |
41 | if (process.env.NODE_ENV === 'production') {
42 | // Set header to fetch containers info every time
43 | return res.status(200).setHeader('Cache-Control', 'no-store').json({
44 | success: true,
45 | data: apps,
46 | });
47 | }
48 |
49 | res.status(200).json({
50 | success: true,
51 | data: apps,
52 | });
53 | });
54 |
55 | module.exports = getAllApps;
56 |
--------------------------------------------------------------------------------
/controllers/apps/getSingleApp.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 | const ErrorResponse = require('../../utils/ErrorResponse');
4 |
5 | // @desc Get single app
6 | // @route GET /api/apps/:id
7 | // @access Public
8 | const getSingleApp = asyncWrapper(async (req, res, next) => {
9 | const visibility = req.isAuthenticated ? {} : { isPublic: true };
10 |
11 | const app = await App.findOne({
12 | where: { id: req.params.id, ...visibility },
13 | });
14 |
15 | if (!app) {
16 | return next(
17 | new ErrorResponse(
18 | `App with the id of ${req.params.id} was not found`,
19 | 404
20 | )
21 | );
22 | }
23 |
24 | res.status(200).json({
25 | success: true,
26 | data: app,
27 | });
28 | });
29 |
30 | module.exports = getSingleApp;
31 |
--------------------------------------------------------------------------------
/controllers/apps/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | createApp: require('./createApp'),
3 | getSingleApp: require('./getSingleApp'),
4 | deleteApp: require('./deleteApp'),
5 | updateApp: require('./updateApp'),
6 | reorderApps: require('./reorderApps'),
7 | getAllApps: require('./getAllApps'),
8 | };
9 |
--------------------------------------------------------------------------------
/controllers/apps/reorderApps.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 |
4 | // @desc Reorder apps
5 | // @route PUT /api/apps/0/reorder
6 | // @access Public
7 | const reorderApps = asyncWrapper(async (req, res, next) => {
8 | req.body.apps.forEach(async ({ id, orderId }) => {
9 | await App.update(
10 | { orderId },
11 | {
12 | where: { id },
13 | }
14 | );
15 | });
16 |
17 | res.status(200).json({
18 | success: true,
19 | data: {},
20 | });
21 | });
22 |
23 | module.exports = reorderApps;
24 |
--------------------------------------------------------------------------------
/controllers/apps/updateApp.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const App = require('../../models/App');
3 |
4 | // @desc Update app
5 | // @route PUT /api/apps/:id
6 | // @access Public
7 | const updateApp = asyncWrapper(async (req, res, next) => {
8 | let app = await App.findOne({
9 | where: { id: req.params.id },
10 | });
11 |
12 | if (!app) {
13 | return next(
14 | new ErrorResponse(
15 | `App with the id of ${req.params.id} was not found`,
16 | 404
17 | )
18 | );
19 | }
20 |
21 | let body = { ...req.body };
22 |
23 | if (body.icon) {
24 | body.icon = body.icon.trim();
25 | }
26 |
27 | if (req.file) {
28 | body.icon = req.file.filename;
29 | }
30 |
31 | app = await app.update(body);
32 |
33 | res.status(200).json({
34 | success: true,
35 | data: app,
36 | });
37 | });
38 |
39 | module.exports = updateApp;
40 |
--------------------------------------------------------------------------------
/controllers/auth/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | login: require('./login'),
3 | validate: require('./validate'),
4 | };
5 |
--------------------------------------------------------------------------------
/controllers/auth/login.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const signToken = require('../../utils/signToken');
4 |
5 | // @desc Login user
6 | // @route POST /api/auth/
7 | // @access Public
8 | const login = asyncWrapper(async (req, res, next) => {
9 | const { password, duration } = req.body;
10 |
11 | const isMatch = process.env.PASSWORD == password;
12 |
13 | if (!isMatch) {
14 | return next(new ErrorResponse('Invalid credentials', 401));
15 | }
16 |
17 | const token = signToken(duration);
18 |
19 | res.status(200).json({
20 | success: true,
21 | data: { token },
22 | });
23 | });
24 |
25 | module.exports = login;
26 |
--------------------------------------------------------------------------------
/controllers/auth/validate.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const jwt = require('jsonwebtoken');
4 |
5 | // @desc Verify token
6 | // @route POST /api/auth/verify
7 | // @access Public
8 | const validate = asyncWrapper(async (req, res, next) => {
9 | try {
10 | jwt.verify(req.body.token, process.env.SECRET);
11 |
12 | res.status(200).json({
13 | success: true,
14 | data: { token: { isValid: true } },
15 | });
16 | } catch (err) {
17 | return next(new ErrorResponse('Token expired', 401));
18 | }
19 | });
20 |
21 | module.exports = validate;
22 |
--------------------------------------------------------------------------------
/controllers/bookmarks/createBookmark.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Bookmark = require('../../models/Bookmark');
3 |
4 | // @desc Create new bookmark
5 | // @route POST /api/bookmarks
6 | // @access Public
7 | const createBookmark = asyncWrapper(async (req, res, next) => {
8 | let bookmark;
9 |
10 | let body = {
11 | ...req.body,
12 | categoryId: parseInt(req.body.categoryId),
13 | };
14 |
15 | if (body.icon) {
16 | body.icon = body.icon.trim();
17 | }
18 |
19 | if (req.file) {
20 | body.icon = req.file.filename;
21 | }
22 |
23 | bookmark = await Bookmark.create(body);
24 |
25 | res.status(201).json({
26 | success: true,
27 | data: bookmark,
28 | });
29 | });
30 |
31 | module.exports = createBookmark;
32 |
--------------------------------------------------------------------------------
/controllers/bookmarks/deleteBookmark.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Bookmark = require('../../models/Bookmark');
3 |
4 | // @desc Delete bookmark
5 | // @route DELETE /api/bookmarks/:id
6 | // @access Public
7 | const deleteBookmark = asyncWrapper(async (req, res, next) => {
8 | await Bookmark.destroy({
9 | where: { id: req.params.id },
10 | });
11 |
12 | res.status(200).json({
13 | success: true,
14 | data: {},
15 | });
16 | });
17 |
18 | module.exports = deleteBookmark;
19 |
--------------------------------------------------------------------------------
/controllers/bookmarks/getAllBookmarks.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Bookmark = require('../../models/Bookmark');
3 | const { Sequelize } = require('sequelize');
4 | const loadConfig = require('../../utils/loadConfig');
5 |
6 | // @desc Get all bookmarks
7 | // @route GET /api/bookmarks
8 | // @access Public
9 | const getAllBookmarks = asyncWrapper(async (req, res, next) => {
10 | const { useOrdering: orderType } = await loadConfig();
11 |
12 | // bookmarks visibility
13 | const where = req.isAuthenticated ? {} : { isPublic: true };
14 |
15 | const order =
16 | orderType == 'name'
17 | ? [[Sequelize.fn('lower', Sequelize.col('name')), 'ASC']]
18 | : [[orderType, 'ASC']];
19 |
20 | const bookmarks = await Bookmark.findAll({
21 | order,
22 | where,
23 | });
24 |
25 | res.status(200).json({
26 | success: true,
27 | data: bookmarks,
28 | });
29 | });
30 |
31 | module.exports = getAllBookmarks;
32 |
--------------------------------------------------------------------------------
/controllers/bookmarks/getSingleBookmark.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const Bookmark = require('../../models/Bookmark');
4 |
5 | // @desc Get single bookmark
6 | // @route GET /api/bookmarks/:id
7 | // @access Public
8 | const getSingleBookmark = asyncWrapper(async (req, res, next) => {
9 | const visibility = req.isAuthenticated ? {} : { isPublic: true };
10 |
11 | const bookmark = await Bookmark.findOne({
12 | where: { id: req.params.id, ...visibility },
13 | });
14 |
15 | if (!bookmark) {
16 | return next(
17 | new ErrorResponse(
18 | `Bookmark with the id of ${req.params.id} was not found`,
19 | 404
20 | )
21 | );
22 | }
23 |
24 | res.status(200).json({
25 | success: true,
26 | data: bookmark,
27 | });
28 | });
29 |
30 | module.exports = getSingleBookmark;
31 |
--------------------------------------------------------------------------------
/controllers/bookmarks/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | createBookmark: require('./createBookmark'),
3 | getAllBookmarks: require('./getAllBookmarks'),
4 | getSingleBookmark: require('./getSingleBookmark'),
5 | updateBookmark: require('./updateBookmark'),
6 | deleteBookmark: require('./deleteBookmark'),
7 | reorderBookmarks: require('./reorderBookmarks'),
8 | };
9 |
--------------------------------------------------------------------------------
/controllers/bookmarks/reorderBookmarks.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Bookmark = require('../../models/Bookmark');
3 |
4 | // @desc Reorder bookmarks
5 | // @route PUT /api/bookmarks/0/reorder
6 | // @access Public
7 | const reorderBookmarks = asyncWrapper(async (req, res, next) => {
8 | req.body.bookmarks.forEach(async ({ id, orderId }) => {
9 | await Bookmark.update(
10 | { orderId },
11 | {
12 | where: { id },
13 | }
14 | );
15 | });
16 |
17 | res.status(200).json({
18 | success: true,
19 | data: {},
20 | });
21 | });
22 |
23 | module.exports = reorderBookmarks;
24 |
--------------------------------------------------------------------------------
/controllers/bookmarks/updateBookmark.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const Bookmark = require('../../models/Bookmark');
4 |
5 | // @desc Update bookmark
6 | // @route PUT /api/bookmarks/:id
7 | // @access Public
8 | const updateBookmark = asyncWrapper(async (req, res, next) => {
9 | let bookmark = await Bookmark.findOne({
10 | where: { id: req.params.id },
11 | });
12 |
13 | if (!bookmark) {
14 | return next(
15 | new ErrorResponse(
16 | `Bookmark with id of ${req.params.id} was not found`,
17 | 404
18 | )
19 | );
20 | }
21 |
22 | let body = {
23 | ...req.body,
24 | categoryId: parseInt(req.body.categoryId),
25 | };
26 |
27 | if (body.icon) {
28 | body.icon = body.icon.trim();
29 | }
30 |
31 | if (req.file) {
32 | body.icon = req.file.filename;
33 | }
34 |
35 | bookmark = await bookmark.update(body);
36 |
37 | res.status(200).json({
38 | success: true,
39 | data: bookmark,
40 | });
41 | });
42 |
43 | module.exports = updateBookmark;
44 |
--------------------------------------------------------------------------------
/controllers/categories/createCategory.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Category = require('../../models/Category');
3 | const loadConfig = require('../../utils/loadConfig');
4 |
5 | // @desc Create new category
6 | // @route POST /api/categories
7 | // @access Public
8 | const createCategory = asyncWrapper(async (req, res, next) => {
9 | const { pinCategoriesByDefault: pinCategories } = await loadConfig();
10 |
11 | const category = await Category.create({
12 | ...req.body,
13 | isPinned: pinCategories,
14 | });
15 |
16 | res.status(201).json({
17 | success: true,
18 | data: category,
19 | });
20 | });
21 |
22 | module.exports = createCategory;
23 |
--------------------------------------------------------------------------------
/controllers/categories/deleteCategory.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const Category = require('../../models/Category');
4 | const Bookmark = require('../../models/Bookmark');
5 |
6 | // @desc Delete category
7 | // @route DELETE /api/categories/:id
8 | // @access Public
9 | const deleteCategory = asyncWrapper(async (req, res, next) => {
10 | const category = await Category.findOne({
11 | where: { id: req.params.id },
12 | include: [
13 | {
14 | model: Bookmark,
15 | as: 'bookmarks',
16 | },
17 | ],
18 | });
19 |
20 | if (!category) {
21 | return next(
22 | new ErrorResponse(
23 | `Category with id of ${req.params.id} was not found`,
24 | 404
25 | )
26 | );
27 | }
28 |
29 | category.bookmarks.forEach(async (bookmark) => {
30 | await Bookmark.destroy({
31 | where: { id: bookmark.id },
32 | });
33 | });
34 |
35 | await Category.destroy({
36 | where: { id: req.params.id },
37 | });
38 |
39 | res.status(200).json({
40 | success: true,
41 | data: {},
42 | });
43 | });
44 |
45 | module.exports = deleteCategory;
46 |
--------------------------------------------------------------------------------
/controllers/categories/getAllCategories.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Category = require('../../models/Category');
3 | const Bookmark = require('../../models/Bookmark');
4 | const { Sequelize } = require('sequelize');
5 | const loadConfig = require('../../utils/loadConfig');
6 |
7 | // @desc Get all categories
8 | // @route GET /api/categories
9 | // @access Public
10 | const getAllCategories = asyncWrapper(async (req, res, next) => {
11 | const { useOrdering: orderType } = await loadConfig();
12 |
13 | let categories;
14 | let output;
15 |
16 | // categories visibility
17 | const where = req.isAuthenticated ? {} : { isPublic: true };
18 |
19 | const order =
20 | orderType == 'name'
21 | ? [
22 | [Sequelize.fn('lower', Sequelize.col('Category.name')), 'ASC'],
23 | [Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC'],
24 | ]
25 | : [
26 | [orderType, 'ASC'],
27 | [{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC'],
28 | ];
29 |
30 | categories = categories = await Category.findAll({
31 | include: [
32 | {
33 | model: Bookmark,
34 | as: 'bookmarks',
35 | },
36 | ],
37 | order,
38 | where,
39 | });
40 |
41 | if (req.isAuthenticated) {
42 | output = categories;
43 | } else {
44 | // filter out private bookmarks
45 | output = categories.map((c) => c.get({ plain: true }));
46 | output = output.map((c) => ({
47 | ...c,
48 | bookmarks: c.bookmarks.filter((b) => b.isPublic),
49 | }));
50 | }
51 |
52 | res.status(200).json({
53 | success: true,
54 | data: output,
55 | });
56 | });
57 |
58 | module.exports = getAllCategories;
59 |
--------------------------------------------------------------------------------
/controllers/categories/getSingleCategory.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const Category = require('../../models/Category');
4 | const Bookmark = require('../../models/Bookmark');
5 | const { Sequelize } = require('sequelize');
6 | const loadConfig = require('../../utils/loadConfig');
7 |
8 | // @desc Get single category
9 | // @route GET /api/categories/:id
10 | // @access Public
11 | const getSingleCategory = asyncWrapper(async (req, res, next) => {
12 | const { useOrdering: orderType } = await loadConfig();
13 |
14 | const visibility = req.isAuthenticated ? {} : { isPublic: true };
15 |
16 | const order =
17 | orderType == 'name'
18 | ? [[Sequelize.fn('lower', Sequelize.col('bookmarks.name')), 'ASC']]
19 | : [[{ model: Bookmark, as: 'bookmarks' }, orderType, 'ASC']];
20 |
21 | const category = await Category.findOne({
22 | where: { id: req.params.id, ...visibility },
23 | include: [
24 | {
25 | model: Bookmark,
26 | as: 'bookmarks',
27 | where: visibility,
28 | },
29 | ],
30 | order,
31 | });
32 |
33 | if (!category) {
34 | return next(
35 | new ErrorResponse(
36 | `Category with id of ${req.params.id} was not found`,
37 | 404
38 | )
39 | );
40 | }
41 |
42 | res.status(200).json({
43 | success: true,
44 | data: category,
45 | });
46 | });
47 |
48 | module.exports = getSingleCategory;
49 |
--------------------------------------------------------------------------------
/controllers/categories/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | createCategory: require('./createCategory'),
3 | getAllCategories: require('./getAllCategories'),
4 | getSingleCategory: require('./getSingleCategory'),
5 | updateCategory: require('./updateCategory'),
6 | deleteCategory: require('./deleteCategory'),
7 | reorderCategories: require('./reorderCategories'),
8 | };
9 |
--------------------------------------------------------------------------------
/controllers/categories/reorderCategories.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Category = require('../../models/Category');
3 |
4 | // @desc Reorder categories
5 | // @route PUT /api/categories/0/reorder
6 | // @access Public
7 | const reorderCategories = asyncWrapper(async (req, res, next) => {
8 | req.body.categories.forEach(async ({ id, orderId }) => {
9 | await Category.update(
10 | { orderId },
11 | {
12 | where: { id },
13 | }
14 | );
15 | });
16 |
17 | res.status(200).json({
18 | success: true,
19 | data: {},
20 | });
21 | });
22 |
23 | module.exports = reorderCategories;
24 |
--------------------------------------------------------------------------------
/controllers/categories/updateCategory.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const Category = require('../../models/Category');
4 |
5 | // @desc Update category
6 | // @route PUT /api/categories/:id
7 | // @access Public
8 | const updateCategory = asyncWrapper(async (req, res, next) => {
9 | let category = await Category.findOne({
10 | where: { id: req.params.id },
11 | });
12 |
13 | if (!category) {
14 | return next(
15 | new ErrorResponse(
16 | `Category with id of ${req.params.id} was not found`,
17 | 404
18 | )
19 | );
20 | }
21 |
22 | category = await category.update({ ...req.body });
23 |
24 | res.status(200).json({
25 | success: true,
26 | data: category,
27 | });
28 | });
29 |
30 | module.exports = updateCategory;
31 |
--------------------------------------------------------------------------------
/controllers/config/getCSS.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 | const { join } = require('path');
4 |
5 | // @desc Get custom CSS file
6 | // @route GET /api/config/0/css
7 | // @access Public
8 | const getCSS = asyncWrapper(async (req, res, next) => {
9 | const file = new File(join(__dirname, '../../public/flame.css'));
10 | const content = file.read();
11 |
12 | res.status(200).json({
13 | success: true,
14 | data: content,
15 | });
16 | });
17 |
18 | module.exports = getCSS;
19 |
--------------------------------------------------------------------------------
/controllers/config/getConfig.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const loadConfig = require('../../utils/loadConfig');
3 |
4 | // @desc Get config
5 | // @route GET /api/config
6 | // @access Public
7 | const getConfig = asyncWrapper(async (req, res, next) => {
8 | const config = await loadConfig();
9 |
10 | res.status(200).json({
11 | success: true,
12 | data: config,
13 | });
14 | });
15 |
16 | module.exports = getConfig;
17 |
--------------------------------------------------------------------------------
/controllers/config/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | getCSS: require('./getCSS'),
3 | updateCSS: require('./updateCSS'),
4 | getConfig: require('./getConfig'),
5 | updateConfig: require('./updateConfig'),
6 | };
7 |
--------------------------------------------------------------------------------
/controllers/config/updateCSS.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 | const { join } = require('path');
4 | const fs = require('fs');
5 |
6 | // @desc Update custom CSS file
7 | // @route PUT /api/config/0/css
8 | // @access Public
9 | const updateCSS = asyncWrapper(async (req, res, next) => {
10 | const file = new File(join(__dirname, '../../public/flame.css'));
11 | file.write(req.body.styles, false);
12 |
13 | // Copy file to docker volume
14 | fs.copyFileSync(
15 | join(__dirname, '../../public/flame.css'),
16 | join(__dirname, '../../data/flame.css')
17 | );
18 |
19 | res.status(200).json({
20 | success: true,
21 | data: {},
22 | });
23 | });
24 |
25 | module.exports = updateCSS;
26 |
--------------------------------------------------------------------------------
/controllers/config/updateConfig.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const loadConfig = require('../../utils/loadConfig');
3 | const { writeFile } = require('fs/promises');
4 |
5 | // @desc Update config
6 | // @route PUT /api/config/
7 | // @access Public
8 | const updateConfig = asyncWrapper(async (req, res, next) => {
9 | const existingConfig = await loadConfig();
10 |
11 | const newConfig = {
12 | ...existingConfig,
13 | ...req.body,
14 | };
15 |
16 | await writeFile('data/config.json', JSON.stringify(newConfig));
17 |
18 | res.status(200).send({
19 | success: true,
20 | data: newConfig,
21 | });
22 | });
23 |
24 | module.exports = updateConfig;
25 |
--------------------------------------------------------------------------------
/controllers/queries/addQuery.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const File = require('../../utils/File');
4 |
5 | // @desc Add custom search query
6 | // @route POST /api/queries
7 | // @access Public
8 | const addQuery = asyncWrapper(async (req, res, next) => {
9 | const file = new File('data/customQueries.json');
10 | let content = JSON.parse(file.read());
11 |
12 | const prefixes = content.queries.map((q) => q.prefix);
13 |
14 | if (prefixes.includes(req.body.prefix)) {
15 | return next(new ErrorResponse('Prefix must be unique', 400));
16 | }
17 |
18 | // Add new query
19 | content.queries.push(req.body);
20 | file.write(content, true);
21 |
22 | res.status(201).json({
23 | success: true,
24 | data: req.body,
25 | });
26 | });
27 |
28 | module.exports = addQuery;
29 |
--------------------------------------------------------------------------------
/controllers/queries/deleteQuery.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Delete query
5 | // @route DELETE /api/queries/:prefix
6 | // @access Public
7 | const deleteQuery = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/customQueries.json');
9 | let content = JSON.parse(file.read());
10 |
11 | content.queries = content.queries.filter(
12 | (q) => q.prefix != req.params.prefix
13 | );
14 | file.write(content, true);
15 |
16 | res.status(200).json({
17 | success: true,
18 | data: content.queries,
19 | });
20 | });
21 |
22 | module.exports = deleteQuery;
23 |
--------------------------------------------------------------------------------
/controllers/queries/getQueries.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Get custom queries file
5 | // @route GET /api/queries
6 | // @access Public
7 | const getQueries = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/customQueries.json');
9 | const content = JSON.parse(file.read());
10 |
11 | res.status(200).json({
12 | success: true,
13 | data: content.queries,
14 | });
15 | });
16 |
17 | module.exports = getQueries;
18 |
--------------------------------------------------------------------------------
/controllers/queries/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | addQuery: require('./addQuery'),
3 | getQueries: require('./getQueries'),
4 | updateQuery: require('./updateQuery'),
5 | deleteQuery: require('./deleteQuery'),
6 | };
7 |
--------------------------------------------------------------------------------
/controllers/queries/updateQuery.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Update query
5 | // @route PUT /api/queries/:prefix
6 | // @access Public
7 | const updateQuery = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/customQueries.json');
9 | let content = JSON.parse(file.read());
10 |
11 | let queryIdx = content.queries.findIndex(
12 | (q) => q.prefix == req.params.prefix
13 | );
14 |
15 | // query found
16 | if (queryIdx > -1) {
17 | content.queries = [
18 | ...content.queries.slice(0, queryIdx),
19 | req.body,
20 | ...content.queries.slice(queryIdx + 1),
21 | ];
22 | }
23 |
24 | file.write(content, true);
25 |
26 | res.status(200).json({
27 | success: true,
28 | data: content.queries,
29 | });
30 | });
31 |
32 | module.exports = updateQuery;
33 |
--------------------------------------------------------------------------------
/controllers/themes/addTheme.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const ErrorResponse = require('../../utils/ErrorResponse');
3 | const File = require('../../utils/File');
4 |
5 | // @desc Create new theme
6 | // @route POST /api/themes
7 | // @access Private
8 | const addTheme = asyncWrapper(async (req, res, next) => {
9 | const file = new File('data/themes.json');
10 | let content = JSON.parse(file.read());
11 |
12 | const themeNames = content.themes.map((t) => t.name);
13 |
14 | if (themeNames.includes(req.body.name)) {
15 | return next(new ErrorResponse('Name must be unique', 400));
16 | }
17 |
18 | // Add new theme
19 | content.themes.push(req.body);
20 | file.write(content, true);
21 |
22 | res.status(201).json({
23 | success: true,
24 | data: req.body,
25 | });
26 | });
27 |
28 | module.exports = addTheme;
29 |
--------------------------------------------------------------------------------
/controllers/themes/deleteTheme.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Delete theme
5 | // @route DELETE /api/themes/:name
6 | // @access Public
7 | const deleteTheme = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/themes.json');
9 | let content = JSON.parse(file.read());
10 |
11 | content.themes = content.themes.filter((t) => t.name != req.params.name);
12 | file.write(content, true);
13 |
14 | const userThemes = content.themes.filter((t) => t.isCustom);
15 |
16 | res.status(200).json({
17 | success: true,
18 | data: userThemes,
19 | });
20 | });
21 |
22 | module.exports = deleteTheme;
23 |
--------------------------------------------------------------------------------
/controllers/themes/getThemes.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Get themes file
5 | // @route GET /api/themes
6 | // @access Public
7 | const getThemes = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/themes.json');
9 | const content = JSON.parse(file.read());
10 |
11 | res.status(200).json({
12 | success: true,
13 | data: content.themes,
14 | });
15 | });
16 |
17 | module.exports = getThemes;
18 |
--------------------------------------------------------------------------------
/controllers/themes/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | getThemes: require('./getThemes'),
3 | addTheme: require('./addTheme'),
4 | deleteTheme: require('./deleteTheme'),
5 | updateTheme: require('./updateTheme'),
6 | };
7 |
--------------------------------------------------------------------------------
/controllers/themes/updateTheme.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const File = require('../../utils/File');
3 |
4 | // @desc Update theme
5 | // @route PUT /api/themes/:name
6 | // @access Public
7 | const updateTheme = asyncWrapper(async (req, res, next) => {
8 | const file = new File('data/themes.json');
9 | let content = JSON.parse(file.read());
10 |
11 | let themeIdx = content.themes.findIndex((t) => t.name == req.params.name);
12 |
13 | // theme found
14 | if (themeIdx > -1) {
15 | content.themes = [
16 | ...content.themes.slice(0, themeIdx),
17 | req.body,
18 | ...content.themes.slice(themeIdx + 1),
19 | ];
20 | }
21 |
22 | file.write(content, true);
23 |
24 | const userThemes = content.themes.filter((t) => t.isCustom);
25 |
26 | res.status(200).json({
27 | success: true,
28 | data: userThemes,
29 | });
30 | });
31 |
32 | module.exports = updateTheme;
33 |
--------------------------------------------------------------------------------
/controllers/weather/getWather.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const Weather = require('../../models/Weather');
3 |
4 | // @desc Get latest weather status
5 | // @route GET /api/weather
6 | // @access Public
7 | const getWeather = asyncWrapper(async (req, res, next) => {
8 | const weather = await Weather.findAll({
9 | order: [['createdAt', 'DESC']],
10 | limit: 1,
11 | });
12 |
13 | res.status(200).json({
14 | success: true,
15 | data: weather,
16 | });
17 | });
18 |
19 | module.exports = getWeather;
20 |
--------------------------------------------------------------------------------
/controllers/weather/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | getWeather: require('./getWather'),
3 | updateWeather: require('./updateWeather'),
4 | };
5 |
--------------------------------------------------------------------------------
/controllers/weather/updateWeather.js:
--------------------------------------------------------------------------------
1 | const asyncWrapper = require('../../middleware/asyncWrapper');
2 | const getExternalWeather = require('../../utils/getExternalWeather');
3 |
4 | // @desc Update weather
5 | // @route GET /api/weather/update
6 | // @access Public
7 | const updateWeather = asyncWrapper(async (req, res, next) => {
8 | const weather = await getExternalWeather();
9 |
10 | res.status(200).json({
11 | success: true,
12 | data: weather,
13 | });
14 | });
15 |
16 | module.exports = updateWeather;
17 |
--------------------------------------------------------------------------------
/db/index.js:
--------------------------------------------------------------------------------
1 | const { Sequelize } = require('sequelize');
2 | const { join } = require('path');
3 | const Umzug = require('umzug');
4 |
5 | // Utils
6 | const backupDB = require('./utils/backupDb');
7 | const Logger = require('../utils/Logger');
8 | const logger = new Logger();
9 |
10 | const sequelize = new Sequelize({
11 | dialect: 'sqlite',
12 | storage: './data/db.sqlite',
13 | logging: false,
14 | });
15 |
16 | const umzug = new Umzug({
17 | migrations: {
18 | path: join(__dirname, './migrations'),
19 | params: [sequelize.getQueryInterface()],
20 | },
21 | storage: 'sequelize',
22 | storageOptions: {
23 | sequelize,
24 | },
25 | });
26 |
27 | const connectDB = async () => {
28 | try {
29 | backupDB();
30 |
31 | await sequelize.authenticate();
32 |
33 | // execute all pending migrations
34 | const pendingMigrations = await umzug.pending();
35 |
36 | if (pendingMigrations.length > 0) {
37 | logger.log('Executing pending migrations');
38 | await umzug.up();
39 | }
40 |
41 | logger.log('Connected to database');
42 | } catch (error) {
43 | logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR');
44 | process.exit(1);
45 | }
46 | };
47 |
48 | module.exports = {
49 | connectDB,
50 | sequelize,
51 | };
52 |
--------------------------------------------------------------------------------
/db/migrations/01_new-config.js:
--------------------------------------------------------------------------------
1 | const { readFile, writeFile, copyFile } = require('fs/promises');
2 | const Config = require('../../models/Config');
3 |
4 | const up = async (query) => {
5 | await copyFile('utils/init/initialConfig.json', 'data/config.json');
6 |
7 | const initConfigFile = await readFile('data/config.json', 'utf-8');
8 | const parsedNewConfig = JSON.parse(initConfigFile);
9 |
10 | const existingConfig = await Config.findAll({ raw: true });
11 |
12 | for (let pair of existingConfig) {
13 | const { key, value, valueType } = pair;
14 |
15 | let newValue = value;
16 |
17 | if (valueType == 'number') {
18 | newValue = parseFloat(value);
19 | } else if (valueType == 'boolean') {
20 | newValue = value == 1;
21 | }
22 |
23 | parsedNewConfig[key] = newValue;
24 | }
25 |
26 | const newConfig = JSON.stringify(parsedNewConfig);
27 | await writeFile('data/config.json', newConfig);
28 |
29 | await query.dropTable('config');
30 | };
31 |
32 | const down = async (query) => {};
33 |
34 | module.exports = {
35 | up,
36 | down,
37 | };
38 |
--------------------------------------------------------------------------------
/db/migrations/02_resource-access.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { INTEGER } = DataTypes;
3 |
4 | const tables = ['categories', 'bookmarks', 'apps'];
5 |
6 | const up = async (query) => {
7 | const template = {
8 | type: INTEGER,
9 | allowNull: true,
10 | defaultValue: 1,
11 | };
12 |
13 | for await (let table of tables) {
14 | await query.addColumn(table, 'isPublic', template);
15 | }
16 | };
17 |
18 | const down = async (query) => {
19 | for await (let table of tables) {
20 | await query.removeColumn(table, 'isPublic');
21 | }
22 | };
23 |
24 | module.exports = {
25 | up,
26 | down,
27 | };
28 |
--------------------------------------------------------------------------------
/db/migrations/03_weather.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { INTEGER, FLOAT } = DataTypes;
3 |
4 | const up = async (query) => {
5 | await query.addColumn('weather', 'humidity', {
6 | type: INTEGER,
7 | });
8 |
9 | await query.addColumn('weather', 'windK', {
10 | type: FLOAT,
11 | });
12 |
13 | await query.addColumn('weather', 'windM', {
14 | type: FLOAT,
15 | });
16 | };
17 |
18 | const down = async (query) => {
19 | await query.removeColumn('weather', 'humidity');
20 | await query.removeColumn('weather', 'windK');
21 | await query.removeColumn('weather', 'windM');
22 | };
23 |
24 | module.exports = {
25 | up,
26 | down,
27 | };
28 |
--------------------------------------------------------------------------------
/db/migrations/04_bookmarks-order.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { INTEGER } = DataTypes;
3 |
4 | const up = async (query) => {
5 | await query.addColumn('bookmarks', 'orderId', {
6 | type: INTEGER,
7 | allowNull: true,
8 | defaultValue: null,
9 | });
10 | };
11 |
12 | const down = async (query) => {
13 | await query.removeColumn('bookmarks', 'orderId');
14 | };
15 |
16 | module.exports = {
17 | up,
18 | down,
19 | };
20 |
--------------------------------------------------------------------------------
/db/migrations/05_app-description.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { STRING } = DataTypes;
3 |
4 | const up = async (query) => {
5 | await query.addColumn('apps', 'description', {
6 | type: STRING,
7 | allowNull: false,
8 | defaultValue: '',
9 | });
10 | };
11 |
12 | const down = async (query) => {
13 | await query.removeColumn('apps', 'description');
14 | };
15 |
16 | module.exports = {
17 | up,
18 | down,
19 | };
20 |
--------------------------------------------------------------------------------
/db/utils/backupDb.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const { slugify } = require('./slugify');
3 |
4 | const backupDB = () => {
5 | if (!fs.existsSync('data/db_backups')) {
6 | fs.mkdirSync('data/db_backups');
7 | }
8 |
9 | const slug = slugify();
10 |
11 | const srcPath = 'data/db.sqlite';
12 | const destPath = `data/db_backups/${slug}`;
13 |
14 | if (fs.existsSync(srcPath)) {
15 | if (!fs.existsSync(destPath)) {
16 | fs.copyFileSync(srcPath, destPath);
17 | }
18 | }
19 | };
20 |
21 | module.exports = backupDB;
22 |
--------------------------------------------------------------------------------
/db/utils/slugify.js:
--------------------------------------------------------------------------------
1 | const slugify = () => {
2 | const version = process.env.VERSION;
3 | const slug = `db-${version.replace(/\./g, '')}-backup.sqlite`;
4 | return slug;
5 | };
6 |
7 | const parseSlug = (slug) => {
8 | const parts = slug.split('-');
9 | const version = {
10 | raw: parts[1],
11 | parsed: parts[1].split('').join('.'),
12 | };
13 | return version;
14 | };
15 |
16 | module.exports = {
17 | slugify,
18 | parseSlug,
19 | };
20 |
--------------------------------------------------------------------------------
/k8s/base/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: Deployment
3 | apiVersion: apps/v1
4 | metadata:
5 | name: flame
6 | spec:
7 | selector:
8 | matchLabels:
9 | app: flame
10 | template:
11 | metadata:
12 | labels:
13 | app: flame
14 | spec:
15 | serviceAccountName: flame
16 | securityContext:
17 | fsGroup: 1000
18 | containers:
19 | - name: flame
20 | image: shokohsc/flame
21 | ports:
22 | - name: http
23 | containerPort: 5005
24 | protocol: TCP
25 | readinessProbe:
26 | httpGet:
27 | path: /
28 | port: http
29 |
--------------------------------------------------------------------------------
/k8s/base/ingress.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | name: flame
6 | spec:
7 | rules:
8 | - host: flame.cluster.local
9 | http:
10 | paths:
11 | - path: /
12 | pathType: Prefix
13 | backend:
14 | service:
15 | name: flame
16 | port:
17 | number: 80
18 |
--------------------------------------------------------------------------------
/k8s/base/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 | namespace: flame
4 | resources:
5 | - namespace.yaml
6 | - deployment.yaml
7 | - service.yaml
8 | - ingress.yaml
9 | - rbac.yaml
10 |
--------------------------------------------------------------------------------
/k8s/base/namespace.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Namespace
4 | metadata:
5 | name: flame
6 | labels:
7 | namespace: flame
8 | goldilocks.fairwinds.com/enabled: "true"
9 |
--------------------------------------------------------------------------------
/k8s/base/rbac.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: flame
6 | ---
7 | kind: ClusterRole
8 | apiVersion: rbac.authorization.k8s.io/v1
9 | metadata:
10 | name: flame
11 | rules:
12 | - apiGroups: ["networking.k8s.io"]
13 | resources: ["ingresses"]
14 | verbs: ["get", "list", "watch"]
15 | ---
16 | apiVersion: rbac.authorization.k8s.io/v1
17 | kind: ClusterRoleBinding
18 | metadata:
19 | name: flame
20 | subjects:
21 | - kind: ServiceAccount
22 | name: flame
23 | roleRef:
24 | apiGroup: rbac.authorization.k8s.io
25 | kind: ClusterRole
26 | name: flame
27 |
--------------------------------------------------------------------------------
/k8s/base/service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: flame
6 | labels:
7 | app: flame
8 | spec:
9 | type: ClusterIP
10 | ports:
11 | - port: 80
12 | targetPort: http
13 | protocol: TCP
14 | name: http
15 | selector:
16 | app: flame
17 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | kind: Deployment
3 | apiVersion: apps/v1
4 | metadata:
5 | name: flame
6 | spec:
7 | selector:
8 | matchLabels:
9 | app: flame
10 | template:
11 | metadata:
12 | labels:
13 | app: flame
14 | spec:
15 | serviceAccountName: flame-dev
16 | securityContext:
17 | fsGroup: 1000
18 | containers:
19 | - name: flame
20 | image: shokohsc/flame
21 | command:
22 | - npm
23 | args:
24 | - run
25 | - skaffold
26 | env:
27 | - name: NODE_ENV
28 | value: development
29 | ports:
30 | - name: http
31 | containerPort: 5005
32 | protocol: TCP
33 | readinessProbe:
34 | httpGet:
35 | path: /
36 | port: http
37 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/ingress.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: networking.k8s.io/v1
3 | kind: Ingress
4 | metadata:
5 | name: flame
6 | annotations:
7 | kubernetes.io/ingress.class: nginx
8 | cert-manager.io/cluster-issuer: ca-cluster-issuer
9 | flame.pawelmalak/name: flame
10 | flame.pawelmalak/url: dev.flame.shokohsc.home
11 | flame.pawelmalak/type: app
12 | flame.pawelmalak/icon: fire
13 | spec:
14 | rules:
15 | - host: dev.flame.shokohsc.home
16 | http:
17 | paths:
18 | - path: /
19 | pathType: Prefix
20 | backend:
21 | service:
22 | name: flame
23 | port:
24 | number: 80
25 | tls:
26 | - hosts:
27 | - dev.flame.shokohsc.home
28 | secretName: flame-cert
29 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/kustomization.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kustomize.config.k8s.io/v1beta1
2 | kind: Kustomization
3 | namespace: flame-dev
4 | resources:
5 | - namespace.yaml
6 | - deployment.yaml
7 | - service.yaml
8 | - ingress.yaml
9 | - rbac.yaml
10 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/namespace.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Namespace
4 | metadata:
5 | name: flame-dev
6 | labels:
7 | namespace: flame-dev
8 | goldilocks.fairwinds.com/enabled: "true"
9 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/rbac.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: flame-dev
6 | ---
7 | kind: ClusterRole
8 | apiVersion: rbac.authorization.k8s.io/v1
9 | metadata:
10 | name: flame-dev
11 | rules:
12 | - apiGroups: ["networking.k8s.io"]
13 | resources: ["ingresses"]
14 | verbs: ["get", "list", "watch"]
15 | ---
16 | apiVersion: rbac.authorization.k8s.io/v1
17 | kind: ClusterRoleBinding
18 | metadata:
19 | name: flame-dev
20 | subjects:
21 | - kind: ServiceAccount
22 | name: flame-dev
23 | roleRef:
24 | apiGroup: rbac.authorization.k8s.io
25 | kind: ClusterRole
26 | name: flame-dev
27 |
--------------------------------------------------------------------------------
/k8s/overlays/shokohsc/service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: flame
6 | labels:
7 | app: flame
8 | spec:
9 | type: ClusterIP
10 | ports:
11 | - port: 80
12 | targetPort: http
13 | protocol: TCP
14 | name: http
15 | selector:
16 | app: flame
17 |
--------------------------------------------------------------------------------
/middleware/asyncWrapper.js:
--------------------------------------------------------------------------------
1 | function asyncWrapper(foo) {
2 | return function (req, res, next) {
3 | return Promise.resolve(foo(req, res, next)).catch(next);
4 | };
5 | }
6 |
7 | module.exports = asyncWrapper;
8 |
--------------------------------------------------------------------------------
/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const auth = (req, res, next) => {
4 | const authHeader = req.header('Authorization-Flame');
5 | let token;
6 | let tokenIsValid = false;
7 |
8 | if (authHeader && authHeader.startsWith('Bearer ')) {
9 | token = authHeader.split(' ')[1];
10 | }
11 |
12 | if (token) {
13 | try {
14 | jwt.verify(token, process.env.SECRET);
15 | } finally {
16 | tokenIsValid = true;
17 | }
18 | }
19 |
20 | req.isAuthenticated = tokenIsValid;
21 |
22 | next();
23 | };
24 |
25 | module.exports = auth;
26 |
--------------------------------------------------------------------------------
/middleware/errorHandler.js:
--------------------------------------------------------------------------------
1 | const ErrorResponse = require('../utils/ErrorResponse');
2 | const Logger = require('../utils/Logger');
3 | const logger = new Logger();
4 |
5 | const errorHandler = (err, req, res, next) => {
6 | let error = { ...err };
7 | error.message = err.message;
8 |
9 | // if (error.errors[0].type === 'unique violation') {
10 | // const msg = error.errors[0].message;
11 | // error = new ErrorResponse(`Field ${msg}`, 400);
12 | // }
13 |
14 | logger.log(error.message.split(',')[0], 'ERROR');
15 |
16 | if (process.env.NODE_ENV == 'development') {
17 | console.log(err);
18 | }
19 |
20 | res.status(err.statusCode || 500).json({
21 | success: false,
22 | error: error.message || 'Server Error',
23 | });
24 | };
25 |
26 | module.exports = errorHandler;
27 |
--------------------------------------------------------------------------------
/middleware/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | asyncWrapper: require('./asyncWrapper'),
3 | auth: require('./auth'),
4 | errorHandler: require('./errorHandler'),
5 | upload: require('./multer'),
6 | requireAuth: require('./requireAuth'),
7 | requireBody: require('./requireBody'),
8 | };
9 |
--------------------------------------------------------------------------------
/middleware/multer.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const multer = require('multer');
3 |
4 | if (!fs.existsSync('data/uploads')) {
5 | fs.mkdirSync('data/uploads', { recursive: true });
6 | }
7 |
8 | const storage = multer.diskStorage({
9 | destination: (req, file, cb) => {
10 | cb(null, './data/uploads');
11 | },
12 | filename: (req, file, cb) => {
13 | cb(null, Date.now() + '--' + file.originalname);
14 | },
15 | });
16 |
17 | const supportedTypes = ['jpg', 'jpeg', 'png', 'svg', 'svg+xml', 'x-icon'];
18 |
19 | const fileFilter = (req, file, cb) => {
20 | if (supportedTypes.includes(file.mimetype.split('/')[1])) {
21 | cb(null, true);
22 | } else {
23 | cb(null, false);
24 | }
25 | };
26 |
27 | const upload = multer({ storage, fileFilter });
28 |
29 | module.exports = upload.single('icon');
30 |
--------------------------------------------------------------------------------
/middleware/requireAuth.js:
--------------------------------------------------------------------------------
1 | const ErrorResponse = require('../utils/ErrorResponse');
2 |
3 | const requireAuth = (req, res, next) => {
4 | if (!req.isAuthenticated) {
5 | return next(new ErrorResponse('Unauthorized', 401));
6 | }
7 |
8 | next();
9 | };
10 |
11 | module.exports = requireAuth;
12 |
--------------------------------------------------------------------------------
/middleware/requireBody.js:
--------------------------------------------------------------------------------
1 | const ErrorResponse = require('../utils/ErrorResponse');
2 |
3 | const requireBody = (keys) => (req, res, next) => {
4 | const missing = keys.filter((key) => !Object.keys(req.body).includes(key));
5 |
6 | if (missing.length) {
7 | return next(new ErrorResponse(`'${missing[0]}' is required`, 400));
8 | }
9 |
10 | next();
11 | };
12 |
13 | module.exports = requireBody;
14 |
--------------------------------------------------------------------------------
/models/App.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../db');
3 |
4 | const App = sequelize.define(
5 | 'App',
6 | {
7 | name: {
8 | type: DataTypes.STRING,
9 | allowNull: false,
10 | },
11 | url: {
12 | type: DataTypes.STRING,
13 | allowNull: false,
14 | },
15 | icon: {
16 | type: DataTypes.STRING,
17 | allowNull: false,
18 | defaultValue: 'cancel',
19 | },
20 | isPinned: {
21 | type: DataTypes.BOOLEAN,
22 | defaultValue: false,
23 | },
24 | orderId: {
25 | type: DataTypes.INTEGER,
26 | allowNull: true,
27 | defaultValue: null,
28 | },
29 | isPublic: {
30 | type: DataTypes.INTEGER,
31 | allowNull: true,
32 | defaultValue: 1,
33 | },
34 | description: {
35 | type: DataTypes.STRING,
36 | allowNull: false,
37 | defaultValue: '',
38 | },
39 | },
40 | {
41 | tableName: 'apps',
42 | }
43 | );
44 |
45 | module.exports = App;
46 |
--------------------------------------------------------------------------------
/models/Bookmark.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../db');
3 |
4 | const Bookmark = sequelize.define(
5 | 'Bookmark',
6 | {
7 | name: {
8 | type: DataTypes.STRING,
9 | allowNull: false,
10 | },
11 | url: {
12 | type: DataTypes.STRING,
13 | allowNull: false,
14 | },
15 | categoryId: {
16 | type: DataTypes.INTEGER,
17 | allowNull: false,
18 | },
19 | icon: {
20 | type: DataTypes.STRING,
21 | defaultValue: '',
22 | },
23 | isPublic: {
24 | type: DataTypes.INTEGER,
25 | allowNull: true,
26 | defaultValue: 1,
27 | },
28 | orderId: {
29 | type: DataTypes.INTEGER,
30 | allowNull: true,
31 | defaultValue: null,
32 | },
33 | },
34 | {
35 | tableName: 'bookmarks',
36 | }
37 | );
38 |
39 | module.exports = Bookmark;
40 |
--------------------------------------------------------------------------------
/models/Category.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../db');
3 |
4 | const Category = sequelize.define(
5 | 'Category',
6 | {
7 | name: {
8 | type: DataTypes.STRING,
9 | allowNull: false,
10 | },
11 | isPinned: {
12 | type: DataTypes.BOOLEAN,
13 | defaultValue: false,
14 | },
15 | orderId: {
16 | type: DataTypes.INTEGER,
17 | allowNull: true,
18 | defaultValue: null,
19 | },
20 | isPublic: {
21 | type: DataTypes.INTEGER,
22 | allowNull: true,
23 | defaultValue: 1,
24 | },
25 | },
26 | {
27 | tableName: 'categories',
28 | }
29 | );
30 |
31 | module.exports = Category;
32 |
--------------------------------------------------------------------------------
/models/Config.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../db');
3 |
4 | const Config = sequelize.define(
5 | 'Config',
6 | {
7 | key: {
8 | type: DataTypes.STRING,
9 | allowNull: false,
10 | unique: true,
11 | },
12 | value: {
13 | type: DataTypes.STRING,
14 | allowNull: false,
15 | },
16 | valueType: {
17 | type: DataTypes.STRING,
18 | allowNull: false,
19 | },
20 | isLocked: {
21 | type: DataTypes.TINYINT,
22 | defaultValue: 0,
23 | },
24 | },
25 | {
26 | tableName: 'config',
27 | }
28 | );
29 |
30 | module.exports = Config;
31 |
--------------------------------------------------------------------------------
/models/Weather.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../db');
3 |
4 | const Weather = sequelize.define(
5 | 'Weather',
6 | {
7 | externalLastUpdate: DataTypes.STRING,
8 | tempC: DataTypes.FLOAT,
9 | tempF: DataTypes.FLOAT,
10 | isDay: DataTypes.INTEGER,
11 | cloud: DataTypes.INTEGER,
12 | conditionText: DataTypes.TEXT,
13 | conditionCode: DataTypes.INTEGER,
14 | humidity: DataTypes.INTEGER,
15 | windK: DataTypes.FLOAT,
16 | windM: DataTypes.FLOAT,
17 | },
18 | {
19 | tableName: 'weather',
20 | }
21 | );
22 |
23 | module.exports = Weather;
24 |
--------------------------------------------------------------------------------
/models/associateModels.js:
--------------------------------------------------------------------------------
1 | const Category = require('./Category');
2 | const Bookmark = require('./Bookmark');
3 |
4 | const associateModels = () => {
5 | Category.hasMany(Bookmark, {
6 | foreignKey: 'categoryId',
7 | as: 'bookmarks'
8 | });
9 |
10 | Bookmark.belongsTo(Category, {
11 | foreignKey: 'categoryId'
12 | });
13 | }
14 |
15 | module.exports = associateModels;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flame",
3 | "version": "0.1.0",
4 | "description": "Self-hosted start page",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "init-server": "echo Instaling server dependencies && npm install",
9 | "init-client": "cd client && echo Instaling client dependencies && npm install",
10 | "dir-init": "npx mkdirp data public && touch public/flame.css public/customQueries.json",
11 | "dev-init": "npm run dir-init && npm run init-server && npm run init-client",
12 | "dev-server": "nodemon server.js -e js",
13 | "dev-client": "npm start --prefix client",
14 | "dev": "concurrently \"npm run dev-server\" \"npm run dev-client\"",
15 | "skaffold": "concurrently \"npm run init-client\" \"npm run dev-server\""
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "dependencies": {
20 | "@kubernetes/client-node": "^0.15.1",
21 | "@types/express": "^4.17.13",
22 | "axios": "^0.24.0",
23 | "concurrently": "^6.3.0",
24 | "docker-secret": "^1.2.4",
25 | "dotenv": "^10.0.0",
26 | "express": "^4.17.1",
27 | "jsonwebtoken": "^8.5.1",
28 | "multer": "^1.4.3",
29 | "node-schedule": "^2.0.0",
30 | "sequelize": "^6.9.0",
31 | "sqlite3": "^5.0.2",
32 | "umzug": "^2.3.0",
33 | "ws": "^8.2.3"
34 | },
35 | "devDependencies": {
36 | "nodemon": "^2.0.14"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/routes/apps.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { auth, requireAuth, upload } = require('../middleware');
6 |
7 | const {
8 | createApp,
9 | getAllApps,
10 | getSingleApp,
11 | updateApp,
12 | deleteApp,
13 | reorderApps,
14 | } = require('../controllers/apps');
15 |
16 | router
17 | .route('/')
18 | .post(auth, requireAuth, upload, createApp)
19 | .get(auth, getAllApps);
20 |
21 | router
22 | .route('/:id')
23 | .get(auth, getSingleApp)
24 | .put(auth, requireAuth, upload, updateApp)
25 | .delete(auth, requireAuth, deleteApp);
26 |
27 | router.route('/0/reorder').put(auth, requireAuth, reorderApps);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const { login, validate } = require('../controllers/auth');
5 | const requireBody = require('../middleware/requireBody');
6 |
7 | router.route('/').post(requireBody(['password', 'duration']), login);
8 |
9 | router.route('/validate').post(requireBody(['token']), validate);
10 |
11 | module.exports = router;
12 |
--------------------------------------------------------------------------------
/routes/bookmark.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { upload, auth, requireAuth } = require('../middleware');
6 |
7 | const {
8 | createBookmark,
9 | getAllBookmarks,
10 | getSingleBookmark,
11 | updateBookmark,
12 | deleteBookmark,
13 | reorderBookmarks,
14 | } = require('../controllers/bookmarks');
15 |
16 | router
17 | .route('/')
18 | .post(auth, requireAuth, upload, createBookmark)
19 | .get(auth, getAllBookmarks);
20 |
21 | router
22 | .route('/:id')
23 | .get(auth, getSingleBookmark)
24 | .put(auth, requireAuth, upload, updateBookmark)
25 | .delete(auth, requireAuth, deleteBookmark);
26 |
27 | router.route('/0/reorder').put(auth, requireAuth, reorderBookmarks);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/routes/category.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { auth, requireAuth } = require('../middleware');
6 |
7 | const {
8 | createCategory,
9 | getAllCategories,
10 | getSingleCategory,
11 | updateCategory,
12 | deleteCategory,
13 | reorderCategories,
14 | } = require('../controllers/categories');
15 |
16 | router
17 | .route('/')
18 | .post(auth, requireAuth, createCategory)
19 | .get(auth, getAllCategories);
20 |
21 | router
22 | .route('/:id')
23 | .get(auth, getSingleCategory)
24 | .put(auth, requireAuth, updateCategory)
25 | .delete(auth, requireAuth, deleteCategory);
26 |
27 | router.route('/0/reorder').put(auth, requireAuth, reorderCategories);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/routes/config.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { auth, requireAuth } = require('../middleware');
6 |
7 | const {
8 | getCSS,
9 | updateCSS,
10 | getConfig,
11 | updateConfig,
12 | } = require('../controllers/config');
13 |
14 | router.route('/').get(getConfig).put(auth, requireAuth, updateConfig);
15 |
16 | router.route('/0/css').get(getCSS).put(auth, requireAuth, updateCSS);
17 |
18 | module.exports = router;
19 |
--------------------------------------------------------------------------------
/routes/queries.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { auth, requireAuth, requireBody } = require('../middleware');
6 |
7 | const {
8 | getQueries,
9 | addQuery,
10 | deleteQuery,
11 | updateQuery,
12 | } = require('../controllers/queries/');
13 |
14 | router
15 | .route('/')
16 | .post(
17 | auth,
18 | requireAuth,
19 | requireBody(['name', 'prefix', 'template']),
20 | addQuery
21 | )
22 | .get(getQueries);
23 |
24 | router
25 | .route('/:prefix')
26 | .delete(auth, requireAuth, deleteQuery)
27 | .put(auth, requireAuth, updateQuery);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/routes/themes.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | // middleware
5 | const { auth, requireAuth, requireBody } = require('../middleware');
6 |
7 | const {
8 | getThemes,
9 | addTheme,
10 | deleteTheme,
11 | updateTheme,
12 | } = require('../controllers/themes/');
13 |
14 | router
15 | .route('/')
16 | .get(getThemes)
17 | .post(
18 | auth,
19 | requireAuth,
20 | requireBody(['name', 'colors', 'isCustom']),
21 | addTheme
22 | );
23 |
24 | router
25 | .route('/:name')
26 | .delete(auth, requireAuth, deleteTheme)
27 | .put(auth, requireAuth, updateTheme);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/routes/weather.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 |
4 | const {
5 | getWeather, updateWeather
6 | } = require('../controllers/weather');
7 |
8 | router
9 | .route('/')
10 | .get(getWeather);
11 |
12 | router
13 | .route('/update')
14 | .get(updateWeather);
15 |
16 |
17 | module.exports = router;
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const http = require('http');
3 |
4 | // Database
5 | const { connectDB } = require('./db');
6 | const associateModels = require('./models/associateModels');
7 |
8 | // Server
9 | const api = require('./api');
10 | const jobs = require('./utils/jobs');
11 | const Socket = require('./Socket');
12 | const Sockets = require('./Sockets');
13 |
14 | // Utils
15 | const initApp = require('./utils/init');
16 | const Logger = require('./utils/Logger');
17 | const logger = new Logger();
18 |
19 | (async () => {
20 | const PORT = process.env.PORT || 5005;
21 |
22 | // Init app
23 | await initApp();
24 | await connectDB();
25 | await associateModels();
26 | await jobs();
27 |
28 | // Create server for Express API and WebSockets
29 | const server = http.createServer();
30 | server.on('request', api);
31 |
32 | // Register weatherSocket
33 | const weatherSocket = new Socket(server);
34 | Sockets.registerSocket('weather', weatherSocket);
35 |
36 | server.listen(PORT, () => {
37 | logger.log(
38 | `Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`
39 | );
40 | });
41 | })();
42 |
--------------------------------------------------------------------------------
/skaffold.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: skaffold/v2beta20
2 | kind: Config
3 | metadata:
4 | name: flame
5 | build:
6 | artifacts:
7 | - image: shokohsc/flame
8 | context: .
9 | sync:
10 | manual:
11 | - src: controllers/*.js
12 | dest: .
13 | docker:
14 | dockerfile: Dockerfile.dev
15 | deploy:
16 | kustomize:
17 | paths:
18 | - k8s/base
19 | profiles:
20 | - name: dev
21 | activation:
22 | - command: dev
23 | build:
24 | artifacts:
25 | - image: shokohsc/flame
26 | sync:
27 | manual:
28 | - src: controllers/*.js
29 | dest: .
30 | docker:
31 | dockerfile: Dockerfile.dev
32 | - name: shokohsc
33 | build:
34 | artifacts:
35 | - image: shokohsc/flame
36 | sync:
37 | manual:
38 | - src: controllers/*.js
39 | dest: .
40 | kaniko:
41 | dockerfile: Dockerfile.dev
42 | cache:
43 | repo: shokohsc/flame
44 | cluster:
45 | dockerConfig:
46 | secretName: kaniko-secret
47 | namespace: kaniko
48 | pullSecretName: kaniko-secret
49 | deploy:
50 | kustomize:
51 | paths:
52 | - k8s/overlays/shokohsc
53 | - name: prod
54 | build:
55 | artifacts:
56 | - image: shokohsc/flame
57 | kaniko:
58 | dockerfile: Dockerfile
59 | cache:
60 | repo: shokohsc/flame
61 | cluster:
62 | dockerConfig:
63 | secretName: kaniko-secret
64 | namespace: kaniko
65 | pullSecretName: kaniko-secret
66 |
--------------------------------------------------------------------------------
/utils/ErrorResponse.js:
--------------------------------------------------------------------------------
1 | class ErrorResponse extends Error {
2 | constructor(message, statusCode) {
3 | super(message);
4 | this.statusCode = statusCode;
5 | }
6 | }
7 |
8 | module.exports = ErrorResponse;
9 |
--------------------------------------------------------------------------------
/utils/File.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | class File {
4 | constructor(path) {
5 | this.path = path;
6 | this.content = null;
7 | }
8 |
9 | read() {
10 | try {
11 | const content = fs.readFileSync(this.path, { encoding: 'utf-8' });
12 | this.content = content;
13 | return this.content;
14 | } catch (err) {
15 | return err.message;
16 | }
17 | }
18 |
19 | write(data, isJSON) {
20 | this.content = data;
21 | fs.writeFileSync(
22 | this.path,
23 | isJSON ? JSON.stringify(this.content) : this.content
24 | );
25 | }
26 | }
27 |
28 | module.exports = File;
29 |
--------------------------------------------------------------------------------
/utils/Logger.js:
--------------------------------------------------------------------------------
1 | class Logger {
2 | log(message, level = 'INFO') {
3 | console.log(`[${this.generateTimestamp()}] [${level}] ${message}`);
4 | }
5 |
6 | generateTimestamp() {
7 | const d = new Date();
8 |
9 | // Date
10 | const year = d.getFullYear();
11 | const month = this.parseDate(d.getMonth() + 1);
12 | const day = this.parseDate(d.getDate());
13 |
14 | // Time
15 | const hour = this.parseDate(d.getHours());
16 | const minutes = this.parseDate(d.getMinutes());
17 | const seconds = this.parseDate(d.getSeconds());
18 | const miliseconds = this.parseDate(d.getMilliseconds(), true);
19 |
20 | // Timezone
21 | const tz = -d.getTimezoneOffset() / 60;
22 |
23 | return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${
24 | tz >= 0 ? '+' + tz : tz
25 | }`;
26 | }
27 |
28 | parseDate(date, ms = false) {
29 | if (ms) {
30 | if (date >= 10 && date < 100) {
31 | return `0${date}`;
32 | } else if (date < 10) {
33 | return `00${date}`;
34 | }
35 | }
36 |
37 | return date < 10 ? `0${date}` : date.toString();
38 | }
39 | }
40 |
41 | module.exports = Logger;
42 |
--------------------------------------------------------------------------------
/utils/checkFileExists.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const checkFileExists = (path) => {
4 | return fs.promises
5 | .access(path, fs.constants.F_OK)
6 | .then(() => true)
7 | .catch(() => false);
8 | };
9 |
10 | module.exports = checkFileExists;
11 |
--------------------------------------------------------------------------------
/utils/clearWeatherData.js:
--------------------------------------------------------------------------------
1 | const { Op } = require('sequelize');
2 | const Weather = require('../models/Weather');
3 |
4 | const clearWeatherData = async () => {
5 | const weather = await Weather.findOne({
6 | order: [['createdAt', 'DESC']],
7 | });
8 |
9 | if (weather) {
10 | await Weather.destroy({
11 | where: {
12 | id: {
13 | [Op.lt]: weather.id,
14 | },
15 | },
16 | });
17 | }
18 | };
19 |
20 | module.exports = clearWeatherData;
21 |
--------------------------------------------------------------------------------
/utils/getExternalWeather.js:
--------------------------------------------------------------------------------
1 | const Weather = require('../models/Weather');
2 | const axios = require('axios');
3 | const loadConfig = require('./loadConfig');
4 |
5 | const getExternalWeather = async () => {
6 | const { WEATHER_API_KEY: secret, lat, long } = await loadConfig();
7 |
8 | // Fetch data from external API
9 | try {
10 | const res = await axios.get(
11 | `http://api.weatherapi.com/v1/current.json?key=${secret}&q=${lat},${long}`
12 | );
13 |
14 | // Save weather data
15 | const cursor = res.data.current;
16 | const weatherData = await Weather.create({
17 | externalLastUpdate: cursor.last_updated,
18 | tempC: cursor.temp_c,
19 | tempF: cursor.temp_f,
20 | isDay: cursor.is_day,
21 | cloud: cursor.cloud,
22 | conditionText: cursor.condition.text,
23 | conditionCode: cursor.condition.code,
24 | humidity: cursor.humidity,
25 | windK: cursor.wind_kph,
26 | windM: cursor.wind_mph,
27 | });
28 | return weatherData;
29 | } catch (err) {
30 | throw new Error('External API request failed');
31 | }
32 | };
33 |
34 | module.exports = getExternalWeather;
35 |
--------------------------------------------------------------------------------
/utils/init/createFile.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const { join } = require('path');
3 |
4 | const Logger = require('../Logger');
5 | const logger = new Logger();
6 |
7 | const createFile = async (file) => {
8 | const { name, msg, template, isJSON, paths } = file;
9 |
10 | const srcPath = join(__dirname, paths.src, name);
11 | const destPath = join(__dirname, paths.dest, name);
12 |
13 | // Check if file exists
14 | if (fs.existsSync(srcPath)) {
15 | fs.copyFileSync(srcPath, destPath);
16 |
17 | if (process.env.NODE_ENV == 'development') {
18 | logger.log(msg.found);
19 | }
20 |
21 | return;
22 | }
23 |
24 | // Create file if not
25 | fs.writeFileSync(destPath, isJSON ? JSON.stringify(template) : template);
26 |
27 | if (process.env.NODE_ENV == 'development') {
28 | logger.log(msg.created);
29 | }
30 | };
31 |
32 | module.exports = createFile;
33 |
--------------------------------------------------------------------------------
/utils/init/index.js:
--------------------------------------------------------------------------------
1 | const initConfig = require('./initConfig');
2 | const initFiles = require('./initFiles');
3 | const initDockerSecrets = require('./initDockerSecrets');
4 | const normalizeTheme = require('./normalizeTheme');
5 |
6 | const initApp = async () => {
7 | initDockerSecrets();
8 | await initFiles();
9 | await initConfig();
10 | await normalizeTheme();
11 | };
12 |
13 | module.exports = initApp;
14 |
--------------------------------------------------------------------------------
/utils/init/initConfig.js:
--------------------------------------------------------------------------------
1 | const { copyFile, readFile, writeFile } = require('fs/promises');
2 | const checkFileExists = require('../checkFileExists');
3 | const initialConfig = require('./initialConfig.json');
4 |
5 | const initConfig = async () => {
6 | const configExists = await checkFileExists('data/config.json');
7 |
8 | if (!configExists) {
9 | await copyFile('utils/init/initialConfig.json', 'data/config.json');
10 | }
11 |
12 | const existingConfig = await readFile('data/config.json', 'utf-8');
13 | const parsedConfig = JSON.parse(existingConfig);
14 |
15 | // Add new config pairs if necessary
16 | for (let key in initialConfig) {
17 | if (!Object.keys(parsedConfig).includes(key)) {
18 | parsedConfig[key] = initialConfig[key];
19 | }
20 | }
21 |
22 | await writeFile('data/config.json', JSON.stringify(parsedConfig));
23 | };
24 |
25 | module.exports = initConfig;
26 |
--------------------------------------------------------------------------------
/utils/init/initDockerSecrets.js:
--------------------------------------------------------------------------------
1 | const { getSecrets } = require('docker-secret');
2 | const Logger = require('../Logger');
3 | const logger = new Logger();
4 |
5 | const initDockerSecrets = () => {
6 | try {
7 | const secrets = getSecrets();
8 |
9 | for (const property in secrets) {
10 | const upperProperty = property.toUpperCase();
11 |
12 | process.env[upperProperty] = secrets[property];
13 |
14 | logger.log(`${upperProperty} was overwritten with docker secret value`);
15 | }
16 | } catch (e) {
17 | logger.log(`Failed to initialize docker secrets. Error: ${e}`, 'ERROR');
18 | }
19 | };
20 |
21 | module.exports = initDockerSecrets;
22 |
--------------------------------------------------------------------------------
/utils/init/initFiles.js:
--------------------------------------------------------------------------------
1 | const createFile = require('./createFile');
2 | const { files } = require('./initialFiles.json');
3 |
4 | const initFiles = async () => {
5 | files.forEach(async (file) => await createFile(file));
6 | };
7 |
8 | module.exports = initFiles;
9 |
--------------------------------------------------------------------------------
/utils/init/initialConfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "WEATHER_API_KEY": "",
3 | "lat": 0,
4 | "long": 0,
5 | "isCelsius": true,
6 | "customTitle": "Flame",
7 | "pinAppsByDefault": true,
8 | "pinCategoriesByDefault": true,
9 | "hideHeader": false,
10 | "useOrdering": "createdAt",
11 | "appsSameTab": false,
12 | "bookmarksSameTab": false,
13 | "searchSameTab": false,
14 | "hideApps": false,
15 | "hideCategories": false,
16 | "hideSearch": false,
17 | "defaultSearchProvider": "l",
18 | "secondarySearchProvider": "d",
19 | "dockerApps": false,
20 | "dockerHost": "localhost",
21 | "kubernetesApps": false,
22 | "unpinStoppedApps": false,
23 | "useAmericanDate": false,
24 | "disableAutofocus": false,
25 | "greetingsSchema": "Good evening!;Good afternoon!;Good morning!;Good night!",
26 | "daySchema": "Sunday;Monday;Tuesday;Wednesday;Thursday;Friday;Saturday",
27 | "monthSchema": "January;February;March;April;May;June;July;August;September;October;November;December",
28 | "showTime": false,
29 | "defaultTheme": "tron",
30 | "isKilometer": true,
31 | "weatherData": "cloud",
32 | "hideDate": false
33 | }
34 |
--------------------------------------------------------------------------------
/utils/init/normalizeTheme.js:
--------------------------------------------------------------------------------
1 | const { readFile, writeFile } = require('fs/promises');
2 |
3 | const normalizeTheme = async () => {
4 | // open main config file
5 | const configFile = await readFile('data/config.json', 'utf8');
6 | const config = JSON.parse(configFile);
7 |
8 | // open default themes file
9 | const themesFile = await readFile('utils/init/themes.json', 'utf8');
10 | const { themes } = JSON.parse(themesFile);
11 |
12 | // find theme
13 | const theme = themes.find((t) => t.name === config.defaultTheme);
14 |
15 | if (theme) {
16 | // save theme in new format
17 | // PAB - primary;accent;background
18 | const { primary: p, accent: a, background: b } = theme.colors;
19 | const normalizedTheme = `${p};${a};${b}`;
20 |
21 | await writeFile(
22 | 'data/config.json',
23 | JSON.stringify({ ...config, defaultTheme: normalizedTheme })
24 | );
25 | }
26 | };
27 |
28 | module.exports = normalizeTheme;
29 |
--------------------------------------------------------------------------------
/utils/jobs.js:
--------------------------------------------------------------------------------
1 | const schedule = require('node-schedule');
2 | const getExternalWeather = require('./getExternalWeather');
3 | const clearWeatherData = require('./clearWeatherData');
4 | const Sockets = require('../Sockets');
5 | const Logger = require('./Logger');
6 | const loadConfig = require('./loadConfig');
7 | const logger = new Logger();
8 |
9 | module.exports = async function () {
10 | const { WEATHER_API_KEY } = await loadConfig();
11 |
12 | if (WEATHER_API_KEY != '') {
13 | // Update weather data every 15 minutes
14 | const weatherJob = schedule.scheduleJob(
15 | 'updateWeather',
16 | '0 */15 * * * *',
17 | async () => {
18 | try {
19 | const weatherData = await getExternalWeather();
20 |
21 | Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData));
22 | } catch (err) {
23 | if (WEATHER_API_KEY) {
24 | logger.log(err.message, 'ERROR');
25 | }
26 | }
27 | }
28 | );
29 |
30 | // Clear old weather data every 4 hours
31 | const weatherCleanerJob = schedule.scheduleJob(
32 | 'clearWeather',
33 | '0 5 */4 * * *',
34 | async () => {
35 | clearWeatherData();
36 | }
37 | );
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/utils/loadConfig.js:
--------------------------------------------------------------------------------
1 | const { readFile } = require('fs/promises');
2 | const checkFileExists = require('../utils/checkFileExists');
3 | const initConfig = require('../utils/init/initConfig');
4 |
5 | const loadConfig = async () => {
6 | const configExists = await checkFileExists('data/config.json');
7 |
8 | if (!configExists) {
9 | await initConfig();
10 | }
11 |
12 | const config = await readFile('data/config.json', 'utf-8');
13 | const parsedConfig = JSON.parse(config);
14 |
15 | return parsedConfig;
16 | };
17 |
18 | module.exports = loadConfig;
19 |
--------------------------------------------------------------------------------
/utils/signToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | const signToken = (expiresIn) => {
4 | const token = jwt.sign({ app: 'flame' }, process.env.SECRET, { expiresIn });
5 | return token;
6 | };
7 |
8 | module.exports = signToken;
9 |
--------------------------------------------------------------------------------