From ee0b4354933e5565138b6c28c6d9bbcf09b392b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 23 Mar 2022 13:02:32 +0100 Subject: [PATCH 01/16] Separated theme components --- .../GeneralSettings/GeneralSettings.tsx | 2 +- .../Themer/ThemeBuilder/ThemeBuilder.tsx | 3 ++ .../ThemeGrid.module.css} | 2 +- .../Settings/Themer/ThemeGrid/ThemeGrid.tsx | 30 +++++++++++++++++++ .../ThemePreview.module.css | 0 .../{ => ThemePreview}/ThemePreview.tsx | 2 +- .../src/components/Settings/Themer/Themer.tsx | 22 +++++--------- 7 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx rename client/src/components/Settings/Themer/{Themer.module.css => ThemeGrid/ThemeGrid.module.css} (99%) create mode 100644 client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx rename client/src/components/Settings/Themer/{ => ThemePreview}/ThemePreview.module.css (100%) rename client/src/components/Settings/Themer/{ => ThemePreview}/ThemePreview.tsx (93%) diff --git a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx index 4173c729..d88208e5 100644 --- a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx +++ b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx @@ -164,7 +164,7 @@ export const GeneralSettings = (): JSX.Element => { - {/* SEARCH SETTINGS */} + {/* === SEARCH OPTIONS === */} diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx new file mode 100644 index 00000000..c95eb38b --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx @@ -0,0 +1,3 @@ +export const ThemeBuilder = (): JSX.Element => { + return

theme builder

; +}; diff --git a/client/src/components/Settings/Themer/Themer.module.css b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.module.css similarity index 99% rename from client/src/components/Settings/Themer/Themer.module.css rename to client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.module.css index 986f6c5d..65dbd446 100644 --- a/client/src/components/Settings/Themer/Themer.module.css +++ b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.module.css @@ -15,4 +15,4 @@ .ThemerGrid { grid-template-columns: 1fr 1fr 1fr; } -} \ No newline at end of file +} diff --git a/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx new file mode 100644 index 00000000..bd889616 --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx @@ -0,0 +1,30 @@ +// Redux +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; + +// Components +import { ThemePreview } from '../ThemePreview/ThemePreview'; + +// Other +import { Theme } from '../../../../interfaces'; +import classes from './ThemeGrid.module.css'; + +interface Props { + themes: Theme[]; +} + +export const ThemeGrid = ({ themes }: Props): JSX.Element => { + const dispatch = useDispatch(); + const { setTheme } = bindActionCreators(actionCreators, dispatch); + + return ( +
+ {themes.map( + (theme: Theme, idx: number): JSX.Element => ( + + ) + )} +
+ ); +}; diff --git a/client/src/components/Settings/Themer/ThemePreview.module.css b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.module.css similarity index 100% rename from client/src/components/Settings/Themer/ThemePreview.module.css rename to client/src/components/Settings/Themer/ThemePreview/ThemePreview.module.css diff --git a/client/src/components/Settings/Themer/ThemePreview.tsx b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx similarity index 93% rename from client/src/components/Settings/Themer/ThemePreview.tsx rename to client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx index eccf872c..81a48fed 100644 --- a/client/src/components/Settings/Themer/ThemePreview.tsx +++ b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx @@ -1,4 +1,4 @@ -import { Theme } from '../../../interfaces/Theme'; +import { Theme } from '../../../../interfaces/Theme'; import classes from './ThemePreview.module.css'; interface Props { diff --git a/client/src/components/Settings/Themer/Themer.tsx b/client/src/components/Settings/Themer/Themer.tsx index 61fbb923..4274a343 100644 --- a/client/src/components/Settings/Themer/Themer.tsx +++ b/client/src/components/Settings/Themer/Themer.tsx @@ -9,11 +9,11 @@ import { actionCreators } from '../../../store'; import { Theme, ThemeSettingsForm } from '../../../interfaces'; // Components -import { ThemePreview } from './ThemePreview'; import { Button, InputGroup, SettingsHeadline } from '../../UI'; +import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder'; +import { ThemeGrid } from './ThemeGrid/ThemeGrid'; // Other -import classes from './Themer.module.css'; import { themes } from './themes.json'; import { State } from '../../../store/reducers'; import { inputHandler, themeSettingsTemplate } from '../../../utility'; @@ -25,10 +25,7 @@ export const Themer = (): JSX.Element => { } = useSelector((state: State) => state); const dispatch = useDispatch(); - const { setTheme, updateConfig } = bindActionCreators( - actionCreators, - dispatch - ); + const { updateConfig } = bindActionCreators(actionCreators, dispatch); // Initial state const [formData, setFormData] = useState( @@ -65,14 +62,11 @@ export const Themer = (): JSX.Element => { return ( - -
- {themes.map( - (theme: Theme, idx: number): JSX.Element => ( - - ) - )} -
+ + + + + {isAuthenticated && (
From e427fbf54c85f9fde185e427fcf4b3eedd923be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 23 Mar 2022 14:13:14 +0100 Subject: [PATCH 02/16] Added theme string normalization to initial process. Added getThemes controller --- controllers/themes/getThemes.js | 17 ++++ controllers/themes/index.js | 3 + utils/init/index.js | 2 + utils/init/initialFiles.json | 136 ++++++++++++++++++++++++++++++++ utils/init/normalizeTheme.js | 28 +++++++ utils/init/themes.json | 124 +++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+) create mode 100644 controllers/themes/getThemes.js create mode 100644 controllers/themes/index.js create mode 100644 utils/init/normalizeTheme.js create mode 100644 utils/init/themes.json diff --git a/controllers/themes/getThemes.js b/controllers/themes/getThemes.js new file mode 100644 index 00000000..af3ff13f --- /dev/null +++ b/controllers/themes/getThemes.js @@ -0,0 +1,17 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const File = require('../../utils/File'); + +// @desc Get themes file +// @route GET /api/themes +// @access Public +const getThemes = asyncWrapper(async (req, res, next) => { + const file = new File('data/themes.json'); + const content = JSON.parse(file.read()); + + res.status(200).json({ + success: true, + data: content.themes, + }); +}); + +module.exports = getThemes; diff --git a/controllers/themes/index.js b/controllers/themes/index.js new file mode 100644 index 00000000..84cc8e8e --- /dev/null +++ b/controllers/themes/index.js @@ -0,0 +1,3 @@ +module.exports = { + getThemes: require('./getThemes'), +}; diff --git a/utils/init/index.js b/utils/init/index.js index 4ff3e3b6..66c97cf9 100644 --- a/utils/init/index.js +++ b/utils/init/index.js @@ -1,11 +1,13 @@ const initConfig = require('./initConfig'); const initFiles = require('./initFiles'); const initDockerSecrets = require('./initDockerSecrets'); +const normalizeTheme = require('./normalizeTheme'); const initApp = async () => { initDockerSecrets(); await initFiles(); await initConfig(); + await normalizeTheme(); }; module.exports = initApp; diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json index 42354d7d..2168d799 100644 --- a/utils/init/initialFiles.json +++ b/utils/init/initialFiles.json @@ -27,6 +27,142 @@ "queries": [] }, "isJSON": true + }, + { + "name": "themes.json", + "msg": { + "created": "Created default theme file", + "found": "Found theme file" + }, + "paths": { + "src": "../../data", + "dest": "../../data" + }, + "template": { + "themes": [ + { + "name": "blackboard", + "colors": { + "background": "#1a1a1a", + "primary": "#FFFDEA", + "accent": "#5c5c5c" + } + }, + { + "name": "gazette", + "colors": { + "background": "#F2F7FF", + "primary": "#000000", + "accent": "#5c5c5c" + } + }, + { + "name": "espresso", + "colors": { + "background": "#21211F", + "primary": "#D1B59A", + "accent": "#4E4E4E" + } + }, + { + "name": "cab", + "colors": { + "background": "#F6D305", + "primary": "#1F1F1F", + "accent": "#424242" + } + }, + { + "name": "cloud", + "colors": { + "background": "#f1f2f0", + "primary": "#35342f", + "accent": "#37bbe4" + } + }, + { + "name": "lime", + "colors": { + "background": "#263238", + "primary": "#AABBC3", + "accent": "#aeea00" + } + }, + { + "name": "white", + "colors": { + "background": "#ffffff", + "primary": "#222222", + "accent": "#dddddd" + } + }, + { + "name": "tron", + "colors": { + "background": "#242B33", + "primary": "#EFFBFF", + "accent": "#6EE2FF" + } + }, + { + "name": "blues", + "colors": { + "background": "#2B2C56", + "primary": "#EFF1FC", + "accent": "#6677EB" + } + }, + { + "name": "passion", + "colors": { + "background": "#f5f5f5", + "primary": "#12005e", + "accent": "#8e24aa" + } + }, + { + "name": "chalk", + "colors": { + "background": "#263238", + "primary": "#AABBC3", + "accent": "#FF869A" + } + }, + { + "name": "paper", + "colors": { + "background": "#F8F6F1", + "primary": "#4C432E", + "accent": "#AA9A73" + } + }, + { + "name": "neon", + "colors": { + "background": "#091833", + "primary": "#EFFBFF", + "accent": "#ea00d9" + } + }, + { + "name": "pumpkin", + "colors": { + "background": "#2d3436", + "primary": "#EFFBFF", + "accent": "#ffa500" + } + }, + { + "name": "onedark", + "colors": { + "background": "#282c34", + "primary": "#dfd9d6", + "accent": "#98c379" + } + } + ] + }, + "isJSON": true } ] } diff --git a/utils/init/normalizeTheme.js b/utils/init/normalizeTheme.js new file mode 100644 index 00000000..272db526 --- /dev/null +++ b/utils/init/normalizeTheme.js @@ -0,0 +1,28 @@ +const { readFile, writeFile } = require('fs/promises'); + +const normalizeTheme = async () => { + // open main config file + const configFile = await readFile('data/config.json', 'utf8'); + const config = JSON.parse(configFile); + + // open default themes file + const themesFile = await readFile('utils/init/themes.json', 'utf8'); + const { themes } = JSON.parse(themesFile); + + // find theme + const theme = themes.find((t) => t.name === config.defaultTheme); + + if (theme) { + // save theme in new format + // PAB - primary;accent;background + const { primary: p, accent: a, background: b } = theme.colors; + const normalizedTheme = `${p};${a};${b}`; + + await writeFile( + 'data/config.json', + JSON.stringify({ ...config, defaultTheme: normalizedTheme }) + ); + } +}; + +module.exports = normalizeTheme; diff --git a/utils/init/themes.json b/utils/init/themes.json new file mode 100644 index 00000000..f3b12bdc --- /dev/null +++ b/utils/init/themes.json @@ -0,0 +1,124 @@ +{ + "themes": [ + { + "name": "blackboard", + "colors": { + "background": "#1a1a1a", + "primary": "#FFFDEA", + "accent": "#5c5c5c" + } + }, + { + "name": "gazette", + "colors": { + "background": "#F2F7FF", + "primary": "#000000", + "accent": "#5c5c5c" + } + }, + { + "name": "espresso", + "colors": { + "background": "#21211F", + "primary": "#D1B59A", + "accent": "#4E4E4E" + } + }, + { + "name": "cab", + "colors": { + "background": "#F6D305", + "primary": "#1F1F1F", + "accent": "#424242" + } + }, + { + "name": "cloud", + "colors": { + "background": "#f1f2f0", + "primary": "#35342f", + "accent": "#37bbe4" + } + }, + { + "name": "lime", + "colors": { + "background": "#263238", + "primary": "#AABBC3", + "accent": "#aeea00" + } + }, + { + "name": "white", + "colors": { + "background": "#ffffff", + "primary": "#222222", + "accent": "#dddddd" + } + }, + { + "name": "tron", + "colors": { + "background": "#242B33", + "primary": "#EFFBFF", + "accent": "#6EE2FF" + } + }, + { + "name": "blues", + "colors": { + "background": "#2B2C56", + "primary": "#EFF1FC", + "accent": "#6677EB" + } + }, + { + "name": "passion", + "colors": { + "background": "#f5f5f5", + "primary": "#12005e", + "accent": "#8e24aa" + } + }, + { + "name": "chalk", + "colors": { + "background": "#263238", + "primary": "#AABBC3", + "accent": "#FF869A" + } + }, + { + "name": "paper", + "colors": { + "background": "#F8F6F1", + "primary": "#4C432E", + "accent": "#AA9A73" + } + }, + { + "name": "neon", + "colors": { + "background": "#091833", + "primary": "#EFFBFF", + "accent": "#ea00d9" + } + }, + { + "name": "pumpkin", + "colors": { + "background": "#2d3436", + "primary": "#EFFBFF", + "accent": "#ffa500" + } + }, + { + "name": "onedark", + "colors": { + "background": "#282c34", + "primary": "#dfd9d6", + "accent": "#98c379" + } + } + ] +} From 89bd92187535d9c65b57f016d3239d0bece7e860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 23 Mar 2022 14:49:35 +0100 Subject: [PATCH 03/16] Changed how theme is set and stored on client --- client/src/App.tsx | 11 +- .../Settings/Themer/ThemeGrid/ThemeGrid.tsx | 10 +- .../Themer/ThemePreview/ThemePreview.tsx | 26 ++-- .../src/components/Settings/Themer/Themer.tsx | 10 +- .../components/Settings/Themer/themes.json | 124 ------------------ client/src/interfaces/Theme.ts | 14 +- client/src/store/action-creators/theme.ts | 38 +++--- client/src/store/action-types/index.ts | 1 + client/src/store/actions/theme.ts | 6 +- client/src/store/reducers/theme.ts | 16 +-- client/src/utility/index.ts | 1 + client/src/utility/parseTheme.ts | 20 +++ 12 files changed, 89 insertions(+), 188 deletions(-) delete mode 100644 client/src/components/Settings/Themer/themes.json create mode 100644 client/src/utility/parseTheme.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 68faaea9..e302c575 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,7 +10,7 @@ import { actionCreators, store } from './store'; import { State } from './store/reducers'; // Utils -import { checkVersion, decodeToken } from './utility'; +import { checkVersion, decodeToken, parsePABToTheme } from './utility'; // Routes import { Home } from './components/Home/Home'; @@ -31,7 +31,7 @@ export const App = (): JSX.Element => { const { config, loading } = useSelector((state: State) => state.config); const dispath = useDispatch(); - const { fetchQueries, setTheme, logout, createNotification } = + const { fetchQueries, setTheme, logout, createNotification, fetchThemes } = bindActionCreators(actionCreators, dispath); useEffect(() => { @@ -51,9 +51,12 @@ export const App = (): JSX.Element => { } }, 1000); + // load themes + fetchThemes(); + // set user theme if present if (localStorage.theme) { - setTheme(localStorage.theme); + setTheme(parsePABToTheme(localStorage.theme)); } // check for updated @@ -68,7 +71,7 @@ export const App = (): JSX.Element => { // If there is no user theme, set the default one useEffect(() => { if (!loading && !localStorage.theme) { - setTheme(config.defaultTheme, false); + setTheme(parsePABToTheme(config.defaultTheme), false); } }, [loading]); diff --git a/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx index bd889616..fbf5dab8 100644 --- a/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx +++ b/client/src/components/Settings/Themer/ThemeGrid/ThemeGrid.tsx @@ -1,8 +1,3 @@ -// Redux -import { useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { actionCreators } from '../../../../store'; - // Components import { ThemePreview } from '../ThemePreview/ThemePreview'; @@ -15,14 +10,11 @@ interface Props { } export const ThemeGrid = ({ themes }: Props): JSX.Element => { - const dispatch = useDispatch(); - const { setTheme } = bindActionCreators(actionCreators, dispatch); - return (
{themes.map( (theme: Theme, idx: number): JSX.Element => ( - + ) )}
diff --git a/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx index 81a48fed..ccbb42ed 100644 --- a/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx +++ b/client/src/components/Settings/Themer/ThemePreview/ThemePreview.tsx @@ -1,32 +1,38 @@ +// Redux +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; + +// Other import { Theme } from '../../../../interfaces/Theme'; import classes from './ThemePreview.module.css'; interface Props { theme: Theme; - applyTheme: Function; } -export const ThemePreview = (props: Props): JSX.Element => { +export const ThemePreview = ({ + theme: { colors, name }, +}: Props): JSX.Element => { + const { setTheme } = bindActionCreators(actionCreators, useDispatch()); + return ( -
props.applyTheme(props.theme.name)} - > +
setTheme(colors)}>
-

{props.theme.name}

+

{name}

); }; diff --git a/client/src/components/Settings/Themer/Themer.tsx b/client/src/components/Settings/Themer/Themer.tsx index 4274a343..f8de5ec6 100644 --- a/client/src/components/Settings/Themer/Themer.tsx +++ b/client/src/components/Settings/Themer/Themer.tsx @@ -9,12 +9,11 @@ import { actionCreators } from '../../../store'; import { Theme, ThemeSettingsForm } from '../../../interfaces'; // Components -import { Button, InputGroup, SettingsHeadline } from '../../UI'; +import { Button, InputGroup, SettingsHeadline, Spinner } from '../../UI'; import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder'; import { ThemeGrid } from './ThemeGrid/ThemeGrid'; // Other -import { themes } from './themes.json'; import { State } from '../../../store/reducers'; import { inputHandler, themeSettingsTemplate } from '../../../utility'; @@ -22,6 +21,7 @@ export const Themer = (): JSX.Element => { const { auth: { isAuthenticated }, config: { loading, config }, + theme: { themes }, } = useSelector((state: State) => state); const dispatch = useDispatch(); @@ -63,10 +63,10 @@ export const Themer = (): JSX.Element => { return ( - + {!themes.length ? : } - - + {/* + */} {isAuthenticated && ( diff --git a/client/src/components/Settings/Themer/themes.json b/client/src/components/Settings/Themer/themes.json deleted file mode 100644 index f3b12bdc..00000000 --- a/client/src/components/Settings/Themer/themes.json +++ /dev/null @@ -1,124 +0,0 @@ -{ - "themes": [ - { - "name": "blackboard", - "colors": { - "background": "#1a1a1a", - "primary": "#FFFDEA", - "accent": "#5c5c5c" - } - }, - { - "name": "gazette", - "colors": { - "background": "#F2F7FF", - "primary": "#000000", - "accent": "#5c5c5c" - } - }, - { - "name": "espresso", - "colors": { - "background": "#21211F", - "primary": "#D1B59A", - "accent": "#4E4E4E" - } - }, - { - "name": "cab", - "colors": { - "background": "#F6D305", - "primary": "#1F1F1F", - "accent": "#424242" - } - }, - { - "name": "cloud", - "colors": { - "background": "#f1f2f0", - "primary": "#35342f", - "accent": "#37bbe4" - } - }, - { - "name": "lime", - "colors": { - "background": "#263238", - "primary": "#AABBC3", - "accent": "#aeea00" - } - }, - { - "name": "white", - "colors": { - "background": "#ffffff", - "primary": "#222222", - "accent": "#dddddd" - } - }, - { - "name": "tron", - "colors": { - "background": "#242B33", - "primary": "#EFFBFF", - "accent": "#6EE2FF" - } - }, - { - "name": "blues", - "colors": { - "background": "#2B2C56", - "primary": "#EFF1FC", - "accent": "#6677EB" - } - }, - { - "name": "passion", - "colors": { - "background": "#f5f5f5", - "primary": "#12005e", - "accent": "#8e24aa" - } - }, - { - "name": "chalk", - "colors": { - "background": "#263238", - "primary": "#AABBC3", - "accent": "#FF869A" - } - }, - { - "name": "paper", - "colors": { - "background": "#F8F6F1", - "primary": "#4C432E", - "accent": "#AA9A73" - } - }, - { - "name": "neon", - "colors": { - "background": "#091833", - "primary": "#EFFBFF", - "accent": "#ea00d9" - } - }, - { - "name": "pumpkin", - "colors": { - "background": "#2d3436", - "primary": "#EFFBFF", - "accent": "#ffa500" - } - }, - { - "name": "onedark", - "colors": { - "background": "#282c34", - "primary": "#dfd9d6", - "accent": "#98c379" - } - } - ] -} diff --git a/client/src/interfaces/Theme.ts b/client/src/interfaces/Theme.ts index 97534271..4bf5bfd4 100644 --- a/client/src/interfaces/Theme.ts +++ b/client/src/interfaces/Theme.ts @@ -1,8 +1,10 @@ +export interface ThemeColors { + background: string; + primary: string; + accent: string; +} + export interface Theme { name: string; - colors: { - background: string; - primary: string; - accent: string; - } -} \ No newline at end of file + colors: ThemeColors; +} diff --git a/client/src/store/action-creators/theme.ts b/client/src/store/action-creators/theme.ts index 8eb6fefb..87bf184b 100644 --- a/client/src/store/action-creators/theme.ts +++ b/client/src/store/action-creators/theme.ts @@ -1,30 +1,32 @@ import { Dispatch } from 'redux'; -import { SetThemeAction } from '../actions/theme'; +import { FetchThemesAction, SetThemeAction } from '../actions/theme'; import { ActionType } from '../action-types'; -import { Theme } from '../../interfaces/Theme'; -import { themes } from '../../components/Settings/Themer/themes.json'; +import { Theme, ApiResponse, ThemeColors } from '../../interfaces'; +import { parseThemeToPAB } from '../../utility'; +import axios from 'axios'; export const setTheme = - (name: string, remeberTheme: boolean = true) => + (colors: ThemeColors, remeberTheme: boolean = true) => (dispatch: Dispatch) => { - const theme = themes.find((theme) => theme.name === name); + if (remeberTheme) { + localStorage.setItem('theme', parseThemeToPAB(colors)); + } - if (theme) { - if (remeberTheme) { - localStorage.setItem('theme', name); - } + for (const [key, value] of Object.entries(colors)) { + document.body.style.setProperty(`--color-${key}`, value); + } + }; - loadTheme(theme); +export const fetchThemes = + () => async (dispatch: Dispatch) => { + try { + const res = await axios.get>('/api/themes'); dispatch({ - type: ActionType.setTheme, - payload: theme, + type: ActionType.fetchThemes, + payload: res.data.data, }); + } catch (err) { + console.log(err); } }; - -export const loadTheme = (theme: Theme): void => { - for (const [key, value] of Object.entries(theme.colors)) { - document.body.style.setProperty(`--color-${key}`, value); - } -}; diff --git a/client/src/store/action-types/index.ts b/client/src/store/action-types/index.ts index 4be159fc..601308bf 100644 --- a/client/src/store/action-types/index.ts +++ b/client/src/store/action-types/index.ts @@ -1,6 +1,7 @@ export enum ActionType { // THEME setTheme = 'SET_THEME', + fetchThemes = 'FETCH_THEMES', // CONFIG getConfig = 'GET_CONFIG', updateConfig = 'UPDATE_CONFIG', diff --git a/client/src/store/actions/theme.ts b/client/src/store/actions/theme.ts index 036b1a33..6d40d213 100644 --- a/client/src/store/actions/theme.ts +++ b/client/src/store/actions/theme.ts @@ -3,5 +3,9 @@ import { Theme } from '../../interfaces'; export interface SetThemeAction { type: ActionType.setTheme; - payload: Theme; +} + +export interface FetchThemesAction { + type: ActionType.fetchThemes; + payload: Theme[]; } diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index 6db29fe8..c204e20f 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -3,18 +3,11 @@ import { ActionType } from '../action-types'; import { Theme } from '../../interfaces/Theme'; interface ThemeState { - theme: Theme; + themes: Theme[]; } const initialState: ThemeState = { - theme: { - name: 'tron', - colors: { - background: '#242B33', - primary: '#EFFBFF', - accent: '#6EE2FF', - }, - }, + themes: [], }; export const themeReducer = ( @@ -22,8 +15,9 @@ export const themeReducer = ( action: Action ): ThemeState => { switch (action.type) { - case ActionType.setTheme: - return { theme: action.payload }; + case ActionType.fetchThemes: { + return { themes: action.payload }; + } default: return state; diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 7358da4e..990db068 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -12,3 +12,4 @@ export * from './parseTime'; export * from './decodeToken'; export * from './applyAuth'; export * from './escapeRegex'; +export * from './parseTheme'; diff --git a/client/src/utility/parseTheme.ts b/client/src/utility/parseTheme.ts new file mode 100644 index 00000000..eaa800f9 --- /dev/null +++ b/client/src/utility/parseTheme.ts @@ -0,0 +1,20 @@ +import { ThemeColors } from '../interfaces'; + +// parse theme in PAB (primary;accent;background) format to theme colors object +export const parsePABToTheme = (themeStr: string): ThemeColors => { + const [primary, accent, background] = themeStr.split(';'); + + return { + primary, + accent, + background, + }; +}; + +export const parseThemeToPAB = ({ + primary: p, + accent: a, + background: b, +}: ThemeColors): string => { + return `${p};${a};${b}`; +}; From 48e28b9abdad609a39a37f21e86a839a560f26f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Wed, 23 Mar 2022 16:13:34 +0100 Subject: [PATCH 04/16] Added user themes section to Theme settings --- .../ThemeBuilder/ThemeBuilder.module.css | 7 +++ .../Themer/ThemeBuilder/ThemeBuilder.tsx | 22 ++++++++- .../src/components/Settings/Themer/Themer.tsx | 26 +++++++---- client/src/interfaces/Theme.ts | 1 + client/src/store/reducers/theme.ts | 14 +++++- client/src/utility/arrayPartition.ts | 11 +++++ client/src/utility/index.ts | 1 + utils/Logger.js | 8 ++-- utils/init/initialFiles.json | 45 ++++++++++++------- utils/init/themes.json | 45 ++++++++++++------- 10 files changed, 136 insertions(+), 44 deletions(-) create mode 100644 client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.module.css create mode 100644 client/src/utility/arrayPartition.ts diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.module.css b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.module.css new file mode 100644 index 00000000..2c63e136 --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.module.css @@ -0,0 +1,7 @@ +.ThemeBuilder { + margin-bottom: 30px; +} + +.Buttons button:not(:last-child) { + margin-right: 10px; +} diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx index c95eb38b..0762f13d 100644 --- a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx @@ -1,3 +1,21 @@ -export const ThemeBuilder = (): JSX.Element => { - return

theme builder

; +import { Theme } from '../../../../interfaces'; +import { Button } from '../../../UI'; +import { ThemeGrid } from '../ThemeGrid/ThemeGrid'; +import classes from './ThemeBuilder.module.css'; + +interface Props { + themes: Theme[]; +} + +export const ThemeBuilder = ({ themes }: Props): JSX.Element => { + return ( +
+ + +
+ + {themes.length && } +
+
+ ); }; diff --git a/client/src/components/Settings/Themer/Themer.tsx b/client/src/components/Settings/Themer/Themer.tsx index f8de5ec6..3bcaa2ed 100644 --- a/client/src/components/Settings/Themer/Themer.tsx +++ b/client/src/components/Settings/Themer/Themer.tsx @@ -15,13 +15,17 @@ import { ThemeGrid } from './ThemeGrid/ThemeGrid'; // Other import { State } from '../../../store/reducers'; -import { inputHandler, themeSettingsTemplate } from '../../../utility'; +import { + inputHandler, + parseThemeToPAB, + themeSettingsTemplate, +} from '../../../utility'; export const Themer = (): JSX.Element => { const { auth: { isAuthenticated }, config: { loading, config }, - theme: { themes }, + theme: { themes, userThemes }, } = useSelector((state: State) => state); const dispatch = useDispatch(); @@ -44,7 +48,7 @@ export const Themer = (): JSX.Element => { e.preventDefault(); // Save settings - await updateConfig(formData); + await updateConfig({ ...formData }); }; // Input handler @@ -65,8 +69,14 @@ export const Themer = (): JSX.Element => { {!themes.length ? : } - {/* - */} + {!userThemes.length ? ( + isAuthenticated && 'auth and empty' + ) : ( + + + + + )} {isAuthenticated && ( @@ -79,9 +89,9 @@ export const Themer = (): JSX.Element => { value={formData.defaultTheme} onChange={(e) => inputChangeHandler(e)} > - {themes.map((theme: Theme, idx) => ( - ))} diff --git a/client/src/interfaces/Theme.ts b/client/src/interfaces/Theme.ts index 4bf5bfd4..e319ef1b 100644 --- a/client/src/interfaces/Theme.ts +++ b/client/src/interfaces/Theme.ts @@ -7,4 +7,5 @@ export interface ThemeColors { export interface Theme { name: string; colors: ThemeColors; + isCustom: boolean; } diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index c204e20f..7738a3bc 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -1,13 +1,16 @@ import { Action } from '../actions'; import { ActionType } from '../action-types'; import { Theme } from '../../interfaces/Theme'; +import { arrayPartition } from '../../utility'; interface ThemeState { themes: Theme[]; + userThemes: Theme[]; } const initialState: ThemeState = { themes: [], + userThemes: [], }; export const themeReducer = ( @@ -16,7 +19,16 @@ export const themeReducer = ( ): ThemeState => { switch (action.type) { case ActionType.fetchThemes: { - return { themes: action.payload }; + const [themes, userThemes] = arrayPartition( + action.payload, + (e) => !e.isCustom + ); + + return { + ...state, + themes, + userThemes, + }; } default: diff --git a/client/src/utility/arrayPartition.ts b/client/src/utility/arrayPartition.ts new file mode 100644 index 00000000..eae67c3d --- /dev/null +++ b/client/src/utility/arrayPartition.ts @@ -0,0 +1,11 @@ +export const arrayPartition = ( + arr: T[], + isValid: (e: T) => boolean +): T[][] => { + let pass: T[] = []; + let fail: T[] = []; + + arr.forEach((e) => (isValid(e) ? pass : fail).push(e)); + + return [pass, fail]; +}; diff --git a/client/src/utility/index.ts b/client/src/utility/index.ts index 990db068..0d002bc3 100644 --- a/client/src/utility/index.ts +++ b/client/src/utility/index.ts @@ -13,3 +13,4 @@ export * from './decodeToken'; export * from './applyAuth'; export * from './escapeRegex'; export * from './parseTheme'; +export * from './arrayPartition'; diff --git a/utils/Logger.js b/utils/Logger.js index 1d1deef1..411b39f5 100644 --- a/utils/Logger.js +++ b/utils/Logger.js @@ -1,6 +1,6 @@ class Logger { log(message, level = 'INFO') { - console.log(`[${this.generateTimestamp()}] [${level}] ${message}`) + console.log(`[${this.generateTimestamp()}] [${level}] ${message}`); } generateTimestamp() { @@ -20,7 +20,9 @@ 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) { @@ -36,4 +38,4 @@ class Logger { } } -module.exports = Logger; \ No newline at end of file +module.exports = Logger; diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json index 2168d799..43b68d53 100644 --- a/utils/init/initialFiles.json +++ b/utils/init/initialFiles.json @@ -46,7 +46,8 @@ "background": "#1a1a1a", "primary": "#FFFDEA", "accent": "#5c5c5c" - } + }, + "isCustom": false }, { "name": "gazette", @@ -54,7 +55,8 @@ "background": "#F2F7FF", "primary": "#000000", "accent": "#5c5c5c" - } + }, + "isCustom": false }, { "name": "espresso", @@ -62,7 +64,8 @@ "background": "#21211F", "primary": "#D1B59A", "accent": "#4E4E4E" - } + }, + "isCustom": false }, { "name": "cab", @@ -70,7 +73,8 @@ "background": "#F6D305", "primary": "#1F1F1F", "accent": "#424242" - } + }, + "isCustom": false }, { "name": "cloud", @@ -78,7 +82,8 @@ "background": "#f1f2f0", "primary": "#35342f", "accent": "#37bbe4" - } + }, + "isCustom": false }, { "name": "lime", @@ -86,7 +91,8 @@ "background": "#263238", "primary": "#AABBC3", "accent": "#aeea00" - } + }, + "isCustom": false }, { "name": "white", @@ -94,7 +100,8 @@ "background": "#ffffff", "primary": "#222222", "accent": "#dddddd" - } + }, + "isCustom": false }, { "name": "tron", @@ -102,7 +109,8 @@ "background": "#242B33", "primary": "#EFFBFF", "accent": "#6EE2FF" - } + }, + "isCustom": false }, { "name": "blues", @@ -110,7 +118,8 @@ "background": "#2B2C56", "primary": "#EFF1FC", "accent": "#6677EB" - } + }, + "isCustom": false }, { "name": "passion", @@ -118,7 +127,8 @@ "background": "#f5f5f5", "primary": "#12005e", "accent": "#8e24aa" - } + }, + "isCustom": false }, { "name": "chalk", @@ -126,7 +136,8 @@ "background": "#263238", "primary": "#AABBC3", "accent": "#FF869A" - } + }, + "isCustom": false }, { "name": "paper", @@ -134,7 +145,8 @@ "background": "#F8F6F1", "primary": "#4C432E", "accent": "#AA9A73" - } + }, + "isCustom": false }, { "name": "neon", @@ -142,7 +154,8 @@ "background": "#091833", "primary": "#EFFBFF", "accent": "#ea00d9" - } + }, + "isCustom": false }, { "name": "pumpkin", @@ -150,7 +163,8 @@ "background": "#2d3436", "primary": "#EFFBFF", "accent": "#ffa500" - } + }, + "isCustom": false }, { "name": "onedark", @@ -158,7 +172,8 @@ "background": "#282c34", "primary": "#dfd9d6", "accent": "#98c379" - } + }, + "isCustom": false } ] }, diff --git a/utils/init/themes.json b/utils/init/themes.json index f3b12bdc..685a81e0 100644 --- a/utils/init/themes.json +++ b/utils/init/themes.json @@ -6,7 +6,8 @@ "background": "#1a1a1a", "primary": "#FFFDEA", "accent": "#5c5c5c" - } + }, + "isCustom": false }, { "name": "gazette", @@ -14,7 +15,8 @@ "background": "#F2F7FF", "primary": "#000000", "accent": "#5c5c5c" - } + }, + "isCustom": false }, { "name": "espresso", @@ -22,7 +24,8 @@ "background": "#21211F", "primary": "#D1B59A", "accent": "#4E4E4E" - } + }, + "isCustom": false }, { "name": "cab", @@ -30,7 +33,8 @@ "background": "#F6D305", "primary": "#1F1F1F", "accent": "#424242" - } + }, + "isCustom": false }, { "name": "cloud", @@ -38,7 +42,8 @@ "background": "#f1f2f0", "primary": "#35342f", "accent": "#37bbe4" - } + }, + "isCustom": false }, { "name": "lime", @@ -46,7 +51,8 @@ "background": "#263238", "primary": "#AABBC3", "accent": "#aeea00" - } + }, + "isCustom": false }, { "name": "white", @@ -54,7 +60,8 @@ "background": "#ffffff", "primary": "#222222", "accent": "#dddddd" - } + }, + "isCustom": false }, { "name": "tron", @@ -62,7 +69,8 @@ "background": "#242B33", "primary": "#EFFBFF", "accent": "#6EE2FF" - } + }, + "isCustom": false }, { "name": "blues", @@ -70,7 +78,8 @@ "background": "#2B2C56", "primary": "#EFF1FC", "accent": "#6677EB" - } + }, + "isCustom": false }, { "name": "passion", @@ -78,7 +87,8 @@ "background": "#f5f5f5", "primary": "#12005e", "accent": "#8e24aa" - } + }, + "isCustom": false }, { "name": "chalk", @@ -86,7 +96,8 @@ "background": "#263238", "primary": "#AABBC3", "accent": "#FF869A" - } + }, + "isCustom": false }, { "name": "paper", @@ -94,7 +105,8 @@ "background": "#F8F6F1", "primary": "#4C432E", "accent": "#AA9A73" - } + }, + "isCustom": false }, { "name": "neon", @@ -102,7 +114,8 @@ "background": "#091833", "primary": "#EFFBFF", "accent": "#ea00d9" - } + }, + "isCustom": false }, { "name": "pumpkin", @@ -110,7 +123,8 @@ "background": "#2d3436", "primary": "#EFFBFF", "accent": "#ffa500" - } + }, + "isCustom": false }, { "name": "onedark", @@ -118,7 +132,8 @@ "background": "#282c34", "primary": "#dfd9d6", "accent": "#98c379" - } + }, + "isCustom": false } ] } From b8af178cbfc63e2b91c40721d76ef61695fe4a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 24 Mar 2022 14:48:10 +0100 Subject: [PATCH 05/16] API routes to get and add themes --- api.js | 1 + controllers/queries/addQuery.js | 7 +++++++ controllers/themes/addTheme.js | 28 ++++++++++++++++++++++++++++ controllers/themes/index.js | 1 + routes/queries.js | 13 +++++++++++-- routes/themes.js | 19 +++++++++++++++++++ 6 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 controllers/themes/addTheme.js create mode 100644 routes/themes.js diff --git a/api.js b/api.js index 840529a5..45be3595 100644 --- a/api.js +++ b/api.js @@ -22,6 +22,7 @@ api.use('/api/categories', require('./routes/category')); api.use('/api/bookmarks', require('./routes/bookmark')); api.use('/api/queries', require('./routes/queries')); api.use('/api/auth', require('./routes/auth')); +api.use('/api/themes', require('./routes/themes')); // Custom error handler api.use(errorHandler); diff --git a/controllers/queries/addQuery.js b/controllers/queries/addQuery.js index cd61c67e..9db41a88 100644 --- a/controllers/queries/addQuery.js +++ b/controllers/queries/addQuery.js @@ -1,4 +1,5 @@ const asyncWrapper = require('../../middleware/asyncWrapper'); +const ErrorResponse = require('../../utils/ErrorResponse'); const File = require('../../utils/File'); // @desc Add custom search query @@ -8,6 +9,12 @@ const addQuery = asyncWrapper(async (req, res, next) => { const file = new File('data/customQueries.json'); let content = JSON.parse(file.read()); + const prefixes = content.queries.map((q) => q.prefix); + + if (prefixes.includes(req.body.prefix)) { + return next(new ErrorResponse('Prefix must be unique', 400)); + } + // Add new query content.queries.push(req.body); file.write(content, true); diff --git a/controllers/themes/addTheme.js b/controllers/themes/addTheme.js new file mode 100644 index 00000000..1d2cf0c3 --- /dev/null +++ b/controllers/themes/addTheme.js @@ -0,0 +1,28 @@ +const asyncWrapper = require('../../middleware/asyncWrapper'); +const ErrorResponse = require('../../utils/ErrorResponse'); +const File = require('../../utils/File'); + +// @desc Create new theme +// @route POST /api/themes +// @access Private +const addTheme = asyncWrapper(async (req, res, next) => { + const file = new File('data/themes.json'); + let content = JSON.parse(file.read()); + + const themeNames = content.themes.map((t) => t.name); + + if (themeNames.includes(req.body.name)) { + return next(new ErrorResponse('Name must be unique', 400)); + } + + // Add new theme + content.themes.push(req.body); + file.write(content, true); + + res.status(201).json({ + success: true, + data: req.body, + }); +}); + +module.exports = addTheme; diff --git a/controllers/themes/index.js b/controllers/themes/index.js index 84cc8e8e..7ec3b92a 100644 --- a/controllers/themes/index.js +++ b/controllers/themes/index.js @@ -1,3 +1,4 @@ module.exports = { getThemes: require('./getThemes'), + addTheme: require('./addTheme'), }; diff --git a/routes/queries.js b/routes/queries.js index 22626115..ec15790b 100644 --- a/routes/queries.js +++ b/routes/queries.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); // middleware -const { auth, requireAuth } = require('../middleware'); +const { auth, requireAuth, requireBody } = require('../middleware'); const { getQueries, @@ -11,7 +11,16 @@ const { updateQuery, } = require('../controllers/queries/'); -router.route('/').post(auth, requireAuth, addQuery).get(getQueries); +router + .route('/') + .post( + auth, + requireAuth, + requireBody(['name', 'prefix', 'template']), + addQuery + ) + .get(getQueries); + router .route('/:prefix') .delete(auth, requireAuth, deleteQuery) diff --git a/routes/themes.js b/routes/themes.js new file mode 100644 index 00000000..42d6b353 --- /dev/null +++ b/routes/themes.js @@ -0,0 +1,19 @@ +const express = require('express'); +const router = express.Router(); + +// middleware +const { auth, requireAuth, requireBody } = require('../middleware'); + +const { getThemes, addTheme } = require('../controllers/themes/'); + +router + .route('/') + .get(getThemes) + .post( + auth, + requireAuth, + requireBody(['name', 'colors', 'isCustom']), + addTheme + ); + +module.exports = router; From 378dd8e36df8e753682d647938c3eacce326b0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 24 Mar 2022 14:56:36 +0100 Subject: [PATCH 06/16] Fixed color of weather icon when changing theme --- .../UI/Icons/WeatherIcon/WeatherIcon.tsx | 6 ++-- client/src/store/action-creators/config.ts | 12 +++++-- client/src/store/action-creators/theme.ts | 35 +++++++++++++++++++ client/src/store/actions/theme.ts | 3 +- client/src/store/reducers/theme.ts | 28 +++++++++++++-- 5 files changed, 76 insertions(+), 8 deletions(-) diff --git a/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx b/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx index 2664b474..d28aff41 100644 --- a/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx +++ b/client/src/components/UI/Icons/WeatherIcon/WeatherIcon.tsx @@ -10,7 +10,7 @@ interface Props { } export const WeatherIcon = (props: Props): JSX.Element => { - const { theme } = useSelector((state: State) => state.theme); + const { activeTheme } = useSelector((state: State) => state.theme); const icon = props.isDay ? new IconMapping().mapIcon(props.weatherStatusCode, TimeOfDay.day) @@ -18,7 +18,7 @@ export const WeatherIcon = (props: Props): JSX.Element => { useEffect(() => { const delay = setTimeout(() => { - const skycons = new Skycons({ color: theme.colors.accent }); + const skycons = new Skycons({ color: activeTheme.colors.accent }); skycons.add(`weather-icon`, icon); skycons.play(); }, 1); @@ -26,7 +26,7 @@ export const WeatherIcon = (props: Props): JSX.Element => { return () => { clearTimeout(delay); }; - }, [props.weatherStatusCode, icon, theme.colors.accent]); + }, [props.weatherStatusCode, icon, activeTheme.colors.accent]); return ; }; diff --git a/client/src/store/action-creators/config.ts b/client/src/store/action-creators/config.ts index 6b516f74..d0198763 100644 --- a/client/src/store/action-creators/config.ts +++ b/client/src/store/action-creators/config.ts @@ -7,7 +7,7 @@ import { UpdateConfigAction, UpdateQueryAction, } from '../actions/config'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import { ApiResponse, Config, Query } from '../../interfaces'; import { ActionType } from '../action-types'; import { storeUIConfig, applyAuth } from '../../utility'; @@ -103,7 +103,15 @@ export const addQuery = payload: res.data.data, }); } catch (err) { - console.log(err); + const error = err as AxiosError<{ error: string }>; + + dispatch({ + type: ActionType.createNotification, + payload: { + title: 'Error', + message: error.response?.data.error, + }, + }); } }; diff --git a/client/src/store/action-creators/theme.ts b/client/src/store/action-creators/theme.ts index 87bf184b..2870b6cd 100644 --- a/client/src/store/action-creators/theme.ts +++ b/client/src/store/action-creators/theme.ts @@ -15,6 +15,11 @@ export const setTheme = for (const [key, value] of Object.entries(colors)) { document.body.style.setProperty(`--color-${key}`, value); } + + dispatch({ + type: ActionType.setTheme, + payload: colors, + }); }; export const fetchThemes = @@ -30,3 +35,33 @@ export const fetchThemes = console.log(err); } }; + +// export const addTheme = () => async (dispatch: Dispatch<>) => { +// try { +// // const res = await axios.post<>('/api/themes') +// } catch (err) {} +// }; + +// export const addQuery = +// (query: Query) => async (dispatch: Dispatch) => { +// try { +// const res = await axios.post>('/api/queries', query, { +// headers: applyAuth(), +// }); + +// dispatch({ +// type: ActionType.addQuery, +// payload: res.data.data, +// }); +// } catch (err) { +// const error = err as AxiosError<{ error: string }>; + +// dispatch({ +// type: ActionType.createNotification, +// payload: { +// title: 'Error', +// message: error.response?.data.error, +// }, +// }); +// } +// }; diff --git a/client/src/store/actions/theme.ts b/client/src/store/actions/theme.ts index 6d40d213..b3d3e9da 100644 --- a/client/src/store/actions/theme.ts +++ b/client/src/store/actions/theme.ts @@ -1,8 +1,9 @@ import { ActionType } from '../action-types'; -import { Theme } from '../../interfaces'; +import { Theme, ThemeColors } from '../../interfaces'; export interface SetThemeAction { type: ActionType.setTheme; + payload: ThemeColors; } export interface FetchThemesAction { diff --git a/client/src/store/reducers/theme.ts b/client/src/store/reducers/theme.ts index 7738a3bc..62ccf6b8 100644 --- a/client/src/store/reducers/theme.ts +++ b/client/src/store/reducers/theme.ts @@ -1,14 +1,28 @@ import { Action } from '../actions'; import { ActionType } from '../action-types'; -import { Theme } from '../../interfaces/Theme'; -import { arrayPartition } from '../../utility'; +import { Theme, ThemeColors } from '../../interfaces/Theme'; +import { arrayPartition, parsePABToTheme } from '../../utility'; interface ThemeState { + activeTheme: Theme; themes: Theme[]; userThemes: Theme[]; } +const savedTheme: ThemeColors = parsePABToTheme(localStorage.theme) || { + primary: '#effbff', + accent: '#6ee2ff', + background: '#242b33', +}; + const initialState: ThemeState = { + activeTheme: { + name: 'main', + isCustom: false, + colors: { + ...savedTheme, + }, + }, themes: [], userThemes: [], }; @@ -18,6 +32,16 @@ export const themeReducer = ( action: Action ): ThemeState => { switch (action.type) { + case ActionType.setTheme: { + return { + ...state, + activeTheme: { + ...state.activeTheme, + colors: action.payload, + }, + }; + } + case ActionType.fetchThemes: { const [themes, userThemes] = arrayPartition( action.payload, From 9ab6c65d85544758e7ad10347ca194a00b6e7dc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Thu, 24 Mar 2022 16:07:14 +0100 Subject: [PATCH 07/16] Added custom theme creator --- .../Themer/ThemeBuilder/ThemeBuilder.tsx | 58 ++++++++- .../ThemeBuilder/ThemeCreator.module.css | 6 + .../Themer/ThemeBuilder/ThemeCreator.tsx | 117 ++++++++++++++++++ .../Themer/ThemeBuilder/ThemeEditor.tsx | 13 ++ .../src/components/Settings/Themer/Themer.tsx | 4 +- .../UI/Forms/InputGroup/InputGroup.module.css | 4 + client/src/store/action-creators/theme.ts | 64 +++++----- client/src/store/action-types/index.ts | 1 + client/src/store/actions/theme.ts | 5 + client/src/store/reducers/theme.ts | 15 ++- 10 files changed, 246 insertions(+), 41 deletions(-) create mode 100644 client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.module.css create mode 100644 client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.tsx create mode 100644 client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx index 0762f13d..4ae28228 100644 --- a/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeBuilder.tsx @@ -1,21 +1,69 @@ +import { useState } from 'react'; + +// Redux +import { useSelector } from 'react-redux'; +import { State } from '../../../../store/reducers'; + +// Other import { Theme } from '../../../../interfaces'; -import { Button } from '../../../UI'; + +// UI +import { Button, Modal } from '../../../UI'; import { ThemeGrid } from '../ThemeGrid/ThemeGrid'; import classes from './ThemeBuilder.module.css'; +import { ThemeCreator } from './ThemeCreator'; +import { ThemeEditor } from './ThemeEditor'; interface Props { themes: Theme[]; } export const ThemeBuilder = ({ themes }: Props): JSX.Element => { + const { + auth: { isAuthenticated }, + } = useSelector((state: State) => state); + + const [showModal, toggleShowModal] = useState(false); + const [isInEdit, toggleIsInEdit] = useState(false); + return (
+ {/* MODALS */} + toggleShowModal(!showModal)}> + {isInEdit ? ( + toggleShowModal(!showModal)} /> + ) : ( + toggleShowModal(!showModal)} /> + )} + + + {/* USER THEMES */} -
- - {themes.length && } -
+ {/* BUTTONS */} + {isAuthenticated && ( +
+ + + {themes.length && ( + + )} +
+ )}
); }; diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.module.css b/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.module.css new file mode 100644 index 00000000..893095f8 --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.module.css @@ -0,0 +1,6 @@ +.ColorsContainer { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 10px; + margin-bottom: 20px; +} diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.tsx b/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.tsx new file mode 100644 index 00000000..0d696e67 --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeCreator.tsx @@ -0,0 +1,117 @@ +import { ChangeEvent, FormEvent, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { actionCreators } from '../../../../store'; +import { Theme } from '../../../../interfaces'; +import { Button, InputGroup, ModalForm } from '../../../UI'; + +import classes from './ThemeCreator.module.css'; + +interface Props { + modalHandler: () => void; +} + +export const ThemeCreator = ({ modalHandler }: Props): JSX.Element => { + const { addTheme } = bindActionCreators(actionCreators, useDispatch()); + + const [formData, setFormData] = useState({ + name: '', + isCustom: true, + colors: { + primary: '#ffffff', + accent: '#ffffff', + background: '#ffffff', + }, + }); + + const inputChangeHandler = (e: ChangeEvent) => { + const { name, value } = e.target; + + setFormData({ + ...formData, + [name]: value, + }); + }; + + const setColor = ({ + target: { value, name }, + }: ChangeEvent) => { + setFormData({ + ...formData, + colors: { + ...formData.colors, + [name]: value, + }, + }); + }; + + const formHandler = (e: FormEvent) => { + e.preventDefault(); + + // add new theme + addTheme(formData); + + // close modal + modalHandler(); + + // clear theme name + setFormData({ ...formData, name: '' }); + }; + + return ( + + + + inputChangeHandler(e)} + /> + + +
+ + + setColor(e)} + /> + + + + + setColor(e)} + /> + + + + + setColor(e)} + /> + +
+ + +
+ ); +}; diff --git a/client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx b/client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx new file mode 100644 index 00000000..b899049a --- /dev/null +++ b/client/src/components/Settings/Themer/ThemeBuilder/ThemeEditor.tsx @@ -0,0 +1,13 @@ +import { ModalForm } from '../../../UI'; + +interface Props { + modalHandler: () => void; +} + +export const ThemeEditor = (props: Props): JSX.Element => { + return ( + {}} modalHandler={props.modalHandler}> +

edit

+
+ ); +}; diff --git a/client/src/components/Settings/Themer/Themer.tsx b/client/src/components/Settings/Themer/Themer.tsx index 3bcaa2ed..5a7ff061 100644 --- a/client/src/components/Settings/Themer/Themer.tsx +++ b/client/src/components/Settings/Themer/Themer.tsx @@ -4,6 +4,7 @@ import { ChangeEvent, FormEvent, Fragment, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; import { actionCreators } from '../../../store'; +import { State } from '../../../store/reducers'; // Typescript import { Theme, ThemeSettingsForm } from '../../../interfaces'; @@ -14,7 +15,6 @@ import { ThemeBuilder } from './ThemeBuilder/ThemeBuilder'; import { ThemeGrid } from './ThemeGrid/ThemeGrid'; // Other -import { State } from '../../../store/reducers'; import { inputHandler, parseThemeToPAB, @@ -82,7 +82,7 @@ export const Themer = (): JSX.Element => { - + { }); }; + const customThemesEl = ( + + + + + ); + return ( {!themes.length ? : } - {!userThemes.length ? ( - isAuthenticated && 'auth and empty' - ) : ( - - - - - )} + {!userThemes.length ? isAuthenticated && customThemesEl : customThemesEl} {isAuthenticated && ( diff --git a/client/src/components/UI/Modal/Modal.tsx b/client/src/components/UI/Modal/Modal.tsx index 43fb5e98..3f2a6bda 100644 --- a/client/src/components/UI/Modal/Modal.tsx +++ b/client/src/components/UI/Modal/Modal.tsx @@ -6,24 +6,32 @@ interface Props { isOpen: boolean; setIsOpen: Function; children: ReactNode; + cb?: Function; } -export const Modal = (props: Props): JSX.Element => { +export const Modal = ({ + isOpen, + setIsOpen, + children, + cb, +}: Props): JSX.Element => { const modalRef = useRef(null); const modalClasses = [ classes.Modal, - props.isOpen ? classes.ModalOpen : classes.ModalClose, + isOpen ? classes.ModalOpen : classes.ModalClose, ].join(' '); const clickHandler = (e: MouseEvent) => { if (e.target === modalRef.current) { - props.setIsOpen(false); + setIsOpen(false); + + if (cb) cb(); } }; return (
- {props.children} + {children}
); }; diff --git a/utils/init/initialFiles.json b/utils/init/initialFiles.json index 43b68d53..83d370a7 100644 --- a/utils/init/initialFiles.json +++ b/utils/init/initialFiles.json @@ -174,6 +174,15 @@ "accent": "#98c379" }, "isCustom": false + }, + { + "name": "mint", + "colors": { + "background": "#282525", + "primary": "#d9d9d9", + "accent": "#50fbc2" + }, + "isCustom": false } ] }, diff --git a/utils/init/themes.json b/utils/init/themes.json index 685a81e0..867fe7e5 100644 --- a/utils/init/themes.json +++ b/utils/init/themes.json @@ -134,6 +134,15 @@ "accent": "#98c379" }, "isCustom": false + }, + { + "name": "mint", + "colors": { + "background": "#282525", + "primary": "#d9d9d9", + "accent": "#50fbc2" + }, + "isCustom": false } ] } From 16121ff5471c85eba80424c67df193f3e12f95a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Malak?= Date: Fri, 25 Mar 2022 14:28:40 +0100 Subject: [PATCH 14/16] Added option to set secondary search provider --- CHANGELOG.md | 1 + client/src/components/SearchBar/SearchBar.tsx | 27 ++++++++------ .../GeneralSettings/GeneralSettings.tsx | 30 ++++++++++++++- client/src/interfaces/Config.ts | 1 + client/src/interfaces/Forms.ts | 1 + client/src/interfaces/SearchResult.ts | 5 ++- client/src/utility/searchParser.ts | 37 +++++++++++++------ .../utility/templateObjects/configTemplate.ts | 1 + utils/init/initialConfig.json | 1 + 9 files changed, 79 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8b571dc..f4521eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### v2.3.0 (TBA) - Added custom theme editor ([#246](/~https://github.com/pawelmalak/flame/issues/246)) +- Added option to set secondary search provider ([#295](/~https://github.com/pawelmalak/flame/issues/295)) - Fixed bug where pressing Enter with empty search bar would redirect to search results ([#325](/~https://github.com/pawelmalak/flame/issues/325)) - Fixed bug where user could create empty app or bookmark which was causing page to go blank ([#332](/~https://github.com/pawelmalak/flame/issues/332)) - Added new theme: Mint diff --git a/client/src/components/SearchBar/SearchBar.tsx b/client/src/components/SearchBar/SearchBar.tsx index 2ec34204..f0b78a58 100644 --- a/client/src/components/SearchBar/SearchBar.tsx +++ b/client/src/components/SearchBar/SearchBar.tsx @@ -64,16 +64,22 @@ export const SearchBar = (props: Props): JSX.Element => { }; const searchHandler = (e: KeyboardEvent) => { - const { isLocal, search, query, isURL, sameTab, rawQuery } = searchParser( - inputRef.current.value - ); + const { + isLocal, + encodedURL, + primarySearch, + secondarySearch, + isURL, + sameTab, + rawQuery, + } = searchParser(inputRef.current.value); if (isLocal) { - setLocalSearch(search); + setLocalSearch(encodedURL); } if (e.code === 'Enter' || e.code === 'NumpadEnter') { - if (!query.prefix) { + if (!primarySearch.prefix) { // Prefix not found -> emit notification createNotification({ title: 'Error', @@ -91,21 +97,20 @@ export const SearchBar = (props: Props): JSX.Element => { redirectUrl(bookmarkSearchResult[0].bookmarks[0].url, sameTab); } else { // no local results -> search the internet with the default search provider if query is not empty - if (!/^ *$/.test(rawQuery)) { - let template = query.template; + let template = primarySearch.template; - if (query.prefix === 'l') { - template = 'https://duckduckgo.com/?q='; + if (primarySearch.prefix === 'l') { + template = secondarySearch.template; } - const url = `${template}${search}`; + const url = `${template}${encodedURL}`; redirectUrl(url, sameTab); } } } else { // Valid query -> redirect to search results - const url = `${query.template}${search}`; + const url = `${primarySearch.template}${encodedURL}`; redirectUrl(url, sameTab); } } else if (e.code === 'Escape') { diff --git a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx index d88208e5..61bc6c2a 100644 --- a/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx +++ b/client/src/components/Settings/GeneralSettings/GeneralSettings.tsx @@ -167,7 +167,7 @@ export const GeneralSettings = (): JSX.Element => { {/* === SEARCH OPTIONS === */} - + + {formData.defaultSearchProvider === 'l' && ( + + + + + Will be used when "Local search" is primary search provider and + there are not any local results + + + )} +