diff --git a/src/components/Form/Settings/Checkbox/Checkbox.jsx b/src/components/Form/Settings/Checkbox/Checkbox.jsx index 952f5b0d..22d65564 100644 --- a/src/components/Form/Settings/Checkbox/Checkbox.jsx +++ b/src/components/Form/Settings/Checkbox/Checkbox.jsx @@ -1,61 +1,53 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { Checkbox as CheckboxUI, FormControlLabel } from '@mui/material'; import EventBus from 'utils/eventbus'; -class Checkbox extends PureComponent { - constructor(props) { - super(props); - this.state = { - checked: localStorage.getItem(this.props.name) === 'true', - }; - } +const Checkbox = memo((props) => { + const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true'); - handleChange = () => { - const value = this.state.checked !== true; - localStorage.setItem(this.props.name, value); + const handleChange = useCallback(() => { + const value = !checked; + localStorage.setItem(props.name, value); + setChecked(value); - this.setState({ - checked: value, - }); - - if (this.props.onChange) { - this.props.onChange(value); + if (props.onChange) { + props.onChange(value); } variables.stats.postEvent( 'setting', - `${this.props.name} ${this.state.checked === true ? 'enabled' : 'disabled'}`, + `${props.name} ${checked ? 'enabled' : 'disabled'}`, ); - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [checked, props]); - render() { - return ( - - } - label={this.props.text} - /> - ); - } -} + return ( + + } + label={props.text} + /> + ); +}); + +Checkbox.displayName = 'Checkbox'; export { Checkbox as default, Checkbox }; diff --git a/src/components/Form/Settings/Dropdown/Dropdown.jsx b/src/components/Form/Settings/Dropdown/Dropdown.jsx index 87979bf9..ac4eebfb 100644 --- a/src/components/Form/Settings/Dropdown/Dropdown.jsx +++ b/src/components/Form/Settings/Dropdown/Dropdown.jsx @@ -1,78 +1,72 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; +import { memo, useState, useCallback, useRef } from 'react'; import { InputLabel, MenuItem, FormControl, Select } from '@mui/material'; import EventBus from 'utils/eventbus'; -class Dropdown extends PureComponent { - constructor(props) { - super(props); - this.state = { - value: localStorage.getItem(this.props.name) || this.props.items[0].value, - title: '', - }; - this.dropdown = createRef(); - } +const Dropdown = memo((props) => { + const [value, setValue] = useState( + localStorage.getItem(props.name) || props.items[0].value, + ); + const dropdown = useRef(); - onChange = (e) => { - const { value } = e.target; + const onChange = useCallback((e) => { + const newValue = e.target.value; - if (value === variables.getMessage('modals.main.loading')) { + if (newValue === variables.getMessage('modals.main.loading')) { return; } - variables.stats.postEvent('setting', `${this.props.name} from ${this.state.value} to ${value}`); + variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`); - this.setState({ - value, - }); + setValue(newValue); - if (!this.props.noSetting) { - localStorage.setItem(this.props.name, value); - localStorage.setItem(this.props.name2, this.props.value2); + if (!props.noSetting) { + localStorage.setItem(props.name, newValue); + localStorage.setItem(props.name2, props.value2); } - if (this.props.onChange) { - this.props.onChange(value); + if (props.onChange) { + props.onChange(newValue); } - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [value, props]); - render() { - const id = 'dropdown' + this.props.name; - const label = this.props.label || ''; + const id = 'dropdown' + props.name; + const label = props.label || ''; - return ( - - {label} - - - ); - } -} + return ( + + {label} + + + ); +}); + +Dropdown.displayName = 'Dropdown'; export { Dropdown as default, Dropdown }; diff --git a/src/components/Form/Settings/FileUpload/FileUpload.jsx b/src/components/Form/Settings/FileUpload/FileUpload.jsx index fa97a208..9ed27c2e 100644 --- a/src/components/Form/Settings/FileUpload/FileUpload.jsx +++ b/src/components/Form/Settings/FileUpload/FileUpload.jsx @@ -1,19 +1,22 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useEffect } from 'react'; import { toast } from 'react-toastify'; import { compressAccurately, filetoDataURL } from 'image-conversion'; import videoCheck from 'features/background/api/videoCheck'; -class FileUpload extends PureComponent { - componentDidMount() { - document.getElementById(this.props.id).onchange = (e) => { +const FileUpload = memo(({ id, type, accept, loadFunction }) => { + useEffect(() => { + const fileInput = document.getElementById(id); + if (!fileInput) return; + + const handleChange = (e) => { const reader = new FileReader(); const file = e.target.files[0]; - if (this.props.type === 'settings') { + if (type === 'settings') { reader.readAsText(file, 'UTF-8'); reader.onload = (e) => { - return this.props.loadFunction(e.target.result); + return loadFunction(e.target.result); }; } else { // background upload @@ -29,7 +32,7 @@ class FileUpload extends PureComponent { return toast(variables.getMessage('toasts.no_storage')); } - return this.props.loadFunction(file); + return loadFunction(file); } compressAccurately(file, { @@ -40,7 +43,7 @@ class FileUpload extends PureComponent { return toast(variables.getMessage('toasts.no_storage')); } - this.props.loadFunction({ + loadFunction({ target: { result: await filetoDataURL(res), }, @@ -48,18 +51,26 @@ class FileUpload extends PureComponent { }); } }; - } - render() { - return ( - - ); - } -} + fileInput.onchange = handleChange; + + return () => { + if (fileInput) { + fileInput.onchange = null; + } + }; + }, [id, type, loadFunction]); + + return ( + + ); +}); + +FileUpload.displayName = 'FileUpload'; export { FileUpload as default, FileUpload }; diff --git a/src/components/Form/Settings/Radio/Radio.jsx b/src/components/Form/Settings/Radio/Radio.jsx index 791e193e..f3f5697d 100644 --- a/src/components/Form/Settings/Radio/Radio.jsx +++ b/src/components/Form/Settings/Radio/Radio.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { Radio as RadioUI, RadioGroup, @@ -11,77 +11,69 @@ import { import EventBus from 'utils/eventbus'; import { translations } from 'lib/translations'; -class Radio extends PureComponent { - constructor(props) { - super(props); - this.state = { - value: localStorage.getItem(this.props.name), - }; - } +const Radio = memo((props) => { + const [value, setValue] = useState(localStorage.getItem(props.name)); - handleChange = async (e) => { - const { value } = e.target; + const handleChange = useCallback(async (e) => { + const newValue = e.target.value; - if (value === 'loading') { + if (newValue === 'loading') { return; } - if (this.props.name === 'language') { + if (props.name === 'language') { // old tab name if (localStorage.getItem('tabName') === variables.getMessage('tabname')) { - localStorage.setItem('tabName', translations[value.replace('-', '_')].tabname); + localStorage.setItem('tabName', translations[newValue.replace('-', '_')].tabname); } } - localStorage.setItem(this.props.name, value); + localStorage.setItem(props.name, newValue); + setValue(newValue); - this.setState({ - value, - }); - - if (this.props.onChange) { - this.props.onChange(value); + if (props.onChange) { + props.onChange(newValue); } - variables.stats.postEvent('setting', `${this.props.name} from ${this.state.value} to ${value}`); + variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`); - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [value, props]); - render() { - return ( - - - {this.props.title} - - - {this.props.options.map((option) => ( - } - label={option.name} - key={option.name} - /> - ))} - - - ); - } -} + return ( + + + {props.title} + + + {props.options.map((option) => ( + } + label={option.name} + key={option.name} + /> + ))} + + + ); +}); + +Radio.displayName = 'Radio'; export { Radio as default, Radio }; diff --git a/src/components/Form/Settings/Slider/Slider.jsx b/src/components/Form/Settings/Slider/Slider.jsx index 764f6679..4c88badb 100644 --- a/src/components/Form/Settings/Slider/Slider.jsx +++ b/src/components/Form/Settings/Slider/Slider.jsx @@ -1,88 +1,80 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { toast } from 'react-toastify'; import { Slider } from '@mui/material'; import { MdRefresh } from 'react-icons/md'; import EventBus from 'utils/eventbus'; -class SliderComponent extends PureComponent { - constructor(props) { - super(props); - this.state = { - value: localStorage.getItem(this.props.name) || this.props.default, - }; - } +const SliderComponent = memo((props) => { + const [value, setValue] = useState(localStorage.getItem(props.name) || props.default); - handleChange = (e, text) => { - let { value } = e.target; - value = Number(value); + const handleChange = useCallback((e, text) => { + let newValue = e.target.value; + newValue = Number(newValue); if (text) { - if (value === '') { - return this.setState({ - value: 0, - }); + if (newValue === '') { + setValue(0); + return; } - if (value > this.props.max) { - value = this.props.max; + if (newValue > props.max) { + newValue = props.max; } - if (value < this.props.min) { - value = this.props.min; + if (newValue < props.min) { + newValue = props.min; } } - localStorage.setItem(this.props.name, value); - this.setState({ - value, - }); + localStorage.setItem(props.name, newValue); + setValue(newValue); - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [props]); - resetItem = () => { - this.handleChange({ + const resetItem = useCallback(() => { + handleChange({ target: { - value: this.props.default || '', + value: props.default || '', }, }); toast(variables.getMessage('toasts.reset')); - }; + }, [handleChange, props.default]); - render() { - return ( - <> - - {this.props.title} - {Number(this.state.value)} - - - {variables.getMessage('modals.main.settings.buttons.reset')} - + return ( + <> + + {props.title} + {Number(value)} + + + {variables.getMessage('modals.main.settings.buttons.reset')} - `${value}`} - marks={this.props.marks || []} - /> - - ); - } -} + + `${value}`} + marks={props.marks || []} + /> + + ); +}); + +SliderComponent.displayName = 'SliderComponent'; export { SliderComponent as default, SliderComponent as Slider }; diff --git a/src/components/Form/Settings/Switch/Switch.jsx b/src/components/Form/Settings/Switch/Switch.jsx index 006388d3..841ca65e 100644 --- a/src/components/Form/Settings/Switch/Switch.jsx +++ b/src/components/Form/Settings/Switch/Switch.jsx @@ -1,60 +1,52 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { Switch as SwitchUI, FormControlLabel } from '@mui/material'; import EventBus from 'utils/eventbus'; -class Switch extends PureComponent { - constructor(props) { - super(props); - this.state = { - checked: localStorage.getItem(this.props.name) === 'true', - }; - } +const Switch = memo((props) => { + const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true'); - handleChange = () => { - const value = this.state.checked !== true; - localStorage.setItem(this.props.name, value); + const handleChange = useCallback(() => { + const value = !checked; + localStorage.setItem(props.name, value); + setChecked(value); - this.setState({ - checked: value, - }); - - if (this.props.onChange) { - this.props.onChange(value); + if (props.onChange) { + props.onChange(value); } variables.stats.postEvent( 'setting', - `${this.props.name} ${this.state.checked === true ? 'enabled' : 'disabled'}`, + `${props.name} ${checked ? 'enabled' : 'disabled'}`, ); - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [checked, props]); - render() { - return ( - - } - label={this.props.header ? '' : this.props.text} - labelPlacement="start" - /> - ); - } -} + return ( + + } + label={props.header ? '' : props.text} + labelPlacement="start" + /> + ); +}); + +Switch.displayName = 'Switch'; export { Switch as default, Switch }; diff --git a/src/components/Form/Settings/Text/Text.jsx b/src/components/Form/Settings/Text/Text.jsx index 38ba1226..e1213a0b 100644 --- a/src/components/Form/Settings/Text/Text.jsx +++ b/src/components/Form/Settings/Text/Text.jsx @@ -1,84 +1,77 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { toast } from 'react-toastify'; import { TextField } from '@mui/material'; import { MdRefresh } from 'react-icons/md'; import EventBus from 'utils/eventbus'; -class Text extends PureComponent { - constructor(props) { - super(props); - this.state = { - value: localStorage.getItem(this.props.name) || '', - }; - } +const Text = memo((props) => { + const [value, setValue] = useState(localStorage.getItem(props.name) || ''); - handleChange = (e) => { + const handleChange = useCallback((e) => { let { value } = e.target; // Alex wanted font to work with montserrat and Montserrat, so I made it work - if (this.props.upperCaseFirst === true) { + if (props.upperCaseFirst === true) { value = value.charAt(0).toUpperCase() + value.slice(1); } - localStorage.setItem(this.props.name, value); - this.setState({ - value, - }); + localStorage.setItem(props.name, value); + setValue(value); - if (this.props.element) { - if (!document.querySelector(this.props.element)) { + if (props.element) { + if (!document.querySelector(props.element)) { document.querySelector('.reminder-info').style.display = 'flex'; return localStorage.setItem('showReminder', true); } } - EventBus.emit('refresh', this.props.category); - }; + EventBus.emit('refresh', props.category); + }, [props.name, props.upperCaseFirst, props.element, props.category]); - resetItem = () => { - this.handleChange({ + const resetItem = useCallback(() => { + handleChange({ target: { - value: this.props.default || '', + value: props.default || '', }, }); toast(variables.getMessage('toasts.reset')); - }; + }, [handleChange, props.default]); - render() { - return ( - <> - {this.props.textarea === true ? ( - - ) : ( - - )} - - - {variables.getMessage('modals.main.settings.buttons.reset')} - - - ); - } -} + return ( + <> + {props.textarea === true ? ( + + ) : ( + + )} + + + {variables.getMessage('modals.main.settings.buttons.reset')} + + + ); +}); + +Text.displayName = 'Text'; export { Text as default, Text }; diff --git a/src/features/background/components/Favourite.jsx b/src/features/background/components/Favourite.jsx index 2f677705..f8c43d95 100644 --- a/src/features/background/components/Favourite.jsx +++ b/src/features/background/components/Favourite.jsx @@ -1,27 +1,20 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback, useEffect, useRef } from 'react'; import { MdStar, MdStarBorder } from 'react-icons/md'; -class Favourite extends PureComponent { - buttons = { - favourited: this.favourite()} className="topicons" />, - unfavourited: this.favourite()} className="topicons" />, +const Favourite = memo(({ tooltipText, credit, offline, pun }) => { + const getInitialButton = () => { + return localStorage.getItem('favourite') ? 'favourited' : 'unfavourited'; }; - constructor() { - super(); - this.state = { - favourited: localStorage.getItem('favourite') - ? this.buttons.favourited - : this.buttons.unfavourited, - }; - } + const [favourited, setFavourited] = useState(getInitialButton()); + const previousFavouritedRef = useRef(favourited); - async favourite() { + const favourite = useCallback(async () => { if (localStorage.getItem('favourite')) { localStorage.removeItem('favourite'); - this.setState({ favourited: this.buttons.unfavourited }); - this.props.tooltipText(variables.getMessage('widgets.quote.favourite')); + setFavourited('unfavourited'); + tooltipText(variables.getMessage('widgets.quote.favourite')); variables.stats.postEvent('feature', 'Background favourite'); } else { const type = localStorage.getItem('backgroundType'); @@ -71,12 +64,12 @@ class Favourite extends PureComponent { JSON.stringify({ type, url, - credit: this.props.credit || '', + credit: credit || '', location: location?.innerText, camera: camera?.innerText, resolution: document.getElementById('infoResolution').textContent || '', - offline: this.props.offline, - pun: this.props.pun, + offline, + pun, }), ); } @@ -84,39 +77,47 @@ class Favourite extends PureComponent { } } - this.setState({ favourited: this.buttons.favourited }); - this.props.tooltipText(variables.getMessage('widgets.quote.unfavourite')); + setFavourited('favourited'); + tooltipText(variables.getMessage('widgets.quote.unfavourite')); variables.stats.postEvent('feature', 'Background unfavourite'); } - } + }, [tooltipText, credit, offline, pun]); - componentDidMount() { - this.updateTooltip(); - } - - componentDidUpdate(prevProps, prevState) { - if (prevState.favourited !== this.state.favourited) { - this.updateTooltip(); - } - } - - updateTooltip() { - if (this.props.tooltipText) { - this.props.tooltipText( + const updateTooltip = useCallback(() => { + if (tooltipText) { + tooltipText( localStorage.getItem('favourite') ? variables.getMessage('widgets.quote.unfavourite') : variables.getMessage('widgets.quote.favourite'), ); } - } + }, [tooltipText]); - render() { - if (localStorage.getItem('backgroundType') === 'colour') { - return null; + // componentDidMount + useEffect(() => { + updateTooltip(); + }, [updateTooltip]); + + // componentDidUpdate - only when favourited changes + useEffect(() => { + if (previousFavouritedRef.current !== favourited) { + updateTooltip(); + previousFavouritedRef.current = favourited; } + }, [favourited, updateTooltip]); - return this.state.favourited; + if (localStorage.getItem('backgroundType') === 'colour') { + return null; } -} + + const buttons = { + favourited: , + unfavourited: , + }; + + return buttons[favourited]; +}); + +Favourite.displayName = 'Favourite'; export default Favourite; diff --git a/src/features/background/options/BackgroundOptions.jsx b/src/features/background/options/BackgroundOptions.jsx index 6692f0ec..2c1a66c9 100644 --- a/src/features/background/options/BackgroundOptions.jsx +++ b/src/features/background/options/BackgroundOptions.jsx @@ -1,440 +1,229 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; -import { MdSource, MdOutlineKeyboardArrowRight, MdOutlineAutoAwesome } from 'react-icons/md'; +import { memo, useState, useEffect, useCallback, useRef } from 'react'; +import { MdSource, MdOutlineAutoAwesome } from 'react-icons/md'; import { Header } from 'components/Layout/Settings'; -import { Checkbox, Dropdown, Slider, Radio, Text, ChipSelect } from 'components/Form/Settings'; -import { Row, Content, Action } from 'components/Layout/Settings/Item'; -//import Text from 'components/Form/Settings/Text/Text'; +import { Dropdown } from 'components/Form/Settings'; import ColourSettings from './Colour'; import CustomSettings from './Custom'; +import APISettings from './sections/APISettings'; +import DisplaySettings from './sections/DisplaySettings'; +import EffectsSettings from './sections/EffectsSettings'; +import SourceSection from './sections/SourceSection'; +import NavigationCard from './sections/NavigationCard'; -import values from 'utils/data/slider_values.json'; -import { APIQualityOptions, backgroundImageEffects, getBackgroundOptionItems } from './optionTypes'; +import { getBackgroundOptionItems } from './optionTypes'; -class BackgroundOptions extends PureComponent { - constructor() { - super(); - this.state = { - backgroundType: localStorage.getItem('backgroundType') || 'api', - backgroundFilter: localStorage.getItem('backgroundFilter') || 'none', - backgroundCategories: [variables.getMessage('modals.main.loading')], - backgroundAPI: localStorage.getItem('backgroundAPI') || 'mue', - marketplaceEnabled: localStorage.getItem('photo_packs'), - effects: false, - backgroundSettingsSection: false, - }; - this.controller = new AbortController(); - } +const BackgroundOptions = memo(() => { + const [backgroundType, setBackgroundType] = useState( + localStorage.getItem('backgroundType') || 'api', + ); + const [backgroundFilter, setBackgroundFilter] = useState( + localStorage.getItem('backgroundFilter') || 'none', + ); + const [backgroundCategories, setBackgroundCategories] = useState([ + variables.getMessage('modals.main.loading'), + ]); + const [backgroundCategoriesOG, setBackgroundCategoriesOG] = useState([]); + const [backgroundAPI, setBackgroundAPI] = useState(localStorage.getItem('backgroundAPI') || 'mue'); + const [marketplaceEnabled] = useState(localStorage.getItem('photo_packs')); + const [effects, setEffects] = useState(false); + const [backgroundSettingsSection, setBackgroundSettingsSection] = useState(false); + + const controllerRef = useRef(null); - async getBackgroundCategories() { + const getBackgroundCategories = useCallback(async () => { const data = await ( await fetch(variables.constants.API_URL + '/images/categories', { - signal: this.controller.signal, + signal: controllerRef.current.signal, }) ).json(); - if (this.controller.signal.aborted === true) { + if (controllerRef.current.signal.aborted === true) { return; } - if (this.state.backgroundAPI !== 'mue') { + if (backgroundAPI !== 'mue') { // remove counts from unsplash categories data.forEach((category) => { delete category.count; }); } - this.setState({ - backgroundCategories: data, - backgroundCategoriesOG: data, - }); - } + setBackgroundCategories(data); + setBackgroundCategoriesOG(data); + }, [backgroundAPI]); - updateAPI(e) { + const updateAPI = useCallback((e) => { localStorage.setItem('nextImage', null); if (e === 'mue') { - this.setState({ - backgroundCategories: this.state.backgroundCategoriesOG, - backgroundAPI: 'mue', - }); + setBackgroundCategories(backgroundCategoriesOG); + setBackgroundAPI('mue'); } else { - const data = this.state.backgroundCategories; + const data = [...backgroundCategories]; data.forEach((category) => { delete category.count; }); - this.setState({ - backgroundAPI: 'unsplash', - backgroundCategories: data, - }); + setBackgroundAPI('unsplash'); + setBackgroundCategories(data); } - } + }, [backgroundCategories, backgroundCategoriesOG]); + + useEffect(() => { + controllerRef.current = new AbortController(); - componentDidMount() { if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') { - return this.setState({ - backgroundCategories: [variables.getMessage('modals.update.offline.title')], - }); + setBackgroundCategories([variables.getMessage('modals.update.offline.title')]); + return; } - this.getBackgroundCategories(); - } + getBackgroundCategories(); - componentWillUnmount() { - // stop making requests - this.controller.abort(); - } + return () => { + // stop making requests + controllerRef.current.abort(); + }; + }, [getBackgroundCategories]); - render() { - const APISettings = ( - <> - - - - {this.state.backgroundCategories[0] === variables.getMessage('modals.main.loading') ? ( - <> - - - ) : ( - - )} - - this.updateAPI(e)} - /> - - - {this.state.backgroundAPI === 'unsplash' && ( - - - - - - - )} - - ); - - let backgroundSettings = APISettings; - switch (this.state.backgroundType) { + const getBackgroundSettings = () => { + switch (backgroundType) { case 'custom': - backgroundSettings = ; - break; + return ; case 'colour': - backgroundSettings = ; - break; + return ; case 'random_colour': case 'random_gradient': - backgroundSettings = <>; - break; + return null; default: break; } if ( localStorage.getItem('photo_packs') && - this.state.backgroundType !== 'custom' && - this.state.backgroundType !== 'colour' && - this.state.backgroundType !== 'api' + backgroundType !== 'custom' && + backgroundType !== 'colour' && + backgroundType !== 'api' ) { - backgroundSettings = null; + return null; } - const usingImage = - this.state.backgroundType !== 'colour' && - this.state.backgroundType !== 'random_colour' && - this.state.backgroundType !== 'random_gradient'; + return ( + + ); + }; - let header; - if (this.state.effects === true) { - header = ( + const usingImage = + backgroundType !== 'colour' && + backgroundType !== 'random_colour' && + backgroundType !== 'random_gradient'; + + const showEffects = backgroundType === 'api' || backgroundType === 'custom' || marketplaceEnabled; + + const getHeader = () => { + if (effects) { + return (
this.setState({ effects: false })} + goBack={() => setEffects(false)} /> ); - } else if (this.state.backgroundSettingsSection === true) { - header = ( + } + + if (backgroundSettingsSection) { + return (
this.setState({ backgroundSettingsSection: false })} - /> - ); - } else { - header = ( -
setBackgroundSettingsSection(false)} /> ); } - + return ( - <> - {header} - {this.state.backgroundSettingsSection !== true && this.state.effects !== true ? ( - <> -
this.setState({ backgroundSettingsSection: true })} - > -
- -
- - {variables.getMessage('modals.main.settings.sections.background.source.title')} - - - {variables.getMessage( - 'modals.main.settings.sections.background.source.subtitle', - )} - -
-
-
- this.setState({ backgroundType: value })} - category="background" - items={getBackgroundOptionItems(this.state.marketplaceEnabled)} - /> -
-
- {this.state.backgroundType === 'api' || - this.state.backgroundType === 'custom' || - this.state.marketplaceEnabled ? ( - <> -
this.setState({ effects: true })}> -
- -
- - {variables.getMessage( - 'modals.main.settings.sections.background.effects.title', - )} - - - {variables.getMessage( - 'modals.main.settings.sections.background.effects.subtitle', - )} - -
-
-
- {' '} - -
-
- - ) : null} - - ) : null} - {this.state.backgroundSettingsSection !== true && - this.state.effects !== true && - (this.state.backgroundType === 'api' || - this.state.backgroundType === 'custom' || - this.state.marketplaceEnabled) ? ( - - + ); + }; + + return ( + <> + {getHeader()} + + {!backgroundSettingsSection && !effects && ( + <> + setBackgroundSettingsSection(true)} + action={ + setBackgroundType(value)} + category="background" + items={getBackgroundOptionItems(marketplaceEnabled)} + /> + } + /> + + {showEffects && ( + - - - - - - - ) : null} - {this.state.backgroundSettingsSection && ( - <> - - - - this.setState({ backgroundType: value })} - category="background" - items={getBackgroundOptionItems(this.state.marketplaceEnabled)} - /> - - - {/* todo: ideally refactor all of this file, but we need interval to appear on marketplace too */} - {backgroundSettings} - - )} - {(this.state.backgroundType === 'api' || - this.state.backgroundType === 'custom' || - this.state.marketplaceEnabled) && - this.state.effects ? ( - - setEffects(true)} /> - - - - this.setState({ backgroundFilter: value })} - category="backgroundeffect" - element="#backgroundImage" - items={backgroundImageEffects} - /> - {this.state.backgroundFilter !== 'none' && ( - - )} - - - ) : null} - - ); - } -} + )} + + )} + + {!backgroundSettingsSection && !effects && showEffects && ( + + )} + + {backgroundSettingsSection && ( + <> + setBackgroundType(value)} + /> + {getBackgroundSettings()} + + )} + + {showEffects && effects && ( + setBackgroundFilter(value)} + /> + )} + + ); +}); + +BackgroundOptions.displayName = 'BackgroundOptions'; export { BackgroundOptions as default, BackgroundOptions }; diff --git a/src/features/background/options/Custom.jsx b/src/features/background/options/Custom.jsx index f0c66266..ee993fd2 100644 --- a/src/features/background/options/Custom.jsx +++ b/src/features/background/options/Custom.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; +import { memo, useRef, useState, useEffect, useCallback } from 'react'; import { toast } from 'react-toastify'; import { MdCancel, @@ -19,68 +19,245 @@ import Modal from 'react-modal'; import CustomURLModal from './CustomURLModal'; -export default class CustomSettings extends PureComponent { - getMessage = (text, obj) => variables.getMessage(text, obj || {}); - - constructor() { - super(); - this.state = { - customBackground: this.getCustom(), - customURLModal: false, - urlError: '', - }; - this.customDnd = createRef(null); +const getCustom = () => { + let data; + try { + data = JSON.parse(localStorage.getItem('customBackground')); + } catch (e) { + data = [localStorage.getItem('customBackground')]; } + return data; +}; - resetCustom = () => { +const CustomSettings = memo(() => { + const [customBackground, setCustomBackground] = useState(getCustom()); + const [customURLModal, setCustomURLModal] = useState(false); + const [urlError, setUrlError] = useState(''); + const [currentBackgroundIndex, setCurrentBackgroundIndex] = useState(0); + const customDnd = useRef(null); + + const resetCustom = useCallback(() => { localStorage.setItem('customBackground', '[]'); - this.setState({ - customBackground: [], - }); + setCustomBackground([]); toast(variables.getMessage('toasts.reset')); EventBus.emit('refresh', 'background'); - }; + }, []); - customBackground(e, index) { + const handleCustomBackground = useCallback((e, index) => { const result = e.target.result; - const customBackground = this.state.customBackground; - customBackground[index || customBackground.length] = result; - - this.setState({ - customBackground, + setCustomBackground((prev) => { + const updated = [...prev]; + updated[index || updated.length] = result; + localStorage.setItem('customBackground', JSON.stringify(updated)); + document.querySelector('.reminder-info').style.display = 'flex'; + localStorage.setItem('showReminder', true); + return updated; }); + }, []); - this.forceUpdate(); + const modifyCustomBackground = useCallback((type, index) => { + setCustomBackground((prev) => { + const updated = [...prev]; + if (type === 'add') { + updated.push(''); + } else { + updated.splice(index, 1); + } + localStorage.setItem('customBackground', JSON.stringify(updated)); + document.querySelector('.reminder-info').style.display = 'flex'; + localStorage.setItem('showReminder', true); + return updated; + }); + }, []); - localStorage.setItem('customBackground', JSON.stringify(customBackground)); - document.querySelector('.reminder-info').style.display = 'flex'; - localStorage.setItem('showReminder', true); - } + const uploadCustomBackground = useCallback(() => { + const newIndex = customBackground.length; + document.getElementById('bg-input').setAttribute('index', newIndex); + document.getElementById('bg-input').click(); + setCurrentBackgroundIndex(newIndex); + }, [customBackground.length]); - modifyCustomBackground(type, index) { - const customBackground = this.state.customBackground; - if (type === 'add') { - customBackground.push(''); - } else { - customBackground.splice(index, 1); + const addCustomURL = useCallback((e) => { + // regex: https://ihateregex.io/expr/url/ + const urlRegex = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/; + if (urlRegex.test(e) === false) { + return setUrlError(variables.getMessage('widgets.quicklinks.url_error')); } - this.setState({ - customBackground, - }); - this.forceUpdate(); + const newIndex = customBackground.length; + setCustomURLModal(false); + setCurrentBackgroundIndex(newIndex); + handleCustomBackground({ target: { result: e } }, newIndex); + }, [customBackground.length, handleCustomBackground]); - localStorage.setItem('customBackground', JSON.stringify(customBackground)); - document.querySelector('.reminder-info').style.display = 'flex'; - localStorage.setItem('showReminder', true); - } + useEffect(() => { + const dnd = customDnd.current; + if (!dnd) return; - videoCustomSettings = () => { - const hasVideo = this.state.customBackground.filter((bg) => videoCheck(bg)); + const handleDragOver = (e) => { + e.preventDefault(); + }; - if (hasVideo.length > 0) { - return ( + const handleDrop = (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + const settings = {}; + + Object.keys(localStorage).forEach((key) => { + settings[key] = localStorage.getItem(key); + }); + + const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length; + + if (videoCheck(file.type) === true) { + if (settingsSize + file.size > 4850000) { + return toast(variables.getMessage('toasts.no_storage')); + } + + const reader = new FileReader(); + reader.onloadend = () => { + handleCustomBackground({ target: { result: reader.result } }, currentBackgroundIndex); + }; + reader.readAsDataURL(file); + return; + } + + compressAccurately(file, { + size: 450, + accuracy: 0.9, + }).then(async (res) => { + if (settingsSize + res.size > 4850000) { + return toast(variables.getMessage('toasts.no_storage')); + } + + handleCustomBackground( + { + target: { + result: await filetoDataURL(res), + }, + }, + currentBackgroundIndex, + ); + }); + }; + + dnd.ondragover = handleDragOver; + dnd.ondragenter = handleDragOver; + dnd.ondrop = handleDrop; + + return () => { + if (dnd) { + dnd.ondragover = null; + dnd.ondragenter = null; + dnd.ondrop = null; + } + }; + }, [currentBackgroundIndex, handleCustomBackground]); + + const hasVideo = customBackground.filter((bg) => videoCheck(bg)).length > 0; + + return ( + <> +
+
+
+ +
+ + {variables.getMessage( + 'modals.main.settings.sections.background.source.custom_title', + )} + + + {variables.getMessage( + 'modals.main.settings.sections.background.source.custom_description', + )} + +
+
+
+
+
+
+ {customBackground.length > 0 ? ( +
+ {customBackground.map((url, index) => ( +
+ {'Custom + {videoCheck(url) && } + {customBackground.length > 0 && ( + +
+ ))} +
+ ) : ( +
+
+ + + {variables.getMessage( + 'modals.main.settings.sections.background.source.drop_to_upload', + )} + + + {variables.getMessage( + 'modals.main.settings.sections.background.source.formats', + { + list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg', + }, + )} + +
+
+ )} +
+
+ handleCustomBackground(e, currentBackgroundIndex)} + /> + {hasVideo && ( <> - ); - } else { - return null; - } - }; - - getCustom() { - let data; - try { - data = JSON.parse(localStorage.getItem('customBackground')); - } catch (e) { - data = [localStorage.getItem('customBackground')]; - } - - return data; - } - - uploadCustomBackground() { - document.getElementById('bg-input').setAttribute('index', this.state.customBackground.length); - document.getElementById('bg-input').click(); - // to fix loadFunction - this.setState({ - currentBackgroundIndex: this.state.customBackground.length, - }); - } - - addCustomURL(e) { - // regex: https://ihateregex.io/expr/url/ - - const urlRegex = - /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b([-a-zA-Z0-9()!@:%_.~#?&=]*)/; - if (urlRegex.test(e) === false) { - return this.setState({ - urlError: variables.getMessage('widgets.quicklinks.url_error'), - }); - } - - this.setState({ - customURLModal: false, - currentBackgroundIndex: this.state.customBackground.length, - }); - this.customBackground({ target: { value: e } }, true, this.state.customBackground.length); - } - - componentDidMount() { - const dnd = this.customDnd.current; - dnd.ondragover = dnd.ondragenter = (e) => { - e.preventDefault(); - }; - - // todo: make this get from FileUpload.jsx to prevent duplication - dnd.ondrop = (e) => { - e.preventDefault(); - const file = e.dataTransfer.files[0]; - const settings = {}; - - Object.keys(localStorage).forEach((key) => { - settings[key] = localStorage.getItem(key); - }); - - const settingsSize = new TextEncoder().encode(JSON.stringify(settings)).length; - if (videoCheck(file.type) === true) { - if (settingsSize + file.size > 4850000) { - return toast(variables.getMessage('toasts.no_storage')); - } - - return this.customBackground(file, this.state.currentBackgroundIndex); - } - - compressAccurately(file, { - size: 450, - accuracy: 0.9, - }).then(async (res) => { - if (settingsSize + res.size > 4850000) { - return toast(variables.getMessage('toasts.no_storage')); - } - - this.customBackground( - { - target: { - result: await filetoDataURL(res), - }, - }, - this.state.currentBackgroundIndex, - ); - }); - e.preventDefault(); - }; - } - - render() { - return ( - <> -
-
-
- -
- - {variables.getMessage( - 'modals.main.settings.sections.background.source.custom_title', - )} - - - {variables.getMessage( - 'modals.main.settings.sections.background.source.custom_description', - )} - -
-
-
-
-
-
- {this.state.customBackground.length > 0 ? ( -
- {this.state.customBackground.map((url, index) => ( -
- {'Custom - {videoCheck(url) && } - {this.state.customBackground.length > 0 && ( - -
- ))} -
- ) : ( -
-
- - - {variables.getMessage( - 'modals.main.settings.sections.background.source.drop_to_upload', - )} - - - {variables.getMessage( - 'modals.main.settings.sections.background.source.formats', - { - list: 'jpeg, png, webp, webm, gif, mp4, webm, ogg', - }, - )} - -
-
- )} -
-
- this.customBackground(e, this.state.currentBackgroundIndex)} + )} + setCustomURLModal(false)} + isOpen={customURLModal} + className="Modal resetmodal mainModal" + overlayClassName="Overlay resetoverlay" + ariaHideApp={false} + > + setCustomURLModal(false)} /> - {this.videoCustomSettings()} - this.setState({ customURLModal: false })} - isOpen={this.state.customURLModal} - className="Modal resetmodal mainModal" - overlayClassName="Overlay resetoverlay" - ariaHideApp={false} - > - this.addCustomURL(e)} - urlError={this.state.urlError} - modalCloseOnly={() => this.setState({ customURLModal: false })} - /> - - - ); - } -} + + + ); +}); + +CustomSettings.displayName = 'CustomSettings'; + +export default CustomSettings; diff --git a/src/features/background/options/sections/APISettings.jsx b/src/features/background/options/sections/APISettings.jsx new file mode 100644 index 00000000..8da89fe1 --- /dev/null +++ b/src/features/background/options/sections/APISettings.jsx @@ -0,0 +1,92 @@ +import variables from 'config/variables'; +import { Dropdown, Radio, Text, ChipSelect } from 'components/Form/Settings'; +import { Row, Content, Action } from 'components/Layout/Settings/Item'; +import { APIQualityOptions } from '../optionTypes'; + +const APISettings = ({ backgroundAPI, backgroundCategories, onUpdateAPI }) => { + return ( + <> + + + + {backgroundCategories[0] === variables.getMessage('modals.main.loading') ? ( + <> + + + ) : ( + + )} + + + + + {backgroundAPI === 'unsplash' && ( + + + + + + + )} + + ); +}; + +export default APISettings; diff --git a/src/features/background/options/sections/DisplaySettings.jsx b/src/features/background/options/sections/DisplaySettings.jsx new file mode 100644 index 00000000..a37da0cd --- /dev/null +++ b/src/features/background/options/sections/DisplaySettings.jsx @@ -0,0 +1,37 @@ +import variables from 'config/variables'; +import { Checkbox } from 'components/Form/Settings'; +import { Row, Content, Action } from 'components/Layout/Settings/Item'; + +const DisplaySettings = ({ usingImage }) => { + return ( + + + + + + + + + ); +}; + +export default DisplaySettings; diff --git a/src/features/background/options/sections/EffectsSettings.jsx b/src/features/background/options/sections/EffectsSettings.jsx new file mode 100644 index 00000000..bae7af5b --- /dev/null +++ b/src/features/background/options/sections/EffectsSettings.jsx @@ -0,0 +1,71 @@ +import variables from 'config/variables'; +import { Dropdown, Slider } from 'components/Form/Settings'; +import { Row, Content, Action } from 'components/Layout/Settings/Item'; +import values from 'utils/data/slider_values.json'; +import { backgroundImageEffects } from '../optionTypes'; + +const EffectsSettings = ({ backgroundFilter, onFilterChange }) => { + return ( + + + + + + + {backgroundFilter !== 'none' && ( + + )} + + + ); +}; + +export default EffectsSettings; diff --git a/src/features/background/options/sections/NavigationCard.jsx b/src/features/background/options/sections/NavigationCard.jsx new file mode 100644 index 00000000..9494d985 --- /dev/null +++ b/src/features/background/options/sections/NavigationCard.jsx @@ -0,0 +1,20 @@ +import { MdOutlineKeyboardArrowRight } from 'react-icons/md'; + +const NavigationCard = ({ icon: Icon, title, subtitle, onClick, action }) => { + return ( +
+
+ +
+ {title} + {subtitle} +
+
+
+ {action || } +
+
+ ); +}; + +export default NavigationCard; diff --git a/src/features/background/options/sections/SourceSection.jsx b/src/features/background/options/sections/SourceSection.jsx new file mode 100644 index 00000000..d9a67d9e --- /dev/null +++ b/src/features/background/options/sections/SourceSection.jsx @@ -0,0 +1,30 @@ +import variables from 'config/variables'; +import { Dropdown } from 'components/Form/Settings'; +import { Row, Content, Action } from 'components/Layout/Settings/Item'; +import { getBackgroundOptionItems } from '../optionTypes'; + +const SourceSection = ({ backgroundType, marketplaceEnabled, onTypeChange }) => { + return ( + + + + + + + ); +}; + +export default SourceSection; diff --git a/src/features/marketplace/views/Added.jsx b/src/features/marketplace/views/Added.jsx index b4541254..f2ec749c 100644 --- a/src/features/marketplace/views/Added.jsx +++ b/src/features/marketplace/views/Added.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useEffect, useCallback } from 'react'; import { MdUpdate, MdOutlineExtensionOff, MdSendTimeExtension } from 'react-icons/md'; import { toast } from 'react-toastify'; import Modal from 'react-modal'; @@ -13,37 +13,32 @@ import { Button } from 'components/Elements'; import { install, uninstall, urlParser } from 'utils/marketplace'; -export default class Added extends PureComponent { - constructor() { - super(); - this.state = { - installed: JSON.parse(localStorage.getItem('installed')), - item: {}, - button: '', - showFailed: false, - failedReason: '', - }; - this.buttons = { - uninstall: ( -
+ sortAddons(value)} + items={[ + { value: 'newest', text: variables.getMessage('modals.main.addons.sort.newest') }, + { value: 'oldest', text: variables.getMessage('modals.main.addons.sort.oldest') }, + { value: 'a-z', text: variables.getMessage('modals.main.addons.sort.a_z') }, + { value: 'z-a', text: variables.getMessage('modals.main.addons.sort.z_a') }, + ]} + /> + toggle('item', input)} + showCreateYourOwn={false} + /> + + ); +}); + +Added.displayName = 'Added'; + +export default Added; diff --git a/src/features/marketplace/views/Create.jsx b/src/features/marketplace/views/Create.jsx index a68f774c..393101a4 100644 --- a/src/features/marketplace/views/Create.jsx +++ b/src/features/marketplace/views/Create.jsx @@ -1,41 +1,35 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; import { MdOutlineExtensionOff } from 'react-icons/md'; import { Button } from 'components/Elements'; -export default class Create extends PureComponent { - constructor() { - super(); - this.state = {}; - } - - render() { - return ( - <> -
- - {variables.getMessage('modals.main.addons.create.title')} +const Create = () => { + return ( + <> +
+ + {variables.getMessage('modals.main.addons.create.title')} + +
+
+
+ + + {variables.getMessage('modals.main.addons.create.moved_title')} -
-
-
- - - {variables.getMessage('modals.main.addons.create.moved_title')} - - - {variables.getMessage('modals.main.addons.create.moved_description')} - -
-
+ + {variables.getMessage('modals.main.addons.create.moved_description')} + +
+
- - ); - } -} +
+ + ); +}; + +export { Create as default, Create }; diff --git a/src/features/message/options/MessageOptions.jsx b/src/features/message/options/MessageOptions.jsx index 0c292861..f2784cd7 100644 --- a/src/features/message/options/MessageOptions.jsx +++ b/src/features/message/options/MessageOptions.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { useState } from 'react'; import { MdCancel, MdAdd, MdOutlineTextsms } from 'react-icons/md'; import { toast } from 'react-toastify'; import { TextareaAutosize } from '@mui/material'; @@ -8,142 +8,126 @@ import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Lay import { Button } from 'components/Elements'; import EventBus from 'utils/eventbus'; -class MessageOptions extends PureComponent { - constructor() { - super(); - this.state = { - messages: JSON.parse(localStorage.getItem('messages')) || [], - }; - } +const MessageOptions = () => { + const [messages, setMessages] = useState(JSON.parse(localStorage.getItem('messages')) || []); - reset = () => { + const reset = () => { localStorage.setItem('messages', '[]'); - this.setState({ - messages: [], - }); - toast(variables.getMessage(this.languagecode, 'toasts.reset')); + setMessages([]); + toast(variables.getMessage('toasts.reset')); EventBus.emit('refresh', 'message'); }; - modifyMessage(type, index) { - const messages = this.state.messages; + const modifyMessage = (type, index) => { + const updatedMessages = [...messages]; if (type === 'add') { - messages.push(''); + updatedMessages.push(''); } else { - messages.splice(index, 1); + updatedMessages.splice(index, 1); } - this.setState({ - messages, - }); - this.forceUpdate(); + setMessages(updatedMessages); + localStorage.setItem('messages', JSON.stringify(updatedMessages)); + }; - localStorage.setItem('messages', JSON.stringify(messages)); - } - - message(e, text, index) { + const message = (e, text, index) => { const result = text === true ? e.target.value : e.target.result; - const messages = this.state.messages; - messages[index] = result; - this.setState({ - messages, - }); - this.forceUpdate(); + const updatedMessages = [...messages]; + updatedMessages[index] = result; + setMessages(updatedMessages); - localStorage.setItem('messages', JSON.stringify(messages)); + localStorage.setItem('messages', JSON.stringify(updatedMessages)); document.querySelector('.reminder-info').style.display = 'flex'; localStorage.setItem('showReminder', true); - } + }; - render() { - const MESSAGE_SECTION = 'modals.main.settings.sections.message'; + const MESSAGE_SECTION = 'modals.main.settings.sections.message'; - return ( - <> -
- - - - + return ( + <> +
+ + + + +
+
+ + ))} + + {messages.length === 0 && ( +
+
+ + + {variables.getMessage(`${MESSAGE_SECTION}.no_messages`)} + + + {variables.getMessage(`${MESSAGE_SECTION}.add_some`)} +
-
- - ))} - - {this.state.messages.length === 0 && ( -
-
- - - {variables.getMessage(`${MESSAGE_SECTION}.no_messages`)} - - - {variables.getMessage(`${MESSAGE_SECTION}.add_some`)} - -
- )} - - - ); - } -} + + )} + + + ); +}; export { MessageOptions as default, MessageOptions }; diff --git a/src/features/misc/modals/Modals.jsx b/src/features/misc/modals/Modals.jsx index 92bf67c6..bff1010b 100644 --- a/src/features/misc/modals/Modals.jsx +++ b/src/features/misc/modals/Modals.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { useState, useEffect } from 'react'; import Modal from 'react-modal'; import { MainModal } from 'components/Elements'; @@ -11,28 +11,21 @@ import { parseDeepLink, shouldAutoOpenModal } from 'utils/deepLinking'; import Welcome from 'features/welcome/Welcome'; -export default class Modals extends PureComponent { - constructor() { - super(); - this.state = { - mainModal: false, - updateModal: false, - welcomeModal: false, - appsModal: false, - preview: false, - deepLinkData: null, - }; - } +const Modals = () => { + const [mainModal, setMainModal] = useState(false); + const [updateModal, setUpdateModal] = useState(false); + const [welcomeModal, setWelcomeModal] = useState(false); + const [appsModal, setAppsModal] = useState(false); + const [preview, setPreview] = useState(false); + const [deepLinkData, setDeepLinkData] = useState(null); - componentDidMount() { + useEffect(() => { // Check for deep link first (has priority) if (shouldAutoOpenModal()) { - const deepLinkData = parseDeepLink(); - this.setState({ - mainModal: true, - deepLinkData, - }); - variables.stats.postEvent('modal', `Opened via deep link: ${deepLinkData.tab}`); + const linkData = parseDeepLink(); + setMainModal(true); + setDeepLinkData(linkData); + variables.stats.postEvent('modal', `Opened via deep link: ${linkData.tab}`); return; } @@ -40,9 +33,7 @@ export default class Modals extends PureComponent { localStorage.getItem('showWelcome') === 'true' && window.location.search !== '?nointro=true' ) { - this.setState({ - welcomeModal: true, - }); + setWelcomeModal(true); variables.stats.postEvent('modal', 'Opened welcome'); } @@ -56,71 +47,69 @@ export default class Modals extends PureComponent { // hide refresh reminder once the user has refreshed the page localStorage.setItem('showReminder', false); - } + }, []); - closeWelcome() { + const closeWelcome = () => { localStorage.setItem('showWelcome', false); - this.setState({ - welcomeModal: false, - }); + setWelcomeModal(false); EventBus.emit('refresh', 'widgetsWelcomeDone'); EventBus.emit('refresh', 'widgets'); EventBus.emit('refresh', 'backgroundwelcome'); - } + }; - previewWelcome() { + const previewWelcome = () => { localStorage.setItem('showWelcome', false); localStorage.setItem('welcomePreview', true); - this.setState({ - welcomeModal: false, - preview: true, - }); + setWelcomeModal(false); + setPreview(true); EventBus.emit('refresh', 'widgetsWelcome'); - } + }; - toggleModal(type, action) { - this.setState({ - [type]: action, - }); + const toggleModal = (type, action) => { + const modalSetters = { + mainModal: setMainModal, + updateModal: setUpdateModal, + welcomeModal: setWelcomeModal, + appsModal: setAppsModal, + }; + + if (modalSetters[type]) { + modalSetters[type](action); + } if (action !== false) { variables.stats.postEvent('modal', `Opened ${type.replace('Modal', '')}`); } - } + }; - render() { - return ( - <> - {this.state.welcomeModal === false && ( - this.toggleModal(modal, true)} /> - )} - this.toggleModal('mainModal', false)} - isOpen={this.state.mainModal} - className="Modal mainModal" - overlayClassName="Overlay" - ariaHideApp={false} - > - this.toggleModal('mainModal', false)} - deepLinkData={this.state.deepLinkData} - /> - - this.closeWelcome()} - isOpen={this.state.welcomeModal} - className="Modal welcomemodal mainModal" - overlayClassName="Overlay mainModal" - shouldCloseOnOverlayClick={false} - ariaHideApp={false} - > - this.closeWelcome()} modalSkip={() => this.previewWelcome()} /> - - {this.state.preview && window.location.reload()} />} - - ); - } -} + return ( + <> + {welcomeModal === false && toggleModal(modal, true)} />} + toggleModal('mainModal', false)} + isOpen={mainModal} + className="Modal mainModal" + overlayClassName="Overlay" + ariaHideApp={false} + > + toggleModal('mainModal', false)} deepLinkData={deepLinkData} /> + + closeWelcome()} + isOpen={welcomeModal} + className="Modal welcomemodal mainModal" + overlayClassName="Overlay mainModal" + shouldCloseOnOverlayClick={false} + ariaHideApp={false} + > + closeWelcome()} modalSkip={() => previewWelcome()} /> + + {preview && window.location.reload()} />} + + ); +}; + +export { Modals as default, Modals }; diff --git a/src/features/misc/sections/Changelog.jsx b/src/features/misc/sections/Changelog.jsx index 8a51f954..03df9dcc 100644 --- a/src/features/misc/sections/Changelog.jsx +++ b/src/features/misc/sections/Changelog.jsx @@ -1,24 +1,24 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { MdOutlineWifiOff } from 'react-icons/md'; import Modal from 'react-modal'; import Lightbox from '../../marketplace/components/Elements/Lightbox/Lightbox'; -class Changelog extends PureComponent { - constructor() { - super(); - this.state = { - title: null, - showLightbox: false, - lightboxImg: null, - }; - this.offlineMode = localStorage.getItem('offlineMode') === 'true'; - this.controller = new AbortController(); - this.changelog = createRef(); - } +const Changelog = () => { + const [title, setTitle] = useState(null); + const [content, setContent] = useState(null); + const [date, setDate] = useState(null); + const [image, setImage] = useState(null); + const [error, setError] = useState(false); + const [showLightbox, setShowLightbox] = useState(false); + const [lightboxImg, setLightboxImg] = useState(null); + + const offlineMode = localStorage.getItem('offlineMode') === 'true'; + const controllerRef = useRef(new AbortController()); + const changelog = useRef(); - parseMarkdown = (text) => { + const parseMarkdown = (text) => { if (typeof text !== 'string') { throw new Error('Input must be a string'); } @@ -46,15 +46,15 @@ class Changelog extends PureComponent { return text; }; - async getUpdate() { + const getUpdate = async () => { const releases = await fetch( `https://api.github.com/repos/${variables.constants.ORG_NAME}/${variables.constants.REPO_NAME}/releases`, { - signal: this.controller.signal, + signal: controllerRef.current.signal, }, ); - if (this.controller.signal.aborted === true) { + if (controllerRef.current.signal.aborted === true) { return; } @@ -67,109 +67,105 @@ class Changelog extends PureComponent { } // request the changelog - const res = await fetch(release.url, { signal: this.controller.signal }); + const res = await fetch(release.url, { signal: controllerRef.current.signal }); if (res.status === 404) { - this.setState({ error: true }); + setError(true); return; } - if (this.controller.signal.aborted === true) { + if (controllerRef.current.signal.aborted === true) { return; } const changelog = await res.json(); - this.setState({ - title: changelog.name, - content: this.parseMarkdown(changelog.body), - date: new Date(changelog.published_at).toLocaleDateString(), - }); - } + setTitle(changelog.name); + setContent(parseMarkdown(changelog.body)); + setDate(new Date(changelog.published_at).toLocaleDateString()); + }; - componentDidMount() { - if (navigator.onLine === false || this.offlineMode) { + useEffect(() => { + if (navigator.onLine === false || offlineMode) { return; } - this.getUpdate(); - } + getUpdate(); - componentWillUnmount() { - // stop making requests - this.controller.abort(); - } - - render() { - const errorMessage = (msg) => { - return ( -
-
{msg}
-
- ); + return () => { + // stop making requests + controllerRef.current.abort(); }; + }, []); - if (navigator.onLine === false || this.offlineMode) { - return errorMessage( - <> - -

{variables.getMessage('modals.main.marketplace.offline.title')}

-

- {variables.getMessage('modals.main.marketplace.offline.description')} -

- , - ); - } - - if (this.state.error === true) { - return errorMessage( - <> - - {variables.getMessage('modals.main.error_boundary.title')} - - {variables.getMessage('modals.main.error_boundary.message')} - - , - ); - } - - if (!this.state.title) { - return errorMessage( -
-
- {variables.getMessage('modals.main.loading')} -
, - ); - } - + const errorMessage = (msg) => { return ( -
- {this.state.title} - Released on {this.state.date} - {this.state.image && ( - {this.state.title} - )} -
- this.setState({ showLightbox: false })} - isOpen={this.state.showLightbox} - className="Modal lightBoxModal" - overlayClassName="Overlay resetoverlay" - ariaHideApp={false} - > - this.setState({ showLightbox: false })} - img={this.state.lightboxImg} - /> - +
+
{msg}
); + }; + + if (navigator.onLine === false || offlineMode) { + return errorMessage( + <> + +

{variables.getMessage('modals.main.marketplace.offline.title')}

+

+ {variables.getMessage('modals.main.marketplace.offline.description')} +

+ , + ); } -} + + if (error === true) { + return errorMessage( + <> + + {variables.getMessage('modals.main.error_boundary.title')} + + {variables.getMessage('modals.main.error_boundary.message')} + + , + ); + } + + if (!title) { + return errorMessage( +
+
+ {variables.getMessage('modals.main.loading')} +
, + ); + } + + return ( +
+ {title} + Released on {date} + {image && ( + {title} + )} +
+ setShowLightbox(false)} + isOpen={showLightbox} + className="Modal lightBoxModal" + overlayClassName="Overlay resetoverlay" + ariaHideApp={false} + > + setShowLightbox(false)} + img={lightboxImg} + /> + +
+ ); +}; export { Changelog as default, Changelog }; diff --git a/src/features/misc/sections/Language.jsx b/src/features/misc/sections/Language.jsx index 1789773a..9b548e64 100644 --- a/src/features/misc/sections/Language.jsx +++ b/src/features/misc/sections/Language.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { MdOutlineOpenInNew } from 'react-icons/md'; @@ -7,32 +7,28 @@ import { Radio } from 'components/Form/Settings'; import languages from '@/i18n/languages.json'; -class LanguageOptions extends PureComponent { - constructor() { - super(); - this.state = { - quoteLanguages: [ - { - name: variables.getMessage('modals.main.loading'), - value: 'loading', - }, - ], - }; - this.controller = new AbortController(); - } +const LanguageOptions = () => { + const [quoteLanguages, setQuoteLanguages] = useState([ + { + name: variables.getMessage('modals.main.loading'), + value: 'loading', + }, + ]); - async getquoteLanguages() { + const controllerRef = useRef(new AbortController()); + + const getquoteLanguages = async () => { const data = await ( await fetch(variables.constants.API_URL + '/quotes/languages', { - signal: this.controller.signal, + signal: controllerRef.current.signal, }) ).json(); - if (this.controller.signal.aborted === true) { + if (controllerRef.current.signal.aborted === true) { return; } - const quoteLanguages = data.map((language) => { + const fetchedQuoteLanguages = data.map((language) => { return { name: languages.find((l) => l.value === language.name) ? languages.find((l) => l.value === language.name).name @@ -41,69 +37,64 @@ class LanguageOptions extends PureComponent { }; }); - this.setState({ - quoteLanguages, - }); - } + setQuoteLanguages(fetchedQuoteLanguages); + }; - componentDidMount() { + useEffect(() => { if (navigator.onLine === false || localStorage.getItem('offlineMode') === 'true') { - return this.setState({ - quoteLanguages: [ - { - name: variables.getMessage('modals.main.marketplace.offline.description'), - value: 'loading', - }, - ], - }); + setQuoteLanguages([ + { + name: variables.getMessage('modals.main.marketplace.offline.description'), + value: 'loading', + }, + ]); + return; } - this.getquoteLanguages(); - } + getquoteLanguages(); - componentWillUnmount() { - // stop making requests - this.controller.abort(); - } + return () => { + // stop making requests + controllerRef.current.abort(); + }; + }, []); - render() { - return ( - <> -
- - {variables.getMessage('modals.main.settings.sections.language.title')} - - -
-
- -
+ return ( + <> +
- {variables.getMessage('modals.main.settings.sections.language.quote')} + {variables.getMessage('modals.main.settings.sections.language.title')} -
- { - return { name: language.name, value: language.value.name }; - })} - defaultValue={this.state.quoteLanguages[0].name} - category="quote" - /> + - - ); - } -} +
+
+ +
+ + {variables.getMessage('modals.main.settings.sections.language.quote')} + +
+ { + return { name: language.name, value: language.value.name }; + })} + defaultValue={quoteLanguages[0].name} + category="quote" + /> +
+ + ); +}; export { LanguageOptions as default, LanguageOptions }; diff --git a/src/features/misc/views/Widgets.jsx b/src/features/misc/views/Widgets.jsx index a2bb889f..0d29496a 100644 --- a/src/features/misc/views/Widgets.jsx +++ b/src/features/misc/views/Widgets.jsx @@ -1,4 +1,4 @@ -import { PureComponent, Fragment, Suspense, lazy } from 'react'; +import { useState, useEffect, Fragment, Suspense, lazy, useMemo } from 'react'; import Clock from '../../time/Clock'; import Greeting from '../../greeting/Greeting'; @@ -17,74 +17,69 @@ import EventBus from 'utils/eventbus'; // as seen here it is ridiculously large const Weather = lazy(() => import('../../weather/Weather')); -export default class Widgets extends PureComponent { - online = localStorage.getItem('offlineMode') === 'false'; - constructor() { - super(); - this.state = { - order: JSON.parse(localStorage.getItem('order')), - welcome: localStorage.getItem('showWelcome'), - }; - // widgets we can re-order - this.widgets = { - time: this.enabled('time') && , - greeting: this.enabled('greeting') && , - quote: this.enabled('quote') && , - date: this.enabled('date') && , - quicklinks: this.enabled('quicklinksenabled') && this.online ? : null, - message: this.enabled('message') && , - }; - } +const Widgets = () => { + const online = localStorage.getItem('offlineMode') === 'false'; + const [order, setOrder] = useState(JSON.parse(localStorage.getItem('order'))); + const [welcome, setWelcome] = useState(localStorage.getItem('showWelcome')); - enabled(key) { + const enabled = (key) => { return localStorage.getItem(key) === 'true'; - } + }; - componentDidMount() { - EventBus.on('refresh', (data) => { + // widgets we can re-order + const widgets = useMemo( + () => ({ + time: enabled('time') && , + greeting: enabled('greeting') && , + quote: enabled('quote') && , + date: enabled('date') && , + quicklinks: enabled('quicklinksenabled') && online ? : null, + message: enabled('message') && , + }), + [order], // Re-create widgets when order changes + ); + + useEffect(() => { + const handleRefresh = (data) => { switch (data) { case 'widgets': - return this.setState({ - order: JSON.parse(localStorage.getItem('order')), - }); + return setOrder(JSON.parse(localStorage.getItem('order'))); case 'widgetsWelcome': - this.setState({ - welcome: localStorage.getItem('showWelcome'), - }); + setWelcome(localStorage.getItem('showWelcome')); localStorage.setItem('showWelcome', true); window.onbeforeunload = () => { localStorage.clear(); }; break; case 'widgetsWelcomeDone': - this.setState({ - welcome: localStorage.getItem('showWelcome'), - }); - return (window.onbeforeunload = null); + setWelcome(localStorage.getItem('showWelcome')); + window.onbeforeunload = null; + break; default: break; } - }); - } + }; - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + }; + }, []); - render() { - // don't show when welcome is there - return this.state.welcome !== 'false' ? ( - - ) : ( - - }> - {this.enabled('searchBar') && } - {this.state.order.map((element, key) => ( - {this.widgets[element]} - ))} - {this.enabled('weatherEnabled') && this.online ? : null} - - - ); - } -} + // don't show when welcome is there + return welcome !== 'false' ? ( + + ) : ( + + }> + {enabled('searchBar') && } + {order.map((element, key) => ( + {widgets[element]} + ))} + {enabled('weatherEnabled') && online ? : null} + + + ); +}; + +export { Widgets as default, Widgets }; diff --git a/src/features/navbar/Navbar.jsx b/src/features/navbar/Navbar.jsx index b0718627..6ec9875e 100644 --- a/src/features/navbar/Navbar.jsx +++ b/src/features/navbar/Navbar.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { MdSettings } from 'react-icons/md'; @@ -10,26 +10,22 @@ import EventBus from 'utils/eventbus'; import './scss/index.scss'; -class Navbar extends PureComponent { - constructor() { - super(); - this.navbarContainer = createRef(); - this.state = { - classList: localStorage.getItem('widgetStyle') === 'legacy' ? 'navbar old' : 'navbar new', - refreshText: '', - refreshEnabled: localStorage.getItem('refresh'), - refreshOption: localStorage.getItem('refreshOption') || '', - appsOpen: false, - }; - } +const Navbar = ({ openModal }) => { + const navbarContainer = useRef(); + const [classList] = useState( + localStorage.getItem('widgetStyle') === 'legacy' ? 'navbar old' : 'navbar new', + ); + const [refreshText, setRefreshText] = useState(''); + const [refreshEnabled, setRefreshEnabled] = useState(localStorage.getItem('refresh')); + const [refreshOption, setRefreshOption] = useState(localStorage.getItem('refreshOption') || ''); + const [appsOpen, setAppsOpen] = useState(false); + const [zoomFontSize, setZoomFontSize] = useState('1.2rem'); - setZoom() { - this.setState({ - zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem', - }); - } + const setZoom = () => { + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); + }; - updateRefreshText() { + const updateRefreshText = () => { let refreshText; switch (localStorage.getItem('refreshOption')) { case 'background': @@ -51,38 +47,35 @@ class Navbar extends PureComponent { break; } - this.setState({ refreshText }); - } + setRefreshText(refreshText); + }; - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'navbar' || data === 'background') { - this.setState({ - refreshEnabled: localStorage.getItem('refresh'), - refreshOption: localStorage.getItem('refreshOption'), - }); - - this.forceUpdate(); + setRefreshEnabled(localStorage.getItem('refresh')); + setRefreshOption(localStorage.getItem('refreshOption')); try { - this.updateRefreshText(); - this.setZoom(); + updateRefreshText(); + setZoom(); } catch { // Ignore errors } } - }); + }; - this.updateRefreshText(); - this.setZoom(); - } + updateRefreshText(); + setZoom(); - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + }; + }, []); - refresh() { - switch (this.state.refreshOption) { + const refresh = () => { + switch (refreshOption) { case 'background': return EventBus.emit('refresh', 'backgroundrefresh'); case 'quote': @@ -93,59 +86,47 @@ class Navbar extends PureComponent { default: window.location.reload(); } - } + }; - render() { - const backgroundEnabled = localStorage.getItem('background') === 'true'; + const backgroundEnabled = localStorage.getItem('background') === 'true'; - const navbar = ( -
-
- {localStorage.getItem('view') === 'true' && backgroundEnabled ? ( - - ) : null} - {localStorage.getItem('notesEnabled') === 'true' && ( - - )} - {localStorage.getItem('todoEnabled') === 'true' && ( - - )} - {localStorage.getItem('appsEnabled') === 'true' && ( - - )} + const navbar = ( +
+
+ {localStorage.getItem('view') === 'true' && backgroundEnabled ? ( + + ) : null} + {localStorage.getItem('notesEnabled') === 'true' && } + {localStorage.getItem('todoEnabled') === 'true' && } + {localStorage.getItem('appsEnabled') === 'true' && } - {this.state.refreshEnabled !== 'false' && } + {refreshEnabled !== 'false' && } - + - -
+ + +
- ); +
+ ); - return localStorage.getItem('navbarHover') === 'true' ? ( -
{navbar}
- ) : ( - navbar - ); - } -} + return localStorage.getItem('navbarHover') === 'true' ? ( +
{navbar}
+ ) : ( + navbar + ); +}; export { Navbar as default, Navbar }; diff --git a/src/features/navbar/components/Apps.jsx b/src/features/navbar/components/Apps.jsx index bee909d8..0abadabd 100644 --- a/src/features/navbar/components/Apps.jsx +++ b/src/features/navbar/components/Apps.jsx @@ -1,6 +1,6 @@ // TODO: make it work with pins or on click or smth import variables from 'config/variables'; -import { PureComponent, memo, useState } from 'react'; +import { memo, useState, useEffect } from 'react'; import { MdPlaylistRemove, MdOutlineApps } from 'react-icons/md'; import { Tooltip } from 'components/Elements'; @@ -8,124 +8,121 @@ import { Tooltip } from 'components/Elements'; import { shift, useFloating } from '@floating-ui/react-dom'; import EventBus from 'utils/eventbus'; -class Apps extends PureComponent { - constructor() { - super(); - this.state = { - apps: JSON.parse(localStorage.getItem('applinks')), - visibility: localStorage.getItem('appsPinned') === 'true' ? 'visible' : 'hidden', - marginLeft: localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px', - showApps: localStorage.getItem('appsPinned') === 'true', - }; - } +const Apps = ({ appsRef, floatRef, position, xPosition, yPosition }) => { + const [apps, setApps] = useState(JSON.parse(localStorage.getItem('applinks'))); + const [visibility, setVisibility] = useState( + localStorage.getItem('appsPinned') === 'true' ? 'visible' : 'hidden', + ); + const [marginLeft, setMarginLeft] = useState( + localStorage.getItem('refresh') === 'false' ? '-200px' : '-130px', + ); + const [showApps, setShowApps] = useState(localStorage.getItem('appsPinned') === 'true'); + const [zoomFontSize, setZoomFontSize] = useState('1.2rem'); - setZoom() { - this.setState({ - zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem', - }); - } + const setZoom = () => { + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); + }; - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'navbar') { - this.forceUpdate(); + setApps(JSON.parse(localStorage.getItem('applinks'))); try { - this.setZoom(); + setZoom(); } catch { // Ignore errors } } - }); + }; - this.setZoom(); - } + setZoom(); - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh', handleRefresh); + }; + }, []); - showApps() { - this.setState({ showApps: true }); - } + const handleShowApps = () => { + setShowApps(true); + }; - hideApps() { - this.setState({ showApps: localStorage.getItem('AppsPinned') === 'true' }); - } + const handleHideApps = () => { + setShowApps(localStorage.getItem('AppsPinned') === 'true'); + }; - render() { - const appsInfo = this.state.apps; + const appsInfo = apps; - return ( -
this.hideApps()} onFocus={() => this.showApps()}> - + {showApps && ( + - - - {this.state.showApps && ( - -
-
- - {variables.getMessage('widgets.navbar.apps.title')} +
+
+ + {variables.getMessage('widgets.navbar.apps.title')} +
+
+ {appsInfo.length > 0 ? ( +
+ {appsInfo.map((info, i) => ( + + + Google + {info.name} + + + ))} +
+ ) : ( +
+
+ + + {variables.language.getMessage( + variables.languagecode, + 'widgets.navbar.apps.no_apps', + )} +
- {appsInfo.length > 0 ? ( -
- {appsInfo.map((info, i) => ( - - - Google - {info.name} - - - ))} -
- ) : ( -
-
- - - {variables.language.getMessage( - variables.languagecode, - 'widgets.navbar.apps.no_apps', - )} - -
-
- )} - - )} -
- ); - } -} + )} + + )} +
+ ); +}; function AppsWrapper() { const [reference, setReference] = useState(null); diff --git a/src/features/navbar/components/Maximise.jsx b/src/features/navbar/components/Maximise.jsx index 222d0d7e..8d290623 100644 --- a/src/features/navbar/components/Maximise.jsx +++ b/src/features/navbar/components/Maximise.jsx @@ -1,18 +1,14 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useCallback } from 'react'; import { MdCropFree } from 'react-icons/md'; import { Tooltip } from 'components/Elements'; -class Maximise extends PureComponent { - constructor() { - super(); - this.state = { - hidden: false, - }; - } - setAttribute(blur, brightness, filter) { +const Maximise = memo(({ fontSize }) => { + const [hidden, setHidden] = useState(false); + + const setAttribute = useCallback((blur, brightness, filter) => { // don't attempt to modify the background if it isn't an image const backgroundType = localStorage.getItem('backgroundType'); if ( @@ -43,48 +39,41 @@ class Maximise extends PureComponent { : '' };`, ); - } + }, []); - maximise = () => { + const maximise = useCallback(() => { // hide widgets const widgets = document.getElementById('widgets'); - this.state.hidden === false - ? (widgets.style.display = 'none') - : (widgets.style.display = 'flex'); - - if (this.state.hidden === false) { - this.setState({ - hidden: true, - }); - - this.setAttribute(0, 100); + + if (!hidden) { + widgets.style.display = 'none'; + setHidden(true); + setAttribute(0, 100); variables.stats.postEvent('feature', 'Background maximise'); } else { - this.setState({ - hidden: false, - }); - - this.setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true); + widgets.style.display = 'flex'; + setHidden(false); + setAttribute(localStorage.getItem('blur'), localStorage.getItem('brightness'), true); variables.stats.postEvent('feature', 'Background unmaximise'); } - }; + }, [hidden, setAttribute]); - render() { - return ( - + - - ); - } -} + + + + ); +}); + +Maximise.displayName = 'Maximise'; export { Maximise as default, Maximise }; diff --git a/src/features/navbar/components/Notes.jsx b/src/features/navbar/components/Notes.jsx index 34b2f890..7b3750a7 100644 --- a/src/features/navbar/components/Notes.jsx +++ b/src/features/navbar/components/Notes.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent, memo, useState } from 'react'; +import { memo, useState, useEffect } from 'react'; import { MdContentCopy, MdAssignment, MdPushPin, MdDownload } from 'react-icons/md'; import { useFloating, shift } from '@floating-ui/react-dom'; @@ -10,137 +10,119 @@ import { Tooltip } from 'components/Elements'; import { saveFile } from 'utils/saveFile'; import EventBus from 'utils/eventbus'; -class Notes extends PureComponent { - constructor() { - super(); - this.state = { - notes: localStorage.getItem('notes') || '', - visibility: localStorage.getItem('notesPinned') === 'true' ? 'visible' : 'hidden', - showNotes: localStorage.getItem('notesPinned') === 'true' ? true : false, - }; - } +const Notes = ({ notesRef, floatRef, position, xPosition, yPosition }) => { + const [notes, setNotes] = useState(localStorage.getItem('notes') || ''); + const [showNotes, setShowNotes] = useState(localStorage.getItem('notesPinned') === 'true'); + const [zoomFontSize, setZoomFontSize] = useState('1.2rem'); - setZoom() { - this.setState({ - zoomFontSize: Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem', - }); - } - - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'navbar') { - this.forceUpdate(); - try { - this.setZoom(); - } catch { - // Ignore errors - } + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); } - }); + }; - this.setZoom(); - } + setZoomFontSize(Number(((localStorage.getItem('zoomNavbar') || 100) / 100) * 1.2) + 'rem'); - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + }; + }, []); - setNotes = (e) => { + const handleSetNotes = (e) => { localStorage.setItem('notes', e.target.value); - this.setState({ notes: e.target.value }); + setNotes(e.target.value); }; - showNotes() { - this.setState({ showNotes: true }); - } + const handleShowNotes = () => { + setShowNotes(true); + }; - hideNotes() { - this.setState({ showNotes: localStorage.getItem('notesPinned') === 'true' }); - } + const handleHideNotes = () => { + setShowNotes(localStorage.getItem('notesPinned') === 'true'); + }; - pin() { + const handlePin = () => { variables.stats.postEvent('feature', 'Notes pin'); const notesPinned = localStorage.getItem('notesPinned') === 'true'; localStorage.setItem('notesPinned', !notesPinned); - this.setState({ showNotes: !notesPinned }); - } + setShowNotes(!notesPinned); + }; - copy() { + const handleCopy = () => { variables.stats.postEvent('feature', 'Notes copied'); - navigator.clipboard.writeText(this.state.notes); + navigator.clipboard.writeText(notes); toast(variables.getMessage('toasts.notes')); - } + }; - download() { - const notes = localStorage.getItem('notes'); + const handleDownload = () => { if (!notes || notes === '') { return; } variables.stats.postEvent('feature', 'Notes download'); - saveFile(this.state.notes, 'mue-notes.txt', 'text/plain'); - } + saveFile(notes, 'mue-notes.txt', 'text/plain'); + }; - render() { - return ( -
this.hideNotes()} onFocus={() => this.showNotes()}> - + {showNotes && ( + - - - {this.state.showNotes && ( - -
-
- - {variables.getMessage('widgets.navbar.notes.title')} -
-
- - - - - - - - - -
- +
+
+ + {variables.getMessage('widgets.navbar.notes.title')}
- - )} -
- ); - } -} +
+ + + + + + + + + +
+ +
+
+ )} +
+ ); +}; function NotesWrapper() { const [reference, setReference] = useState(null); diff --git a/src/features/quicklinks/QuickLinks.jsx b/src/features/quicklinks/QuickLinks.jsx index 6ebfa0da..254203a5 100644 --- a/src/features/quicklinks/QuickLinks.jsx +++ b/src/features/quicklinks/QuickLinks.jsx @@ -1,4 +1,4 @@ -import { PureComponent, createRef } from 'react'; +import { memo, useRef, useState, useEffect, useCallback } from 'react'; import { Tooltip } from 'components/Elements'; import EventBus from 'utils/eventbus'; @@ -18,18 +18,13 @@ const readQuicklinks = () => { } }; -class QuickLinks extends PureComponent { - constructor() { - super(); - this.state = { - items: readQuicklinks(), - forceUpdate: 0, // Used to force complete re-render - }; - this.quicklinksContainer = createRef(); - } +const QuickLinks = memo(() => { + const [items, setItems] = useState(readQuicklinks()); + const [forceUpdate, setForceUpdate] = useState(0); // Used to force complete re-render + const quicklinksContainer = useRef(null); // widget zoom - setZoom(element) { + const setZoom = useCallback((element) => { if (!element) return; const zoom = localStorage.getItem('zoomQuicklinks') || 100; @@ -42,103 +37,109 @@ class QuickLinks extends PureComponent { img.style.height = `${30 * Number(zoom / 100)}px`; } } - } + }, []); - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'quicklinks') { if (localStorage.getItem('quicklinksenabled') === 'false') { - return (this.quicklinksContainer.current.style.display = 'none'); + if (quicklinksContainer.current) { + quicklinksContainer.current.style.display = 'none'; + } + return; } - this.quicklinksContainer.current.style.display = 'flex'; + if (quicklinksContainer.current) { + quicklinksContainer.current.style.display = 'flex'; + } - this.setState({ - items: readQuicklinks(), - forceUpdate: Date.now(), - }, () => { - this.setZoom(this.quicklinksContainer.current); - }); + setItems(readQuicklinks()); + setForceUpdate(Date.now()); } - }); - - this.setZoom(this.quicklinksContainer.current); - } - - componentWillUnmount() { - EventBus.off('refresh'); - } - - render() { - let target, - rel = null; - if (localStorage.getItem('quicklinksnewtab') === 'true') { - target = '_blank'; - rel = 'noopener noreferrer'; - } - - const tooltipEnabled = localStorage.getItem('quicklinkstooltip'); - - const quickLink = (item, index) => { - if (localStorage.getItem('quickLinksStyle') === 'text') { - return ( - - {item.name} - - ); - } - - const img = - item.icon || - 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', ''); - - if (localStorage.getItem('quickLinksStyle') === 'metro') { - return ( - - {item.name} - {item.name} - - ); - } - - const link = ( - - {item.name} - - ); - - return tooltipEnabled === 'true' ? ( - - {link} - - ) : ( - link - ); }; - return ( -
- {this.state.items && this.state.items.map((item, index) => quickLink(item, index))} -
- ); + EventBus.on('refresh', handleRefresh); + + setZoom(quicklinksContainer.current); + + return () => { + EventBus.off('refresh', handleRefresh); + }; + }, [setZoom]); + + useEffect(() => { + setZoom(quicklinksContainer.current); + }, [items, forceUpdate, setZoom]); + + let target, rel = null; + if (localStorage.getItem('quicklinksnewtab') === 'true') { + target = '_blank'; + rel = 'noopener noreferrer'; } -} + + const tooltipEnabled = localStorage.getItem('quicklinkstooltip'); + + const quickLink = (item, index) => { + if (localStorage.getItem('quickLinksStyle') === 'text') { + return ( + + {item.name} + + ); + } + + const img = + item.icon || + 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', ''); + + if (localStorage.getItem('quickLinksStyle') === 'metro') { + return ( + + {item.name} + {item.name} + + ); + } + + const link = ( + + {item.name} + + ); + + return tooltipEnabled === 'true' ? ( + + {link} + + ) : ( + link + ); + }; + + return ( +
+ {items && items.map((item, index) => quickLink(item, index))} +
+ ); +}); + +QuickLinks.displayName = 'QuickLinks'; export { QuickLinks as default, QuickLinks }; \ No newline at end of file diff --git a/src/features/quicklinks/options/QuickLinksOptions.jsx b/src/features/quicklinks/options/QuickLinksOptions.jsx index ff5e31fa..af0c6fb6 100644 --- a/src/features/quicklinks/options/QuickLinksOptions.jsx +++ b/src/features/quicklinks/options/QuickLinksOptions.jsx @@ -1,190 +1,58 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; -import { MdAddLink, MdLinkOff, MdOutlineDragIndicator, MdEdit, MdDelete } from 'react-icons/md'; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - DragOverlay, -} from '@dnd-kit/core'; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; +import { useState, useEffect, useRef } from 'react'; +import { MdAddLink, MdLinkOff } from 'react-icons/md'; +import { arrayMove } from '@dnd-kit/sortable'; import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings'; import { Checkbox, Dropdown } from 'components/Form/Settings'; import { Button } from 'components/Elements'; import Modal from 'react-modal'; import { AddModal } from 'components/Elements/AddModal'; +import { SortableList } from './components'; +import { readQuicklinks } from './utils/quicklinksUtils'; import EventBus from 'utils/eventbus'; import { getTitleFromUrl, isValidUrl } from 'utils/links'; -const readQuicklinks = () => { - try { - const raw = localStorage.getItem('quicklinks'); - if (!raw) return []; - const data = JSON.parse(raw); - return Array.isArray(data) ? data : []; - } catch (e) { - console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e); - return []; - } -}; +const QuickLinksOptions = () => { + const [items, setItems] = useState(readQuicklinks()); + const [showAddModal, setShowAddModal] = useState(false); + const [urlError, setUrlError] = useState(''); + const [iconError, setIconError] = useState(''); + const [edit, setEdit] = useState(false); + const [editData, setEditData] = useState(''); + const [enabled, setEnabled] = useState(localStorage.getItem('quicklinksenabled') !== 'false'); -const DragHandle = () => ( - -); + const quicklinksContainer = useRef(); + const silenceEventRef = useRef(false); -const SortableItem = ({ value, enabled, startEditLink, deleteLink }) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: value.key, - disabled: !enabled, - }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - - const getIconUrl = (item) => { - return ( - item.icon || - 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '') - ); - }; - - return ( -
- -
- {value.name} -
-
-
{value.name}
-
{value.url}
-
-
- - -
-
- ); -}; - -const SortableList = ({ items, enabled, onDragEnd, startEditLink, deleteLink }) => { - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), - useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), - ); - - return ( - - item.key)} strategy={verticalListSortingStrategy}> -
- {items.map((item) => ( - - ))} -
-
-
- ); -}; - -class QuickLinksOptions extends PureComponent { - constructor() { - super(); - this.state = { - items: readQuicklinks(), - showAddModal: false, - urlError: '', - iconError: '', - edit: false, - editData: '', - enabled: localStorage.getItem('quicklinksenabled') !== 'false', - }; - this.quicklinksContainer = createRef(); - this.silenceEvent = false; - this.handleRefresh = null; - } - - setContainerDisplay(enabled) { - if (!this.quicklinksContainer || !this.quicklinksContainer.current) return; - const el = this.quicklinksContainer.current; + const setContainerDisplay = (enabled) => { + if (!quicklinksContainer || !quicklinksContainer.current) return; + const el = quicklinksContainer.current; el.classList.toggle('disabled', !enabled); if (!enabled) { el.setAttribute('aria-hidden', 'true'); } else { el.removeAttribute('aria-hidden'); } - } - deleteLink(key, event) { + }; + + const deleteLink = (key, event) => { event.preventDefault(); const stored = readQuicklinks(); const data = stored.filter((i) => i.key !== key); - this.silenceEvent = true; + silenceEventRef.current = true; localStorage.setItem('quicklinks', JSON.stringify(data)); - this.setState({ items: data }, () => { - variables.stats.postEvent('feature', 'Quicklink delete'); - EventBus.emit('refresh', 'quicklinks'); - setTimeout(() => { - this.silenceEvent = false; - }, 0); - }); - } + setItems(data); + variables.stats.postEvent('feature', 'Quicklink delete'); + EventBus.emit('refresh', 'quicklinks'); + setTimeout(() => { + silenceEventRef.current = false; + }, 0); + }; - async addLink(name, url, icon) { + const addLink = async (name, url, icon) => { const data = readQuicklinks(); if (!url.startsWith('http://') && !url.startsWith('https://')) { @@ -192,11 +60,13 @@ class QuickLinksOptions extends PureComponent { } if (url.length <= 0 || isValidUrl(url) === false) { - return this.setState({ urlError: variables.getMessage('widgets.quicklinks.url_error') }); + setUrlError(variables.getMessage('widgets.quicklinks.url_error')); + return; } if (icon.length > 0 && isValidUrl(icon) === false) { - return this.setState({ iconError: variables.getMessage('widgets.quicklinks.url_error') }); + setIconError(variables.getMessage('widgets.quicklinks.url_error')); + return; } const newItem = { @@ -208,22 +78,26 @@ class QuickLinksOptions extends PureComponent { data.push(newItem); - this.silenceEvent = true; + silenceEventRef.current = true; localStorage.setItem('quicklinks', JSON.stringify(data)); - this.setState({ items: data, showAddModal: false, urlError: '', iconError: '' }, () => { - variables.stats.postEvent('feature', 'Quicklink add'); - EventBus.emit('refresh', 'quicklinks'); - setTimeout(() => { - this.silenceEvent = false; - }, 0); - }); - } + setItems(data); + setShowAddModal(false); + setUrlError(''); + setIconError(''); + variables.stats.postEvent('feature', 'Quicklink add'); + EventBus.emit('refresh', 'quicklinks'); + setTimeout(() => { + silenceEventRef.current = false; + }, 0); + }; - startEditLink(data) { - this.setState({ edit: true, editData: data, showAddModal: true }); - } + const startEditLink = (data) => { + setEdit(true); + setEditData(data); + setShowAddModal(true); + }; - async editLink(og, name, url, icon) { + const editLink = async (og, name, url, icon) => { const data = readQuicklinks(); const dataobj = data.find((i) => i.key === og.key); if (!dataobj) return; @@ -232,233 +106,220 @@ class QuickLinksOptions extends PureComponent { dataobj.url = url; dataobj.icon = icon || ''; - this.silenceEvent = true; + silenceEventRef.current = true; localStorage.setItem('quicklinks', JSON.stringify(data)); - this.setState({ items: data, showAddModal: false, edit: false }, () => { - EventBus.emit('refresh', 'quicklinks'); - setTimeout(() => { - this.silenceEvent = false; - }, 0); - }); - } - - arrayMove = (array, oldIndex, newIndex) => { - const result = Array.from(array); - const [removed] = result.splice(oldIndex, 1); - result.splice(newIndex, 0, removed); - return result; + setItems(data); + setShowAddModal(false); + setEdit(false); + EventBus.emit('refresh', 'quicklinks'); + setTimeout(() => { + silenceEventRef.current = false; + }, 0); }; - handleDragEnd = (event) => { + const handleDragEnd = (event) => { const { active, over } = event; - if (!over || !this.state.enabled) return; + if (!over || !enabled) return; if (active.id === over.id) return; - const oldIndex = this.state.items.findIndex((item) => item.key === active.id); - const newIndex = this.state.items.findIndex((item) => item.key === over.id); + const oldIndex = items.findIndex((item) => item.key === active.id); + const newIndex = items.findIndex((item) => item.key === over.id); if (oldIndex === -1 || newIndex === -1) return; - const newItems = arrayMove(this.state.items, oldIndex, newIndex); + const newItems = arrayMove(items, oldIndex, newIndex); - this.silenceEvent = true; - this.setState({ items: newItems }, () => { - localStorage.setItem('quicklinks', JSON.stringify(newItems)); - EventBus.emit('refresh', 'quicklinks'); - setTimeout(() => { - this.silenceEvent = false; - }, 0); - }); + silenceEventRef.current = true; + setItems(newItems); + localStorage.setItem('quicklinks', JSON.stringify(newItems)); + EventBus.emit('refresh', 'quicklinks'); + setTimeout(() => { + silenceEventRef.current = false; + }, 0); }; - componentDidMount() { - this.setContainerDisplay(this.state.enabled); + useEffect(() => { + setContainerDisplay(enabled); + }, [enabled]); - this.handleRefresh = (data) => { + useEffect(() => { + setContainerDisplay(enabled); + + const handleRefresh = (data) => { if (data !== 'quicklinks') return; - if (this.silenceEvent) return; + if (silenceEventRef.current) return; - const enabled = localStorage.getItem('quicklinksenabled') !== 'false'; + const newEnabled = localStorage.getItem('quicklinksenabled') !== 'false'; const newItems = readQuicklinks(); - const oldItems = this.state.items || []; - const oldKeys = new Set(oldItems.map((i) => i.key)); - const newKeys = new Set(newItems.map((i) => i.key)); + const oldItems = items || []; const keysEqual = - oldItems.length === newItems.length && oldItems.every((i) => newKeys.has(i.key)); + oldItems.length === newItems.length && + oldItems.every((i) => newItems.some((n) => n.key === i.key)); - if (enabled !== this.state.enabled || !keysEqual) { - this.setContainerDisplay(enabled); - this.setState({ items: newItems, enabled }); + if (newEnabled !== enabled || !keysEqual) { + setContainerDisplay(newEnabled); + setItems(newItems); + setEnabled(newEnabled); } }; - EventBus.on('refresh', this.handleRefresh); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh', handleRefresh); + }; + }, [enabled, items]); - componentDidUpdate(prevProps, prevState) { - if (prevState.enabled !== this.state.enabled) { - this.setContainerDisplay(this.state.enabled); - } - } + const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks'; - componentWillUnmount() { - if (this.handleRefresh) { - EventBus.off('refresh', this.handleRefresh); - } else { - try { - EventBus.off('refresh'); - } catch { - // Ignore errors - } - } - } - render() { - const QUICKLINKS_SECTION = 'modals.main.settings.sections.quicklinks'; - const { enabled } = this.state; - - const AdditionalSettings = () => ( - - - - - - - - ); - - const StylingOptions = () => ( - - - - - - - ); - - const AddLink = () => ( - - - -
+ const StylingOptions = () => ( + + + + + + + ); + + const AddLink = () => ( + + + +
- )} - -
-
- this.startEditLink(data)} - deleteLink={(key, e) => this.deleteLink(key, e)} - />
-
- - this.setState({ showAddModal: false, urlError: '', iconError: '' })} - isOpen={this.state.showAddModal} - className="Modal resetmodal mainModal" - overlayClassName="Overlay resetoverlay" - ariaHideApp={false} - > - this.addLink(name, url, icon)} - editLink={(og, name, url, icon) => this.editLink(og, name, url, icon)} - edit={this.state.edit} - editData={this.state.editData} - closeModal={() => - this.setState({ showAddModal: false, urlError: '', iconError: '', edit: false }) - } + )} + +
+
+ startEditLink(data)} + deleteLink={(key, e) => deleteLink(key, e)} /> - - - ); - } -} +
+
+ + { + setShowAddModal(false); + setUrlError(''); + setIconError(''); + }} + isOpen={showAddModal} + className="Modal resetmodal mainModal" + overlayClassName="Overlay resetoverlay" + ariaHideApp={false} + > + addLink(name, url, icon)} + editLink={(og, name, url, icon) => editLink(og, name, url, icon)} + edit={edit} + editData={editData} + closeModal={() => { + setShowAddModal(false); + setUrlError(''); + setIconError(''); + setEdit(false); + }} + /> + + + ); +}; export { QuickLinksOptions as default, QuickLinksOptions }; diff --git a/src/features/quicklinks/options/components/DragHandle.jsx b/src/features/quicklinks/options/components/DragHandle.jsx new file mode 100644 index 00000000..8c8c8c35 --- /dev/null +++ b/src/features/quicklinks/options/components/DragHandle.jsx @@ -0,0 +1,7 @@ +import { MdOutlineDragIndicator } from 'react-icons/md'; + +export const DragHandle = () => ( + +); diff --git a/src/features/quicklinks/options/components/SortableItem.jsx b/src/features/quicklinks/options/components/SortableItem.jsx new file mode 100644 index 00000000..b3caefb9 --- /dev/null +++ b/src/features/quicklinks/options/components/SortableItem.jsx @@ -0,0 +1,74 @@ +import { MdEdit, MdDelete } from 'react-icons/md'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { DragHandle } from './DragHandle'; + +export const SortableItem = ({ value, enabled, startEditLink, deleteLink }) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: value.key, + disabled: !enabled, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + + const getIconUrl = (item) => { + return ( + item.icon || + 'https://icon.horse/icon/' + item.url.replace('https://', '').replace('http://', '') + ); + }; + + return ( +
+ +
+ {value.name} +
+
+
{value.name}
+
{value.url}
+
+
+ + +
+
+ ); +}; diff --git a/src/features/quicklinks/options/components/SortableList.jsx b/src/features/quicklinks/options/components/SortableList.jsx new file mode 100644 index 00000000..4e143c6e --- /dev/null +++ b/src/features/quicklinks/options/components/SortableList.jsx @@ -0,0 +1,39 @@ +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { SortableItem } from './SortableItem'; + +export const SortableList = ({ items, enabled, onDragEnd, startEditLink, deleteLink }) => { + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), + ); + + return ( + + item.key)} strategy={verticalListSortingStrategy}> +
+ {items.map((item) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/features/quicklinks/options/components/index.jsx b/src/features/quicklinks/options/components/index.jsx new file mode 100644 index 00000000..ff5c6a49 --- /dev/null +++ b/src/features/quicklinks/options/components/index.jsx @@ -0,0 +1,3 @@ +export * from './DragHandle'; +export * from './SortableItem'; +export * from './SortableList'; diff --git a/src/features/quicklinks/options/utils/quicklinksUtils.js b/src/features/quicklinks/options/utils/quicklinksUtils.js new file mode 100644 index 00000000..94a4b788 --- /dev/null +++ b/src/features/quicklinks/options/utils/quicklinksUtils.js @@ -0,0 +1,11 @@ +export const readQuicklinks = () => { + try { + const raw = localStorage.getItem('quicklinks'); + if (!raw) return []; + const data = JSON.parse(raw); + return Array.isArray(data) ? data : []; + } catch (e) { + console.warn('Failed to parse quicklinks from localStorage. Resetting to []', e); + return []; + } +}; diff --git a/src/features/quote/options/QuoteOptions.jsx b/src/features/quote/options/QuoteOptions.jsx index 43ebb878..1b447b45 100644 --- a/src/features/quote/options/QuoteOptions.jsx +++ b/src/features/quote/options/QuoteOptions.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote } from 'react-icons/md'; import TextareaAutosize from '@mui/material/TextareaAutosize'; @@ -17,281 +17,272 @@ import { Button } from 'components/Elements'; import { toast } from 'react-toastify'; import EventBus from 'utils/eventbus'; -class QuoteOptions extends PureComponent { - constructor() { - super(); - this.state = { - quoteType: localStorage.getItem('quoteType') || 'api', - customQuote: this.getCustom(), - sourceSection: false, - }; - } - - resetCustom = () => { - localStorage.setItem('customQuote', '[{"quote": "", "author": ""}]'); - this.setState({ customQuote: [{ quote: '', author: '' }] }); - toast(variables.getMessage('toasts.reset')); - EventBus.emit('refresh', 'background'); - }; - - customQuote(e, text, index, type) { - const result = text === true ? e.target.value : e.target.result; - - const customQuote = this.state.customQuote; - customQuote[index][type] = result; - this.setState({ customQuote }); - this.forceUpdate(); - - localStorage.setItem('customQuote', JSON.stringify(customQuote)); - document.querySelector('.reminder-info').style.display = 'flex'; - localStorage.setItem('showReminder', true); - } - - modifyCustomQuote(type, index) { - const customQuote = this.state.customQuote; - if (type === 'add') { - customQuote.push({ quote: '', author: '' }); - } else { - customQuote.splice(index, 1); - } - - this.setState({ customQuote }); - this.forceUpdate(); - - localStorage.setItem('customQuote', JSON.stringify(customQuote)); - } - - getCustom() { +const QuoteOptions = () => { + const getCustom = () => { let data = JSON.parse(localStorage.getItem('customQuote')); if (data === null) { data = []; } return data; - } + }; - render() { - const QUOTE_SECTION = 'modals.main.settings.sections.quote'; + const [quoteType, setQuoteType] = useState(localStorage.getItem('quoteType') || 'api'); + const [customQuote, setCustomQuote] = useState(getCustom()); + const [sourceSection, setSourceSection] = useState(false); - const ButtonOptions = () => { - return ( - - - - - - - - - ); - }; + const resetCustom = () => { + localStorage.setItem('customQuote', '[{"quote": "", "author": ""}]'); + setCustomQuote([{ quote: '', author: '' }]); + toast(variables.getMessage('toasts.reset')); + EventBus.emit('refresh', 'background'); + }; - const SourceDropdown = () => { - return ( - this.setState({ quoteType: value })} - category="quote" - items={[ - localStorage.getItem('quote_packs') && { - value: 'quote_pack', - text: variables.getMessage('modals.main.navbar.marketplace'), - }, - { - value: 'api', - text: variables.getMessage('modals.main.settings.sections.background.type.api'), - }, - { value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) }, - ]} + const handleCustomQuote = (e, text, index, type) => { + const result = text === true ? e.target.value : e.target.result; + + const updatedCustomQuote = [...customQuote]; + updatedCustomQuote[index][type] = result; + setCustomQuote(updatedCustomQuote); + + localStorage.setItem('customQuote', JSON.stringify(updatedCustomQuote)); + document.querySelector('.reminder-info').style.display = 'flex'; + localStorage.setItem('showReminder', true); + }; + + const modifyCustomQuote = (type, index) => { + const updatedCustomQuote = [...customQuote]; + if (type === 'add') { + updatedCustomQuote.push({ quote: '', author: '' }); + } else { + updatedCustomQuote.splice(index, 1); + } + + setCustomQuote(updatedCustomQuote); + + localStorage.setItem('customQuote', JSON.stringify(updatedCustomQuote)); + }; + + const QUOTE_SECTION = 'modals.main.settings.sections.quote'; + + const ButtonOptions = () => { + return ( + + - ); - }; + + + + + + + ); + }; - const AdditionalOptions = () => { - return ( + const SourceDropdown = () => { + return ( + setQuoteType(value)} + category="quote" + items={[ + localStorage.getItem('quote_packs') && { + value: 'quote_pack', + text: variables.getMessage('modals.main.navbar.marketplace'), + }, + { + value: 'api', + text: variables.getMessage('modals.main.settings.sections.background.type.api'), + }, + { value: 'custom', text: variables.getMessage(`${QUOTE_SECTION}.custom`) }, + ]} + /> + ); + }; + + const AdditionalOptions = () => { + return ( + + + + + + + + + ); + }; + + let customSettings; + if (quoteType === 'custom' && sourceSection === true) { + customSettings = ( + <> - - - modifyCustomQuote('add')} + icon={} + label={variables.getMessage(`${QUOTE_SECTION}.add`)} /> - ); - }; - let customSettings; - if (this.state.quoteType === 'custom' && this.state.sourceSection === true) { - customSettings = ( - <> - - - + {customQuote.length !== 0 ? ( +
+ {customQuote.map((_url, index) => ( +
+
+ +
+
+ handleCustomQuote(e, true, index, 'quote')} + varient="outlined" + style={{ fontSize: '22px', fontWeight: 'bold' }} + /> + handleCustomQuote(e, true, index, 'author')} + varient="outlined" + /> +
+
+
+
+
+
+ ))} +
+ ) : ( +
+
+ + {variables.getMessage(`${QUOTE_SECTION}.no_quotes`)} + + {variables.getMessage('modals.main.settings.sections.message.add_some')} +
-
-
- ))}
- ) : ( -
-
- - {variables.getMessage(`${QUOTE_SECTION}.no_quotes`)} - - {variables.getMessage('modals.main.settings.sections.message.add_some')} - -
-
- )} - - ); - } else { - // api - customSettings = <>; - } - - return ( - <> - {this.state.sourceSection ? ( -
this.setState({ sourceSection: false })} - report={false} - /> - ) : ( -
+
)} - {this.state.sourceSection && ( - - - - - - - )} - {!this.state.sourceSection && ( - -
} - title={variables.getMessage('modals.main.settings.sections.background.source.title')} - subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)} - onClick={() => this.setState({ sourceSection: true })} - > - -
- - -
- )} - {customSettings} ); + } else { + // api + customSettings = <>; } -} + + return ( + <> + {sourceSection ? ( +
setSourceSection(false)} + report={false} + /> + ) : ( +
+ )} + {sourceSection && ( + + + + + + + )} + {!sourceSection && ( + +
} + title={variables.getMessage('modals.main.settings.sections.background.source.title')} + subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)} + onClick={() => setSourceSection(true)} + > + +
+ + +
+ )} + {customSettings} + + ); +}; export { QuoteOptions as default, QuoteOptions }; diff --git a/src/features/search/components/autocomplete/Autocomplete.jsx b/src/features/search/components/autocomplete/Autocomplete.jsx index e6d40753..e063fc8f 100644 --- a/src/features/search/components/autocomplete/Autocomplete.jsx +++ b/src/features/search/components/autocomplete/Autocomplete.jsx @@ -1,43 +1,37 @@ -import { PureComponent } from 'react'; +import { useState, useEffect } from 'react'; import EventBus from 'utils/eventbus'; import './autocomplete.scss'; -class Autocomplete extends PureComponent { - constructor(props) { - super(props); - this.state = { - filtered: [], - input: '', - autocompleteDisabled: localStorage.getItem('autocomplete') !== 'true', - }; - } +const Autocomplete = ({ suggestions, onChange: onChangeCallback, onClick: onClickCallback, placeholder, id }) => { + const [filtered, setFiltered] = useState([]); + const [input, setInput] = useState(''); + const [autocompleteDisabled, setAutocompleteDisabled] = useState( + localStorage.getItem('autocomplete') !== 'true', + ); - onChange = (e) => { - if (this.state.autocompleteDisabled) { - return this.setState({ - input: e.target.value, - }); + const onChange = (e) => { + if (autocompleteDisabled) { + setInput(e.target.value); + return; } - this.setState({ - filtered: this.props.suggestions.filter( + setFiltered( + suggestions.filter( (suggestion) => suggestion.toLowerCase().indexOf(e.target.value.toLowerCase()) > -1, ), - input: e.target.value, - }); + ); + setInput(e.target.value); - this.props.onChange(e.target.value); + onChangeCallback(e.target.value); }; - onClick = (e) => { - this.setState({ - filtered: [], - input: e.target.innerText, - }); + const onClick = (e) => { + setFiltered([]); + setInput(e.target.innerText); - this.props.onClick({ + onClickCallback({ preventDefault: () => e.preventDefault(), target: { value: e.target.innerText, @@ -45,51 +39,48 @@ class Autocomplete extends PureComponent { }); }; - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'search') { - this.setState({ - autocompleteDisabled: localStorage.getItem('autocomplete') !== 'true', - }); + setAutocompleteDisabled(localStorage.getItem('autocomplete') !== 'true'); } - }); - } + }; - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + }; + }, []); - render() { - let autocomplete = null; + let autocomplete = null; - // length will only be > 0 if enabled - if (this.state.filtered.length > 0 && this.state.input.length > 0) { - autocomplete = ( -
- {this.state.filtered.map((suggestion) => ( -
- {suggestion} -
- ))} -
- ); - } - - return ( -
- - {autocomplete} + // length will only be > 0 if enabled + if (filtered.length > 0 && input.length > 0) { + autocomplete = ( +
+ {filtered.map((suggestion) => ( +
+ {suggestion} +
+ ))}
); } -} + + return ( +
+ + {autocomplete} +
+ ); +}; export { Autocomplete as default, Autocomplete }; diff --git a/src/features/search/options/SearchOptions.jsx b/src/features/search/options/SearchOptions.jsx index 77b545fb..cb4a878a 100644 --- a/src/features/search/options/SearchOptions.jsx +++ b/src/features/search/options/SearchOptions.jsx @@ -1,5 +1,4 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; import { MdOutlineWarning } from 'react-icons/md'; import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings'; @@ -7,65 +6,62 @@ import { Checkbox } from 'components/Form/Settings'; import EventBus from 'utils/eventbus'; -class SearchOptions extends PureComponent { - - render() { - const SEARCH_SECTION = 'modals.main.settings.sections.search'; - - const ChromePolicyWarning = () => { - return ( -
- -
- Search Engine Selection Removed - {variables.getMessage(`${SEARCH_SECTION}.chrome_policy_warning`)} -
-
- ); - }; - - const AdditionalOptions = () => { - return ( - - - - {/* not supported on firefox */} - {navigator.userAgent.includes('Chrome') && typeof InstallTrigger === 'undefined' ? ( - - ) : null} - - - - ); - }; +const SearchOptions = () => { + const SEARCH_SECTION = 'modals.main.settings.sections.search'; + const ChromePolicyWarning = () => { return ( - <> -
- - - - - +
+ +
+ Search Engine Selection Removed + {variables.getMessage(`${SEARCH_SECTION}.chrome_policy_warning`)} +
+
); - } -} + }; + + const AdditionalOptions = () => { + return ( + + + + {/* not supported on firefox */} + {navigator.userAgent.includes('Chrome') && typeof InstallTrigger === 'undefined' ? ( + + ) : null} + + + + ); + }; + + return ( + <> +
+ + + + + + ); +}; export { SearchOptions as default, SearchOptions }; diff --git a/src/features/time/Clock.jsx b/src/features/time/Clock.jsx index 36a96334..04694305 100644 --- a/src/features/time/Clock.jsx +++ b/src/features/time/Clock.jsx @@ -1,4 +1,4 @@ -import { PureComponent } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { convertTimezone } from 'utils/date'; import { AnalogClock } from './components/AnalogClock'; @@ -6,29 +6,25 @@ import { VerticalClock } from './components/VerticalClock'; import EventBus from 'utils/eventbus'; import './clock.scss'; -export default class Clock extends PureComponent { - constructor() { - super(); - this.timer = undefined; - this.state = { - timeType: localStorage.getItem('timeType'), - time: '', - finalHour: '', - finalMinute: '', - finalSeconds: '', - ampm: '', - nowGlobal: new Date(), - }; - } +const Clock = () => { + const [timeType] = useState(localStorage.getItem('timeType')); + const [time, setTime] = useState(''); + const [finalHour, setFinalHour] = useState(''); + const [finalMinute, setFinalMinute] = useState(''); + const [finalSeconds, setFinalSeconds] = useState(''); + const [ampm, setAmpm] = useState(''); + const [display, setDisplay] = useState('block'); + const [fontSize, setFontSize] = useState('4em'); + const timerRef = useRef(undefined); - startTime( + const startTime = ( time = localStorage.getItem('seconds') === 'true' || localStorage.getItem('timeType') === 'analogue' ? 1000 - (Date.now() % 1000) : 60000 - (Date.now() % 60000), - ) { - this.timer = setTimeout(() => { + ) => { + timerRef.current = setTimeout(() => { let now = new Date(); const timezone = localStorage.getItem('timezone'); if (timezone && timezone !== 'auto') { @@ -37,16 +33,14 @@ export default class Clock extends PureComponent { switch (localStorage.getItem('timeType')) { case 'percentageComplete': - this.setState({ - time: (now.getHours() / 24).toFixed(2).replace('0.', '') + '%', - ampm: '', - }); + setTime((now.getHours() / 24).toFixed(2).replace('0.', '') + '%'); + setAmpm(''); break; case 'analogue': // load analog clock css import('react-clock/dist/Clock.css'); - this.setState({ time: now }); + setTime(now); break; default: { // Default clock @@ -56,27 +50,24 @@ export default class Clock extends PureComponent { if (localStorage.getItem('seconds') === 'true') { sec = `:${('00' + now.getSeconds()).slice(-2)}`; - this.setState({ finalSeconds: `${('00' + now.getSeconds()).slice(-2)}` }); + setFinalSeconds(`${('00' + now.getSeconds()).slice(-2)}`); } if (localStorage.getItem('timeformat') === 'twentyfourhour') { if (zero === 'false') { time = `${now.getHours()}:${('00' + now.getMinutes()).slice(-2)}:${sec}`; - this.setState({ - finalHour: `${now.getHours()}`, - finalMinute: `${('00' + now.getMinutes()).slice(-2)}`, - }); + setFinalHour(`${now.getHours()}`); + setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`); } else { time = `${('00' + now.getHours()).slice(-2)}:${('00' + now.getMinutes()).slice( -2, )}${sec}`; - this.setState({ - finalHour: `${('00' + now.getHours()).slice(-2)}`, - finalMinute: `${('00' + now.getMinutes()).slice(-2)}`, - }); + setFinalHour(`${('00' + now.getHours()).slice(-2)}`); + setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`); } - this.setState({ time, ampm: '' }); + setTime(time); + setAmpm(''); } else { // 12 hour let hours = now.getHours(); @@ -89,80 +80,76 @@ export default class Clock extends PureComponent { if (zero === 'false') { time = `${hours}:${('00' + now.getMinutes()).slice(-2)}${sec}`; - this.setState({ - finalHour: `${hours}`, - finalMinute: `${('00' + now.getMinutes()).slice(-2)}`, - }); + setFinalHour(`${hours}`); + setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`); } else { time = `${('00' + hours).slice(-2)}:${('00' + now.getMinutes()).slice(-2)}${sec}`; - this.setState({ - finalHour: `${('00' + hours).slice(-2)}`, - finalMinute: `${('00' + now.getMinutes()).slice(-2)}`, - }); + setFinalHour(`${('00' + hours).slice(-2)}`); + setFinalMinute(`${('00' + now.getMinutes()).slice(-2)}`); } - this.setState({ time, ampm: now.getHours() > 11 ? 'PM' : 'AM' }); + setTime(time); + setAmpm(now.getHours() > 11 ? 'PM' : 'AM'); } break; } } - this.startTime(); + startTime(); }, time); - } + }; - componentDidMount() { - EventBus.on('refresh', (data) => { + useEffect(() => { + const handleRefresh = (data) => { if (data === 'clock' || data === 'timezone') { - const element = document.querySelector('.clock-container'); - if (localStorage.getItem('time') === 'false') { - return (element.style.display = 'none'); + setDisplay('none'); + return; } - this.timer = null; - this.startTime(0); + timerRef.current = null; + startTime(0); - element.style.display = 'block'; - element.style.fontSize = `${ - 4 * Number((localStorage.getItem('zoomClock') || 100) / 100) - }em`; + setDisplay('block'); + setFontSize(`${4 * Number((localStorage.getItem('zoomClock') || 100) / 100)}em`); } - }); + }; if (localStorage.getItem('timeType') !== 'analogue') { - document.querySelector('.clock-container').style.fontSize = `${ - 4 * Number((localStorage.getItem('zoomClock') || 100) / 100) - }em`; + setFontSize(`${4 * Number((localStorage.getItem('zoomClock') || 100) / 100)}em`); } - this.startTime(0); + startTime(0); + + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); + + if (localStorage.getItem('timeType') === 'analogue') { + return ; } - componentWillUnmount() { - EventBus.off('refresh'); - } - - render() { - if (localStorage.getItem('timeType') === 'analogue') { - return ; - } - - if (localStorage.getItem('timeType') === 'verticalClock') { - return ( - - ); - } - + if (localStorage.getItem('timeType') === 'verticalClock') { return ( - - {this.state.time} - {this.state.ampm} - + ); } -} + + return ( + + {time} + {ampm} + + ); +}; + +export { Clock as default, Clock }; diff --git a/src/features/time/Date.jsx b/src/features/time/Date.jsx index 0cc764b1..b0fe4678 100644 --- a/src/features/time/Date.jsx +++ b/src/features/time/Date.jsx @@ -1,26 +1,23 @@ import variables from 'config/variables'; -import { PureComponent, createRef } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { nth, convertTimezone } from 'utils/date'; import EventBus from 'utils/eventbus'; import './date.scss'; -export default class DateWidget extends PureComponent { - constructor() { - super(); - this.state = { - date: '', - weekNumber: null, - }; - this.date = createRef(); - } +const DateWidget = () => { + const [date, setDate] = useState(''); + const [weekNumber, setWeekNumber] = useState(null); + const [display, setDisplay] = useState('block'); + const [fontSize, setFontSize] = useState('1em'); + const dateRef = useRef(); /** * Get the week number of the year for the given date. * @param {Date} date */ - getWeekNumber(date) { + const getWeekNumber = (date) => { const dateToday = new Date(date.valueOf()); const dayNumber = (dateToday.getDay() + 6) % 7; @@ -32,39 +29,37 @@ export default class DateWidget extends PureComponent { dateToday.setMonth(0, 1 + ((4 - dateToday.getDay() + 7) % 7)); } - this.setState({ - weekNumber: `${variables.getMessage('widgets.date.week')} ${ + setWeekNumber( + `${variables.getMessage('widgets.date.week')} ${ 1 + Math.ceil((firstThursday - dateToday) / 604800000) }`, - }); - } + ); + }; - getDate() { + const getDate = () => { let date = new Date(); const timezone = localStorage.getItem('timezone'); if (timezone && timezone !== 'auto') { date = convertTimezone(date, timezone); } - + if (localStorage.getItem('weeknumber') === 'true') { - this.getWeekNumber(date); - } else if (this.state.weekNumber !== null) { - this.setState({ - weekNumber: null, - }); + getWeekNumber(date); + } else if (weekNumber !== null) { + setWeekNumber(null); } - + if (localStorage.getItem('dateType') === 'short') { const dateDay = date.getDate(); const dateMonth = date.getMonth() + 1; const dateYear = date.getFullYear(); - + const zero = localStorage.getItem('datezero') === 'true'; - + let day = zero ? ('00' + dateDay).slice(-2) : dateDay; let month = zero ? ('00' + dateMonth).slice(-2) : dateMonth; let year = dateYear; - + switch (localStorage.getItem('dateFormat')) { case 'MDY': day = dateMonth; @@ -78,7 +73,7 @@ export default class DateWidget extends PureComponent { default: break; } - + let format; switch (localStorage.getItem('shortFormat')) { case 'dots': @@ -96,22 +91,22 @@ export default class DateWidget extends PureComponent { default: break; } - - this.setState({ - date: format, - }); + + setDate(format); } else { // Long date const lang = variables.languagecode.split('_')[0]; - const datenth = localStorage.getItem('datenth') === 'true' ? nth(date.getDate()) : date.getDate(); - const dateDay = localStorage.getItem('dayofweek') === 'true' - ? date.toLocaleDateString(lang, { weekday: 'long' }) - : ''; + const datenth = + localStorage.getItem('datenth') === 'true' ? nth(date.getDate()) : date.getDate(); + const dateDay = + localStorage.getItem('dayofweek') === 'true' + ? date.toLocaleDateString(lang, { weekday: 'long' }) + : ''; const dateMonth = date.toLocaleDateString(lang, { month: 'long' }); const dateYear = date.getFullYear(); - + let formattedDate; - + switch (localStorage.getItem('longFormat')) { case 'MDY': formattedDate = `${dateMonth} ${datenth}, ${dateYear}${dateDay ? `, ${dateDay}` : ''}`; @@ -124,45 +119,41 @@ export default class DateWidget extends PureComponent { formattedDate = `${datenth} ${dateMonth} ${dateYear}${dateDay ? `, ${dateDay}` : ''}`; break; } - - this.setState({ - date: formattedDate, - }); + + setDate(formattedDate); } - } - - componentDidMount() { - EventBus.on('refresh', (data) => { + }; + + useEffect(() => { + const handleRefresh = (data) => { if (data === 'date' || data === 'timezone') { if (localStorage.getItem('date') === 'false') { - return (this.date.current.style.display = 'none'); + setDisplay('none'); + return; } - this.date.current.style.display = 'block'; - this.date.current.style.fontSize = `${Number( - (localStorage.getItem('zoomDate') || 100) / 100, - )}em`; - this.getDate(); + setDisplay('block'); + setFontSize(`${Number((localStorage.getItem('zoomDate') || 100) / 100)}em`); + getDate(); } - }); + }; - this.date.current.style.fontSize = `${Number( - (localStorage.getItem('zoomDate') || 100) / 100, - )}em`; - this.getDate(); - } + setFontSize(`${Number((localStorage.getItem('zoomDate') || 100) / 100)}em`); + getDate(); - componentWillUnmount() { - EventBus.off('refresh'); - } + EventBus.on('refresh', handleRefresh); + return () => { + EventBus.off('refresh'); + }; + }, []); - render() { - return ( - - {this.state.date} -
- {this.state.weekNumber} -
- ); - } -} + return ( + + {date} +
+ {weekNumber} +
+ ); +}; + +export { DateWidget as default, DateWidget }; diff --git a/src/features/weather/Weather.jsx b/src/features/weather/Weather.jsx index 20336c7b..d072fac5 100644 --- a/src/features/weather/Weather.jsx +++ b/src/features/weather/Weather.jsx @@ -1,5 +1,5 @@ import variables from 'config/variables'; -import { PureComponent } from 'react'; +import { memo, useState, useEffect, useCallback } from 'react'; import WeatherIcon from './components/WeatherIcon'; import Expanded from './components/Expanded'; @@ -11,85 +11,85 @@ import { getWeather } from './api/getWeather.js'; import './weather.scss'; -class WeatherWidget extends PureComponent { - constructor() { - super(); - this.state = { - location: localStorage.getItem('location') || 'London', - done: false, - }; - } +const WeatherWidget = memo(() => { + const [location, setLocation] = useState(localStorage.getItem('location') || 'London'); + const [done, setDone] = useState(false); + const [weatherData, setWeatherData] = useState({}); - async componentDidMount() { - EventBus.on('refresh', async (data) => { - if (data === 'weather') { - const weatherData = await getWeather(this.state.location, this.state.done); - this.setState(weatherData); - - const zoomWeather = `${Number((localStorage.getItem('zoomWeather') || 100) / 100)}em`; - document.querySelector('.weather').style.fontSize = zoomWeather; - } - }); - - const weatherData = await getWeather(this.state.location, this.state.done); - this.setState(weatherData); + const updateWeather = useCallback(async () => { + const data = await getWeather(location, done); + setWeatherData(data); + setDone(data.done); const zoomWeather = `${Number((localStorage.getItem('zoomWeather') || 100) / 100)}em`; - document.querySelector('.weather').style.fontSize = zoomWeather; + const weatherElement = document.querySelector('.weather'); + if (weatherElement) { + weatherElement.style.fontSize = zoomWeather; + } + }, [location, done]); + + useEffect(() => { + const handleRefresh = async (data) => { + if (data === 'weather') { + await updateWeather(); + } + }; + + EventBus.on('refresh', handleRefresh); + updateWeather(); + + return () => { + EventBus.off('refresh', handleRefresh); + }; + }, [updateWeather]); + + const weatherType = localStorage.getItem('weatherType') || 1; + + if (done === false) { + return ; } - componentWillUnmount() { - EventBus.off('refresh'); - } - - render() { - const weatherType = localStorage.getItem('weatherType') || 1; - - if (this.state.done === false) { - return ; - } - - if (!this.state.weather) { - return ( -
- {this.state.location} -
- ); - } - + if (!weatherData.weather) { return (
- {/*{this.state.done === false ?

cheese

:

loading finished

}*/} -
-
-
- - {`${this.state.weather.temp}${this.state.temp_text}`} -
- {weatherType >= 2 && ( - - {`${this.state.weather.temp_min}${this.state.temp_text}`} - {`${this.state.weather.temp_max}${this.state.temp_text}`} - - )} -
- {weatherType >= 2 && ( -
- - {variables.getMessage('widgets.weather.feels_like', { - amount: `${this.state.weather.feels_like}${this.state.temp_text}`, - })} - - {this.state.location} -
- )} -
- {weatherType >= 3 && ( - - )} + {location}
); } -} + + return ( +
+
+
+
+ + {`${weatherData.weather.temp}${weatherData.temp_text}`} +
+ {weatherType >= 2 && ( + + {`${weatherData.weather.temp_min}${weatherData.temp_text}`} + {`${weatherData.weather.temp_max}${weatherData.temp_text}`} + + )} +
+ {weatherType >= 2 && ( +
+ + {variables.getMessage('widgets.weather.feels_like', { + amount: `${weatherData.weather.feels_like}${weatherData.temp_text}`, + })} + + {location} +
+ )} +
+ {weatherType >= 3 && ( + + )} +
+ ); +}); + +WeatherWidget.displayName = 'WeatherWidget'; export { WeatherWidget as default, WeatherWidget };