From 5ae4d6e7c4260aea0bd75adb39f1a9c79ccbacbc Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 22 Jun 2021 13:07:32 +0200 Subject: [PATCH 1/6] Read/write css file from app settings. Changed order of operations at app startup. Added nano to Dockerfile --- Dockerfile | 2 + Dockerfile.multiarch | 2 + client/.env | 2 +- client/public/flame.css | 0 client/public/index.html | 11 ++--- client/public/robots.txt | 3 +- client/src/components/Settings/Settings.tsx | 9 ++++ .../Settings/StyleSettings/StyleSettings.tsx | 44 +++++++++++++++++++ .../UI/Forms/InputGroup/InputGroup.module.css | 11 ++++- controllers/config.js | 29 ++++++++++++ db.js | 15 ++++--- models/associateModels.js | 8 ++-- routes/config.js | 7 +++ server.js | 28 ++++++------ utils/File.js | 25 +++++++++++ 15 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 client/public/flame.css create mode 100644 client/src/components/Settings/StyleSettings/StyleSettings.tsx create mode 100644 utils/File.js diff --git a/Dockerfile b/Dockerfile index cd99f47..148d970 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,7 @@ FROM node:14-alpine +RUN apk update && apk add --no-cache nano + WORKDIR /app COPY package*.json ./ diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index 0cb1788..e0827af 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -1,5 +1,7 @@ FROM node:14-alpine +RUN apk update && apk add --no-cache nano + WORKDIR /app COPY package*.json ./ diff --git a/client/.env b/client/.env index 5a3822f..d53bf3c 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.0 \ No newline at end of file +REACT_APP_VERSION=1.4.1 \ No newline at end of file diff --git a/client/public/flame.css b/client/public/flame.css new file mode 100644 index 0000000..e69de29 diff --git a/client/public/index.html b/client/public/index.html index 2ede77f..3f43c40 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -4,15 +4,10 @@ - - - - + + Flame @@ -21,4 +16,4 @@
- + \ No newline at end of file diff --git a/client/public/robots.txt b/client/public/robots.txt index e9e57dc..77470cb 100644 --- a/client/public/robots.txt +++ b/client/public/robots.txt @@ -1,3 +1,2 @@ -# https://www.robotstxt.org/robotstxt.html User-agent: * -Disallow: +Disallow: / \ No newline at end of file diff --git a/client/src/components/Settings/Settings.tsx b/client/src/components/Settings/Settings.tsx index 49b08bd..b1eb300 100644 --- a/client/src/components/Settings/Settings.tsx +++ b/client/src/components/Settings/Settings.tsx @@ -9,6 +9,7 @@ import Themer from '../Themer/Themer'; import WeatherSettings from './WeatherSettings/WeatherSettings'; import OtherSettings from './OtherSettings/OtherSettings'; import AppDetails from './AppDetails/AppDetails'; +import StyleSettings from './StyleSettings/StyleSettings'; const Settings = (): JSX.Element => { return ( @@ -40,6 +41,13 @@ const Settings = (): JSX.Element => { to='/settings/other'> Other + + CSS + { + diff --git a/client/src/components/Settings/StyleSettings/StyleSettings.tsx b/client/src/components/Settings/StyleSettings/StyleSettings.tsx new file mode 100644 index 0000000..f73ac1d --- /dev/null +++ b/client/src/components/Settings/StyleSettings/StyleSettings.tsx @@ -0,0 +1,44 @@ +import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; +import axios from 'axios'; + +import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; +import Button from '../../UI/Buttons/Button/Button'; +import { ApiResponse } from '../../../interfaces'; + +const StyleSettings = (): JSX.Element => { + const [customStyles, setCustomStyles] = useState(''); + + useEffect(() => { + axios.get>('/api/config/0/css') + .then(data => setCustomStyles(data.data.data)) + .catch(err => console.log(err)); + }, []) + + const inputChangeHandler = (e: ChangeEvent) => { + e.preventDefault(); + setCustomStyles(e.target.value); + } + + const formSubmitHandler = (e: FormEvent) => { + e.preventDefault(); + + axios.put>('/api/config/0/css', { styles: customStyles }); + } + + return ( +
formSubmitHandler(e)}> + + + + + +
+ ) +} + +export default StyleSettings; \ No newline at end of file diff --git a/client/src/components/UI/Forms/InputGroup/InputGroup.module.css b/client/src/components/UI/Forms/InputGroup/InputGroup.module.css index 6241764..93b74f1 100644 --- a/client/src/components/UI/Forms/InputGroup/InputGroup.module.css +++ b/client/src/components/UI/Forms/InputGroup/InputGroup.module.css @@ -4,12 +4,14 @@ .InputGroup label, .InputGroup span, -.InputGroup input { +.InputGroup input, +.InputGroup textarea { display: block; } .InputGroup input, -.InputGroup select { +.InputGroup select, +.InputGroup textarea { margin: 8px 0; width: 100%; border: none; @@ -30,4 +32,9 @@ .InputGroup label { color: var(--color-primary); +} + +.InputGroup textarea { + resize: none; + height: 50vh; } \ No newline at end of file diff --git a/controllers/config.js b/controllers/config.js index f8f3613..ef4f707 100644 --- a/controllers/config.js +++ b/controllers/config.js @@ -2,6 +2,8 @@ const asyncWrapper = require('../middleware/asyncWrapper'); const ErrorResponse = require('../utils/ErrorResponse'); const Config = require('../models/Config'); const { Op } = require('sequelize'); +const File = require('../utils/File'); +const { join } = require('path'); // @desc Insert new key:value pair // @route POST /api/config @@ -122,6 +124,33 @@ exports.deletePair = asyncWrapper(async (req, res, next) => { await pair.destroy(); + res.status(200).json({ + success: true, + data: {} + }) +}) + +// @desc Get custom CSS file +// @route GET /api/config/0/css +// @access Public +exports.getCss = asyncWrapper(async (req, res, next) => { + const file = new File(join(__dirname, '../public/flame.css')); + const content = file.read(); + + res.status(200).json({ + success: true, + data: content + }) +}) + + +// @desc Update custom CSS file +// @route PUT /api/config/0/css +// @access Public +exports.updateCss = asyncWrapper(async (req, res, next) => { + const file = new File(join(__dirname, '../public/flame.css')); + file.write(req.body.styles); + res.status(200).json({ success: true, data: {} diff --git a/db.js b/db.js index bc4c536..7728737 100644 --- a/db.js +++ b/db.js @@ -4,21 +4,26 @@ const sequelize = new Sequelize({ dialect: 'sqlite', storage: './data/db.sqlite', logging: false -}); +}) const connectDB = async () => { try { await sequelize.authenticate(); console.log('Connected to database'); - await sequelize.sync({ alter: true }); - console.log('All models were synced'); + const syncModels = true; + + if (syncModels) { + console.log('Starting model synchronization'); + await sequelize.sync({ alter: true }); + console.log('All models were synchronized'); + } } catch (error) { - console.error('Unable to connect to the database:', error); + throw new Error(`Unable to connect to the database: ${error.message}`); } } module.exports = { connectDB, sequelize -}; \ No newline at end of file +} \ No newline at end of file diff --git a/models/associateModels.js b/models/associateModels.js index 2457092..d1b86c1 100644 --- a/models/associateModels.js +++ b/models/associateModels.js @@ -2,12 +2,14 @@ const Category = require('./Category'); const Bookmark = require('./Bookmark'); const associateModels = () => { - // Category <> Bookmark Category.hasMany(Bookmark, { - as: 'bookmarks', + foreignKey: 'categoryId', + as: 'bookmarks' + }); + + Bookmark.belongsTo(Category, { foreignKey: 'categoryId' }); - Bookmark.belongsTo(Category, { foreignKey: 'categoryId' }); } module.exports = associateModels; \ No newline at end of file diff --git a/routes/config.js b/routes/config.js index caadbe5..eebf5dd 100644 --- a/routes/config.js +++ b/routes/config.js @@ -8,6 +8,8 @@ const { updateValue, updateValues, deletePair, + updateCss, + getCss, } = require('../controllers/config'); router @@ -22,4 +24,9 @@ router .put(updateValue) .delete(deletePair); +router + .route('/0/css') + .get(getCss) + .put(updateCss); + module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index 569b7ac..c15ebf0 100644 --- a/server.js +++ b/server.js @@ -10,20 +10,20 @@ const initConfig = require('./utils/initConfig'); const PORT = process.env.PORT || 5005; -connectDB() - .then(() => { - associateModels(); - initConfig(); - }); +(async () => { + await connectDB(); + await associateModels(); + await initConfig(); -// Create server for Express API and WebSockets -const server = http.createServer(); -server.on('request', api); + // Create server for Express API and WebSockets + const server = http.createServer(); + server.on('request', api); -// Register weatherSocket -const weatherSocket = new Socket(server); -Sockets.registerSocket('weather', weatherSocket); + // Register weatherSocket + const weatherSocket = new Socket(server); + Sockets.registerSocket('weather', weatherSocket); -server.listen(PORT, () => { - console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); -}) \ No newline at end of file + server.listen(PORT, () => { + console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); + }) +})(); \ No newline at end of file diff --git a/utils/File.js b/utils/File.js new file mode 100644 index 0000000..0b2fbdc --- /dev/null +++ b/utils/File.js @@ -0,0 +1,25 @@ +const fs = require('fs'); + +class File { + constructor(path) { + this.path = path; + this.content = ''; + } + + read() { + try { + const content = fs.readFileSync(this.path, { encoding: 'utf-8' }); + this.content = content; + return this.content; + } catch (err) { + return err.message; + } + } + + write(data) { + this.content = data; + fs.writeFileSync(this.path, this.content); + } +} + +module.exports = File; \ No newline at end of file From e3ed429da141f24088e914e5b84f2ba4070c8af9 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 22 Jun 2021 14:49:00 +0200 Subject: [PATCH 2/6] Imporved logger --- .dockerignore | 3 +- .gitignore | 5 +- Socket.js | 4 +- client/.env | 2 +- .../Settings/StyleSettings/StyleSettings.tsx | 29 +++++++-- db.js | 15 +++-- middleware/errorHandler.js | 5 +- server.js | 4 +- utils/Logger.js | 63 +++++++++---------- utils/clearWeatherData.js | 4 +- utils/initConfig.js | 4 +- utils/jobs.js | 6 +- 12 files changed, 89 insertions(+), 55 deletions(-) diff --git a/.dockerignore b/.dockerignore index c77a412..5fcee18 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ node_modules -github \ No newline at end of file +github +public \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff227fe..2955045 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -node_modules/ -data/ \ No newline at end of file +node_modules +data +public \ No newline at end of file diff --git a/Socket.js b/Socket.js index 0ae1f56..a6a58c6 100644 --- a/Socket.js +++ b/Socket.js @@ -1,11 +1,13 @@ const WebSocket = require('ws'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); class Socket { constructor(server) { this.webSocketServer = new WebSocket.Server({ server }) this.webSocketServer.on('listening', () => { - console.log('Socket: listen'); + logger.log('Socket: listen'); }) this.webSocketServer.on('connection', (webSocketClient) => { diff --git a/client/.env b/client/.env index d53bf3c..a0df532 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.1 \ No newline at end of file +REACT_APP_VERSION=1.4.2 \ No newline at end of file diff --git a/client/src/components/Settings/StyleSettings/StyleSettings.tsx b/client/src/components/Settings/StyleSettings/StyleSettings.tsx index f73ac1d..9f45065 100644 --- a/client/src/components/Settings/StyleSettings/StyleSettings.tsx +++ b/client/src/components/Settings/StyleSettings/StyleSettings.tsx @@ -1,17 +1,28 @@ import { useState, useEffect, ChangeEvent, FormEvent } from 'react'; import axios from 'axios'; +// Redux +import { connect } from 'react-redux'; +import { createNotification } from '../../../store/actions'; + +// Typescript +import { ApiResponse, NewNotification } from '../../../interfaces'; + +// UI import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; -import { ApiResponse } from '../../../interfaces'; -const StyleSettings = (): JSX.Element => { +interface ComponentProps { + createNotification: (notification: NewNotification) => void; +} + +const StyleSettings = (props: ComponentProps): JSX.Element => { const [customStyles, setCustomStyles] = useState(''); useEffect(() => { axios.get>('/api/config/0/css') .then(data => setCustomStyles(data.data.data)) - .catch(err => console.log(err)); + .catch(err => console.log(err.response)); }, []) const inputChangeHandler = (e: ChangeEvent) => { @@ -22,7 +33,14 @@ const StyleSettings = (): JSX.Element => { const formSubmitHandler = (e: FormEvent) => { e.preventDefault(); - axios.put>('/api/config/0/css', { styles: customStyles }); + axios.put>('/api/config/0/css', { styles: customStyles }) + .then(() => { + props.createNotification({ + title: 'Success', + message: 'CSS saved. Reload page to see changes' + }) + }) + .catch(err => console.log(err.response)); } return ( @@ -34,6 +52,7 @@ const StyleSettings = (): JSX.Element => { name='customStyles' value={customStyles} onChange={(e) => inputChangeHandler(e)} + spellCheck={false} > @@ -41,4 +60,4 @@ const StyleSettings = (): JSX.Element => { ) } -export default StyleSettings; \ No newline at end of file +export default connect(null, { createNotification })(StyleSettings); \ No newline at end of file diff --git a/db.js b/db.js index 7728737..94932e8 100644 --- a/db.js +++ b/db.js @@ -1,4 +1,6 @@ const { Sequelize } = require('sequelize'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); const sequelize = new Sequelize({ dialect: 'sqlite', @@ -9,17 +11,18 @@ const sequelize = new Sequelize({ const connectDB = async () => { try { await sequelize.authenticate(); - console.log('Connected to database'); + logger.log('Connected to database'); + + const syncModels = false; - const syncModels = true; - if (syncModels) { - console.log('Starting model synchronization'); + logger.log('Starting model synchronization'); await sequelize.sync({ alter: true }); - console.log('All models were synchronized'); + logger.log('All models were synchronized'); } } catch (error) { - throw new Error(`Unable to connect to the database: ${error.message}`); + logger.log(`Unable to connect to the database: ${error.message}`, 'ERROR'); + process.exit(1); } } diff --git a/middleware/errorHandler.js b/middleware/errorHandler.js index 2de45a7..5db2bb2 100644 --- a/middleware/errorHandler.js +++ b/middleware/errorHandler.js @@ -1,5 +1,7 @@ const ErrorResponse = require('../utils/ErrorResponse'); const colors = require('colors'); +const Logger = require('../utils/Logger'); +const logger = new Logger(); const errorHandler = (err, req, res, next) => { let error = { ...err }; @@ -10,8 +12,7 @@ const errorHandler = (err, req, res, next) => { // error = new ErrorResponse(`Field ${msg}`, 400); // } - console.log(error); - console.log(`${err}`); + logger.log(error.message.split(',')[0], 'ERROR'); res.status(err.statusCode || 500).json({ success: false, diff --git a/server.js b/server.js index c15ebf0..6ce25d0 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,8 @@ const Socket = require('./Socket'); const Sockets = require('./Sockets'); const associateModels = require('./models/associateModels'); const initConfig = require('./utils/initConfig'); +const Logger = require('./utils/Logger'); +const logger = new Logger(); const PORT = process.env.PORT || 5005; @@ -24,6 +26,6 @@ const PORT = process.env.PORT || 5005; Sockets.registerSocket('weather', weatherSocket); server.listen(PORT, () => { - console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); + logger.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode`); }) })(); \ No newline at end of file diff --git a/utils/Logger.js b/utils/Logger.js index 6301dfe..6648212 100644 --- a/utils/Logger.js +++ b/utils/Logger.js @@ -1,40 +1,39 @@ -const fs = require('fs'); - class Logger { - constructor() { - this.logFileHandler(); + log(message, level = 'INFO') { + console.log(`[${this.generateTimestamp()}] [${level}] ${message}`) } - logFileHandler() { - if (!fs.existsSync('./flame.log')) { - fs.writeFileSync('./flame.log', ''); - } else { - console.log('file exists'); + generateTimestamp() { + const d = new Date(); + + // Date + const year = d.getFullYear(); + const month = this.parseDate(d.getMonth() + 1); + const day = this.parseDate(d.getDate()); + + // Time + const hour = this.parseDate(d.getHours()); + const minutes = this.parseDate(d.getMinutes()); + const seconds = this.parseDate(d.getSeconds()); + const miliseconds = this.parseDate(d.getMilliseconds(), true); + + // Timezone + const tz = -d.getTimezoneOffset() / 60; + + return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${tz > 0 ? '+' + tz : tz}`; + } + + parseDate(date, ms = false) { + if (ms) { + if (date >= 10 && date < 100) { + return `0${date}`; + } else if (date < 10) { + return `00${date}`; + } } - } - writeLog(logMsg, logType) { - - } - - generateLog(logMsg, logType) { - const now = new Date(); - const date = `${this.parseNumber(now.getDate())}-${this.parseNumber(now.getMonth() + 1)}-${now.getFullYear()}`; - const time = `${this.parseNumber(now.getHours())}:${this.parseNumber(now.getMinutes())}:${this.parseNumber(now.getSeconds())}.${now.getMilliseconds()}`; - const log = `[${date} ${time}]: ${logType} ${logMsg}`; - return log; - // const timestamp = new Date().toISOString(); - } - - parseNumber(number) { - if (number > 9) { - return number; - } else { - return `0${number}`; - } + return date < 10 ? `0${date}` : date.toString(); } } -// console.log(logger.generateLog('testMsg', 'INFO')); - -module.exports = new Logger(); \ No newline at end of file +module.exports = Logger; \ No newline at end of file diff --git a/utils/clearWeatherData.js b/utils/clearWeatherData.js index d43f975..07be15b 100644 --- a/utils/clearWeatherData.js +++ b/utils/clearWeatherData.js @@ -1,5 +1,7 @@ const { Op } = require('sequelize'); const Weather = require('../models/Weather'); +const Logger = require('./Logger'); +const logger = new Logger(); const clearWeatherData = async () => { const weather = await Weather.findOne({ @@ -16,7 +18,7 @@ const clearWeatherData = async () => { }) } - console.log('Old weather data was deleted'); + logger.log('Old weather data was deleted'); } module.exports = clearWeatherData; \ No newline at end of file diff --git a/utils/initConfig.js b/utils/initConfig.js index de1cc88..1455ccb 100644 --- a/utils/initConfig.js +++ b/utils/initConfig.js @@ -1,6 +1,8 @@ const { Op } = require('sequelize'); const Config = require('../models/Config'); const { config } = require('./initialConfig.json'); +const Logger = require('./Logger'); +const logger = new Logger(); const initConfig = async () => { // Get config values @@ -26,7 +28,7 @@ const initConfig = async () => { } }) - console.log('Initial config created'); + logger.log('Initial config created'); return; } diff --git a/utils/jobs.js b/utils/jobs.js index 19dc0a8..935f497 100644 --- a/utils/jobs.js +++ b/utils/jobs.js @@ -2,15 +2,17 @@ const schedule = require('node-schedule'); const getExternalWeather = require('./getExternalWeather'); const clearWeatherData = require('./clearWeatherData'); const Sockets = require('../Sockets'); +const Logger = require('./Logger'); +const logger = new Logger(); // Update weather data every 15 minutes const weatherJob = schedule.scheduleJob('updateWeather', '0 */15 * * * *', async () => { try { const weatherData = await getExternalWeather(); - console.log('weather updated'); + logger.log('Weather updated'); Sockets.getSocket('weather').socket.send(JSON.stringify(weatherData)); } catch (err) { - console.log(err.message); + logger.log(err.message, 'ERROR'); } }) From 6c067bee317c9aa91aa2800e00208aa4df9c5bcf Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Jun 2021 14:15:14 +0200 Subject: [PATCH 3/6] Option to open links in the same tab. Api upload icon. Render image icon instead of MDI. Dockerfile client dependencies fix. --- Dockerfile | 1 + Dockerfile.multiarch | 1 + README.md | 5 + api.js | 8 +- client/.env | 2 +- .../Apps/AppCard/AppCard.module.css | 8 ++ .../src/components/Apps/AppCard/AppCard.tsx | 12 +- .../Bookmarks/BookmarkCard/BookmarkCard.tsx | 4 +- .../Settings/OtherSettings/OtherSettings.tsx | 18 ++- client/src/interfaces/Forms.ts | 1 + client/src/setupProxy.js | 5 + controllers/apps.js | 9 +- middleware/multer.js | 27 ++++ package-lock.json | 115 ++++++++++++++++++ package.json | 2 + routes/apps.js | 3 +- utils/Logger.js | 2 +- utils/initialConfig.json | 4 + 18 files changed, 214 insertions(+), 13 deletions(-) create mode 100644 middleware/multer.js diff --git a/Dockerfile b/Dockerfile index 148d970..95ddea4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ COPY . . RUN mkdir -p ./public ./data \ && cd ./client \ + && npm install --production \ && npm run build \ && cd .. \ && mv ./client/build/* ./public \ diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index e0827af..808b815 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -13,6 +13,7 @@ COPY . . RUN mkdir -p ./public ./data \ && cd ./client \ + && npm install --production \ && npm run build \ && cd .. \ && mv ./client/build/* ./public \ diff --git a/README.md b/README.md index 6f4927c..9c86503 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ - Format: `www.domain.com`, `domain.com`, `sub.domain.com`, `local`, `ip`, `ip:port` - Redirect: `http://{dest}` +### Custom CSS +> This is an experimental feature. Its behaviour might change in the future. +> +Follow instructions from wiki: [Custom CSS](https://github.com/pawelmalak/flame/wiki/Custom-CSS) + ## Support If you want to support development of Flame and my upcoming self-hosted and open source projects you can use the following link: diff --git a/api.js b/api.js index 6ea1ab1..a720fe2 100644 --- a/api.js +++ b/api.js @@ -1,15 +1,17 @@ -const path = require('path'); +const { join } = require('path'); const express = require('express'); const errorHandler = require('./middleware/errorHandler'); const api = express(); // Static files -api.use(express.static(path.join(__dirname, 'public'))); +api.use(express.static(join(__dirname, 'public'))); +api.use('/uploads', express.static(join(__dirname, 'data/uploads'))); api.get(/^\/(?!api)/, (req, res) => { - res.sendFile(path.join(__dirname, 'public/index.html')); + res.sendFile(join(__dirname, 'public/index.html')); }) + // Body parser api.use(express.json()); diff --git a/client/.env b/client/.env index a0df532..601e08c 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.2 \ No newline at end of file +REACT_APP_VERSION=1.4.3 \ No newline at end of file diff --git a/client/src/components/Apps/AppCard/AppCard.module.css b/client/src/components/Apps/AppCard/AppCard.module.css index 26a8a69..768ef8e 100644 --- a/client/src/components/Apps/AppCard/AppCard.module.css +++ b/client/src/components/Apps/AppCard/AppCard.module.css @@ -39,4 +39,12 @@ .AppCard:hover { background-color: rgba(0,0,0,0.2); } +} + +.CustomIcon { + width: 90%; + height: 90%; + margin-top: 2px; + margin-left: 2px; + object-fit: contain; } \ No newline at end of file diff --git a/client/src/components/Apps/AppCard/AppCard.tsx b/client/src/components/Apps/AppCard/AppCard.tsx index 50fb979..79e09fe 100644 --- a/client/src/components/Apps/AppCard/AppCard.tsx +++ b/client/src/components/Apps/AppCard/AppCard.tsx @@ -3,6 +3,7 @@ import Icon from '../../UI/Icons/Icon/Icon'; import { iconParser, urlParser } from '../../../utility'; import { App } from '../../../interfaces'; +import { searchConfig } from '../../../utility'; interface ComponentProps { app: App; @@ -15,12 +16,19 @@ const AppCard = (props: ComponentProps): JSX.Element => { return (
- + {(/.(jpeg|jpg|png)$/).test(props.app.icon) + ? {`${props.app.name} + : + }
{props.app.name}
diff --git a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx index 9e8dff9..b0536d9 100644 --- a/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx +++ b/client/src/components/Bookmarks/BookmarkCard/BookmarkCard.tsx @@ -2,7 +2,7 @@ import { Bookmark, Category } from '../../../interfaces'; import classes from './BookmarkCard.module.css'; import Icon from '../../UI/Icons/Icon/Icon'; -import { iconParser, urlParser } from '../../../utility'; +import { iconParser, urlParser, searchConfig } from '../../../utility'; interface ComponentProps { category: Category; @@ -19,7 +19,7 @@ const BookmarkCard = (props: ComponentProps): JSX.Element => { return (
{bookmark.icon && ( diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index bba197d..3c63d83 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -29,7 +29,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: 1, pinCategoriesByDefault: 1, hideHeader: 0, - useOrdering: 'createdAt' + useOrdering: 'createdAt', + openSameTab: 0 }) // Get config @@ -39,7 +40,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: searchConfig('pinAppsByDefault', 1), pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1), hideHeader: searchConfig('hideHeader', 0), - useOrdering: searchConfig('useOrdering', 'createdAt') + useOrdering: searchConfig('useOrdering', 'createdAt'), + openSameTab: searchConfig('openSameTab', 0) }) }, [props.loading]); @@ -134,6 +136,18 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { + + + + ) diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 360dae1..948fb42 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -11,4 +11,5 @@ export interface SettingsForm { pinCategoriesByDefault: number; hideHeader: number; useOrdering: string; + openSameTab: number; } \ No newline at end of file diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js index 5cafcb1..b45d4fe 100644 --- a/client/src/setupProxy.js +++ b/client/src/setupProxy.js @@ -5,11 +5,16 @@ module.exports = function (app) { target: 'http://localhost:5005' }) + const assetsProxy = createProxyMiddleware('/uploads', { + target: 'http://localhost:5005' + }) + const wsProxy = createProxyMiddleware('/socket', { target: 'http://localhost:5005', ws: true }) app.use(apiProxy); + app.use(assetsProxy); app.use(wsProxy); }; \ No newline at end of file diff --git a/controllers/apps.js b/controllers/apps.js index 4f50f96..267daa3 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -14,11 +14,18 @@ exports.createApp = asyncWrapper(async (req, res, next) => { }); let app; + let _body = { ...req.body }; + + if (req.file) { + console.log(req.file.filename) + _body.icon = req.file.filename; + } + if (pinApps) { if (parseInt(pinApps.value)) { app = await App.create({ - ...req.body, + ..._body, isPinned: true }) } else { diff --git a/middleware/multer.js b/middleware/multer.js new file mode 100644 index 0000000..d1d0556 --- /dev/null +++ b/middleware/multer.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const { join } = require('path'); +const multer = require('multer'); +const uuid = require('uuid'); + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, './data/uploads'); + }, + filename: (req, file, cb) => { + cb(null, Date.now() + '--' + file.originalname); + } +}) + +const supportedTypes = ['jpg', 'jpeg', 'png']; + +const fileFilter = (req, file, cb) => { + if (supportedTypes.includes(file.mimetype.split('/')[1])) { + cb(null, true); + } else { + cb(null, false); + } +} + +const upload = multer({ storage, fileFilter }); + +module.exports = upload.single('icon'); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6ac480a..21017a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,6 +224,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -364,6 +369,43 @@ "fill-range": "^7.0.1" } }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -553,6 +595,17 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "concurrently": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.2.tgz", @@ -741,6 +794,38 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -1611,6 +1696,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "needle": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.6.0.tgz", @@ -2411,6 +2511,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-width": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", @@ -2577,6 +2682,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -2804,6 +2914,11 @@ "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", "dev": true }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index b958be8..243a5f0 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "concurrently": "^6.0.2", "dotenv": "^9.0.0", "express": "^4.17.1", + "multer": "^1.4.2", "node-schedule": "^2.0.0", "sequelize": "^6.6.2", "sqlite3": "^5.0.2", + "uuid": "^8.3.2", "ws": "^7.4.6" }, "devDependencies": { diff --git a/routes/apps.js b/routes/apps.js index a0b3f47..091550c 100644 --- a/routes/apps.js +++ b/routes/apps.js @@ -1,5 +1,6 @@ const express = require('express'); const router = express.Router(); +const upload = require('../middleware/multer'); const { createApp, @@ -12,7 +13,7 @@ const { router .route('/') - .post(createApp) + .post(upload, createApp) .get(getApps); router diff --git a/utils/Logger.js b/utils/Logger.js index 6648212..1d1deef 100644 --- a/utils/Logger.js +++ b/utils/Logger.js @@ -20,7 +20,7 @@ class Logger { // Timezone const tz = -d.getTimezoneOffset() / 60; - return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${tz > 0 ? '+' + tz : tz}`; + return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}.${miliseconds} UTC${tz >= 0 ? '+' + tz : tz}`; } parseDate(date, ms = false) { diff --git a/utils/initialConfig.json b/utils/initialConfig.json index 09bf4b8..ab0a861 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -35,6 +35,10 @@ { "key": "useOrdering", "value": "createdAt" + }, + { + "key": "openSameTab", + "value": false } ] } \ No newline at end of file From 12974ab01b8e84bd3fd9dc6ff9c6a812ecb0cb8b Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 23 Jun 2021 15:27:46 +0200 Subject: [PATCH 4/6] Upload custom icon on client --- Dockerfile | 2 +- Dockerfile.multiarch | 2 +- .../Apps/AppForm/AppForm.module.css | 7 ++ .../src/components/Apps/AppForm/AppForm.tsx | 90 ++++++++++++++----- client/src/store/actions/app.ts | 8 +- controllers/apps.js | 1 - 6 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 client/src/components/Apps/AppForm/AppForm.module.css diff --git a/Dockerfile b/Dockerfile index 95ddea4..ac2a11a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN npm install --production COPY . . -RUN mkdir -p ./public ./data \ +RUN mkdir -p ./public ./data ./data/uploads \ && cd ./client \ && npm install --production \ && npm run build \ diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index 808b815..c379e7b 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -11,7 +11,7 @@ RUN apk --no-cache --virtual build-dependencies add python make g++ \ COPY . . -RUN mkdir -p ./public ./data \ +RUN mkdir -p ./public ./data ./data/uploads \ && cd ./client \ && npm install --production \ && npm run build \ diff --git a/client/src/components/Apps/AppForm/AppForm.module.css b/client/src/components/Apps/AppForm/AppForm.module.css new file mode 100644 index 0000000..66b15a0 --- /dev/null +++ b/client/src/components/Apps/AppForm/AppForm.module.css @@ -0,0 +1,7 @@ +.Switch { + text-decoration: underline; +} + +.Switch:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index e9c7beb..61d2023 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -3,18 +3,23 @@ import { connect } from 'react-redux'; import { addApp, updateApp } from '../../../store/actions'; import { App, NewApp } from '../../../interfaces'; +import classes from './AppForm.module.css'; + import ModalForm from '../../UI/Forms/ModalForm/ModalForm'; import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; +import axios from 'axios'; interface ComponentProps { modalHandler: () => void; - addApp: (formData: NewApp) => any; + addApp: (formData: NewApp | FormData) => any; updateApp: (id: number, formData: NewApp) => any; app?: App; } const AppForm = (props: ComponentProps): JSX.Element => { + const [useCustomIcon, toggleUseCustomIcon] = useState(true); + const [customIcon, setCustomIcon] = useState(null); const [formData, setFormData] = useState({ name: '', url: '', @@ -52,11 +57,27 @@ const AppForm = (props: ComponentProps): JSX.Element => { }) } + const fileChangeHandler = (e: ChangeEvent): void => { + if (e.target.files) { + setCustomIcon(e.target.files[0]); + } + } + const formSubmitHandler = (e: SyntheticEvent): void => { e.preventDefault(); if (!props.app) { - props.addApp(formData); + if (customIcon) { + const data = new FormData(); + data.append('icon', customIcon); + + data.append('name', formData.name); + data.append('url', formData.url); + + props.addApp(data); + } else { + props.addApp(formData); + } } else { props.updateApp(props.app.id, formData); props.modalHandler(); @@ -108,26 +129,51 @@ const AppForm = (props: ComponentProps): JSX.Element => { - - - inputChangeHandler(e)} - /> - - Use icon name from MDI. - - {' '}Click here for reference - - - + {!useCustomIcon + // use mdi icon + ? ( + + inputChangeHandler(e)} + /> + + Use icon name from MDI. + + {' '}Click here for reference + + + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to custom icon upload + + ) + // upload custom icon + : ( + + fileChangeHandler(e)} + accept='.jpg,.jpeg,.png' + /> + toggleUseCustomIcon(!useCustomIcon)} + className={classes.Switch}> + Switch to MDI + + ) + } {!props.app ? : diff --git a/client/src/store/actions/app.ts b/client/src/store/actions/app.ts index 97db1c7..3a8e7d5 100644 --- a/client/src/store/actions/app.ts +++ b/client/src/store/actions/app.ts @@ -61,7 +61,7 @@ export interface AddAppAction { payload: App; } -export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { +export const addApp = (formData: NewApp | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.post>('/api/apps', formData); @@ -69,7 +69,7 @@ export const addApp = (formData: NewApp) => async (dispatch: Dispatch) => { type: ActionTypes.createNotification, payload: { title: 'Success', - message: `App ${formData.name} added` + message: `App added` } }) @@ -116,7 +116,7 @@ export interface UpdateAppAction { payload: App; } -export const updateApp = (id: number, formData: NewApp) => async (dispatch: Dispatch) => { +export const updateApp = (id: number, formData: NewApp | FormData) => async (dispatch: Dispatch) => { try { const res = await axios.put>(`/api/apps/${id}`, formData); @@ -124,7 +124,7 @@ export const updateApp = (id: number, formData: NewApp) => async (dispatch: Disp type: ActionTypes.createNotification, payload: { title: 'Success', - message: `App ${formData.name} updated` + message: `App updated` } }) diff --git a/controllers/apps.js b/controllers/apps.js index 267daa3..238c66b 100644 --- a/controllers/apps.js +++ b/controllers/apps.js @@ -17,7 +17,6 @@ exports.createApp = asyncWrapper(async (req, res, next) => { let _body = { ...req.body }; if (req.file) { - console.log(req.file.filename) _body.icon = req.file.filename; } From 550e1e155b9d3b5e30a8c82469775472982cea3e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 24 Jun 2021 10:54:48 +0200 Subject: [PATCH 5/6] Added option to hide apps and categories from home screen --- client/.env | 2 +- .../src/components/Apps/AppForm/AppForm.tsx | 2 +- client/src/components/Home/Home.tsx | 43 ++++++++----- .../OtherSettings/OtherSettings.module.css | 9 +++ .../Settings/OtherSettings/OtherSettings.tsx | 63 +++++++++++++++---- client/src/interfaces/Forms.ts | 2 + utils/initialConfig.json | 8 +++ 7 files changed, 98 insertions(+), 31 deletions(-) create mode 100644 client/src/components/Settings/OtherSettings/OtherSettings.module.css diff --git a/client/.env b/client/.env index 601e08c..c86ae30 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.3 \ No newline at end of file +REACT_APP_VERSION=1.4.4 \ No newline at end of file diff --git a/client/src/components/Apps/AppForm/AppForm.tsx b/client/src/components/Apps/AppForm/AppForm.tsx index 61d2023..a8881c2 100644 --- a/client/src/components/Apps/AppForm/AppForm.tsx +++ b/client/src/components/Apps/AppForm/AppForm.tsx @@ -18,7 +18,7 @@ interface ComponentProps { } const AppForm = (props: ComponentProps): JSX.Element => { - const [useCustomIcon, toggleUseCustomIcon] = useState(true); + const [useCustomIcon, toggleUseCustomIcon] = useState(false); const [customIcon, setCustomIcon] = useState(null); const [formData, setFormData] = useState({ name: '', diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 078c3a3..3742234 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, Fragment } from 'react'; import { Link } from 'react-router-dom'; // Redux @@ -101,24 +101,33 @@ const Home = (props: ComponentProps): JSX.Element => { :
} - - {appsLoading - ? - : app.isPinned)} - totalApps={apps.length} - /> + {searchConfig('hideApps', 0) !== 1 + ? ( + + {appsLoading + ? + : app.isPinned)} + totalApps={apps.length} + /> + } +
+
) + :
} -
- - - {categoriesLoading - ? - : category.isPinned)} - totalCategories={categories.length} - /> + {searchConfig('hideCategories', 0) !== 1 + ? ( + + {categoriesLoading + ? + : category.isPinned)} + totalCategories={categories.length} + /> + } + ) + :
} diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.module.css b/client/src/components/Settings/OtherSettings/OtherSettings.module.css new file mode 100644 index 0000000..36e4deb --- /dev/null +++ b/client/src/components/Settings/OtherSettings/OtherSettings.module.css @@ -0,0 +1,9 @@ +.SettingsSection { + color: var(--color-primary); + padding-bottom: 3px; + margin-bottom: 10px; + font-size: 20px; + font-weight: 500; + border-bottom: 2px solid var(--color-accent); + display: inline-block; +} \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index 3c63d83..b3e4c3e 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -11,6 +11,9 @@ import { GlobalState, NewNotification, SettingsForm } from '../../../interfaces' import InputGroup from '../../UI/Forms/InputGroup/InputGroup'; import Button from '../../UI/Buttons/Button/Button'; +// CSS +import classes from './OtherSettings.module.css'; + // Utils import { searchConfig } from '../../../utility'; @@ -29,6 +32,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: 1, pinCategoriesByDefault: 1, hideHeader: 0, + hideApps: 0, + hideCategories: 0, useOrdering: 'createdAt', openSameTab: 0 }) @@ -40,6 +45,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { pinAppsByDefault: searchConfig('pinAppsByDefault', 1), pinCategoriesByDefault: searchConfig('pinCategoriesByDefault', 1), hideHeader: searchConfig('hideHeader', 0), + hideApps: searchConfig('hideApps', 0), + hideCategories: searchConfig('hideCategories', 0), useOrdering: searchConfig('useOrdering', 'createdAt'), openSameTab: searchConfig('openSameTab', 0) }) @@ -76,6 +83,8 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { return (
formSubmitHandler(e)}> + {/* OTHER OPTIONS */} +

Miscellaneous

{ onChange={(e) => inputChangeHandler(e)} /> + + {/* BEAHVIOR OPTIONS */} +

App Behavior

- - - - + + {/* MODULES OPTIONS */} +

Modules

+ + + + + + + + + + + +
) diff --git a/client/src/interfaces/Forms.ts b/client/src/interfaces/Forms.ts index 948fb42..e63fad7 100644 --- a/client/src/interfaces/Forms.ts +++ b/client/src/interfaces/Forms.ts @@ -10,6 +10,8 @@ export interface SettingsForm { pinAppsByDefault: number; pinCategoriesByDefault: number; hideHeader: number; + hideApps: number; + hideCategories: number; useOrdering: string; openSameTab: number; } \ No newline at end of file diff --git a/utils/initialConfig.json b/utils/initialConfig.json index ab0a861..a954456 100644 --- a/utils/initialConfig.json +++ b/utils/initialConfig.json @@ -39,6 +39,14 @@ { "key": "openSameTab", "value": false + }, + { + "key": "hideApps", + "value": false + }, + { + "key": "hideCategories", + "value": false } ] } \ No newline at end of file From 8026533a066b5cbfdf0f9b9e781e82a38d226829 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 24 Jun 2021 12:53:45 +0200 Subject: [PATCH 6/6] Added search bar --- Dockerfile | 2 +- Dockerfile.multiarch | 2 +- README.md | 17 ++++++++ client/.env | 2 +- client/src/components/Home/Home.tsx | 6 +++ .../components/SearchBox/SearchBox.module.css | 17 ++++++++ client/src/components/SearchBox/SearchBox.tsx | 29 ++++++++++++++ .../Settings/OtherSettings/OtherSettings.tsx | 14 +++++++ client/src/interfaces/Forms.ts | 1 + client/src/interfaces/Query.ts | 5 +++ client/src/interfaces/index.ts | 3 +- client/src/utility/index.ts | 3 +- client/src/utility/searchParser.ts | 22 +++++++++++ client/src/utility/searchQueries.json | 39 +++++++++++++++++++ db.js | 2 +- middleware/multer.js | 6 ++- package.json | 1 - utils/initialConfig.json | 4 ++ 18 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 client/src/components/SearchBox/SearchBox.module.css create mode 100644 client/src/components/SearchBox/SearchBox.tsx create mode 100644 client/src/interfaces/Query.ts create mode 100644 client/src/utility/searchParser.ts create mode 100644 client/src/utility/searchQueries.json diff --git a/Dockerfile b/Dockerfile index ac2a11a..95ddea4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN npm install --production COPY . . -RUN mkdir -p ./public ./data ./data/uploads \ +RUN mkdir -p ./public ./data \ && cd ./client \ && npm install --production \ && npm run build \ diff --git a/Dockerfile.multiarch b/Dockerfile.multiarch index c379e7b..808b815 100644 --- a/Dockerfile.multiarch +++ b/Dockerfile.multiarch @@ -11,7 +11,7 @@ RUN apk --no-cache --virtual build-dependencies add python make g++ \ COPY . . -RUN mkdir -p ./public ./data ./data/uploads \ +RUN mkdir -p ./public ./data \ && cd ./client \ && npm install --production \ && npm run build \ diff --git a/README.md b/README.md index 9c86503..cca0f49 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,23 @@ Follow instructions from wiki: [Installation without Docker](https://github.com/ ![Homescreen screenshot](./github/_themes.png) ## Usage +### Search bar +> While opening links, module will follow `Open all links in the same tab` setting +#### Supported search engines +| Name | Prefix | Search URL | +|------------|--------|-------------------------------------| +| Disroot | /ds | http://search.disroot.org/search?q= | +| DuckDuckGo | /d | https://duckduckgo.com/?q= | +| Google | /g | https://www.google.com/search?q= | + +#### Supported services +| Name | Prefix | Search URL | +|--------------------|--------|-----------------------------------------------| +| IMDb | /im | https://www.imdb.com/find?q= | +| Reddit | /r | -https://www.reddit.com/search?q= | +| The Movie Database | /mv | https://www.themoviedb.org/search?query= | +| Youtube | /yt | https://www.youtube.com/results?search_query= | + ### Setting up weather module 1. Obtain API Key from [Weather API](https://www.weatherapi.com/pricing.aspx). > Free plan allows for 1M calls per month. Flame is making less then 3K API calls per month. diff --git a/client/.env b/client/.env index c86ae30..70acd43 100644 --- a/client/.env +++ b/client/.env @@ -1 +1 @@ -REACT_APP_VERSION=1.4.4 \ No newline at end of file +REACT_APP_VERSION=1.5.0 \ No newline at end of file diff --git a/client/src/components/Home/Home.tsx b/client/src/components/Home/Home.tsx index 3742234..2682d50 100644 --- a/client/src/components/Home/Home.tsx +++ b/client/src/components/Home/Home.tsx @@ -22,6 +22,7 @@ import classes from './Home.module.css'; import AppGrid from '../Apps/AppGrid/AppGrid'; import BookmarkGrid from '../Bookmarks/BookmarkGrid/BookmarkGrid'; import WeatherWidget from '../Widgets/WeatherWidget/WeatherWidget'; +import SearchBox from '../SearchBox/SearchBox'; // Functions import { greeter } from './functions/greeter'; @@ -87,6 +88,11 @@ const Home = (props: ComponentProps): JSX.Element => { return ( + {searchConfig('hideSearch', 0) !== 1 + ? + :
+ } + {searchConfig('hideHeader', 0) !== 1 ? (
diff --git a/client/src/components/SearchBox/SearchBox.module.css b/client/src/components/SearchBox/SearchBox.module.css new file mode 100644 index 0000000..d9fbb4e --- /dev/null +++ b/client/src/components/SearchBox/SearchBox.module.css @@ -0,0 +1,17 @@ +.SearchBox { + width: 100%; + padding: 10px 0; + color: var(--color-primary); + /* font-size: 20px; */ + margin-bottom: 20px; + background-color: transparent; + border: none; + border-bottom: 2px solid var(--color-accent); + opacity: 0.5; + transition: all 0.2s; +} + +.SearchBox:focus { + opacity: 1; + outline: none; +} \ No newline at end of file diff --git a/client/src/components/SearchBox/SearchBox.tsx b/client/src/components/SearchBox/SearchBox.tsx new file mode 100644 index 0000000..ebc0cc6 --- /dev/null +++ b/client/src/components/SearchBox/SearchBox.tsx @@ -0,0 +1,29 @@ +import { useRef, useEffect, KeyboardEvent } from 'react'; + +import classes from './SearchBox.module.css'; +import { searchParser } from '../../utility'; + +const SearchBox = (): JSX.Element => { + const inputRef = useRef(document.createElement('input')); + + useEffect(() => { + inputRef.current.focus(); + }, []) + + const searchHandler = (e: KeyboardEvent) => { + if (e.code === 'Enter') { + searchParser(inputRef.current.value); + } + } + + return ( + searchHandler(e)} + /> + ) +} + +export default SearchBox; \ No newline at end of file diff --git a/client/src/components/Settings/OtherSettings/OtherSettings.tsx b/client/src/components/Settings/OtherSettings/OtherSettings.tsx index b3e4c3e..329b068 100644 --- a/client/src/components/Settings/OtherSettings/OtherSettings.tsx +++ b/client/src/components/Settings/OtherSettings/OtherSettings.tsx @@ -34,6 +34,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { hideHeader: 0, hideApps: 0, hideCategories: 0, + hideSearch: 0, useOrdering: 'createdAt', openSameTab: 0 }) @@ -47,6 +48,7 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { hideHeader: searchConfig('hideHeader', 0), hideApps: searchConfig('hideApps', 0), hideCategories: searchConfig('hideCategories', 0), + hideSearch: searchConfig('hideSearch', 0), useOrdering: searchConfig('useOrdering', 'createdAt'), openSameTab: searchConfig('openSameTab', 0) }) @@ -151,6 +153,18 @@ const OtherSettings = (props: ComponentProps): JSX.Element => { {/* MODULES OPTIONS */}

Modules

+ + + +