|
| 1 | +/* |
| 2 | + * Copyright (c) 2024. Devtron Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +import { useEffect, useState } from 'react' |
| 18 | + |
| 19 | +import { updateUserPreferences } from '@Shared/Hooks' |
| 20 | +import { |
| 21 | + AppThemeType, |
| 22 | + getComponentSpecificThemeClass, |
| 23 | + getThemePreferenceText, |
| 24 | + THEME_PREFERENCE_STORAGE_KEY, |
| 25 | + useTheme, |
| 26 | +} from '@Shared/Providers' |
| 27 | + |
| 28 | +import { ConfirmationModal, ConfirmationModalVariantType } from '../ConfirmationModal' |
| 29 | +import { Icon } from '../Icon' |
| 30 | +import { |
| 31 | + BaseLabelFigureProps, |
| 32 | + SwitchThemeDialogProps, |
| 33 | + ThemePreferenceLabelFigureProps, |
| 34 | + ThemePreferenceOptionProps, |
| 35 | +} from './types' |
| 36 | + |
| 37 | +import './SwitchThemeDialog.scss' |
| 38 | + |
| 39 | +const THEME_PREFERENCE_OPTION_MAP: Record<ThemePreferenceOptionProps['value'], null> = { |
| 40 | + [AppThemeType.light]: null, |
| 41 | + [AppThemeType.dark]: null, |
| 42 | + auto: null, |
| 43 | +} |
| 44 | + |
| 45 | +const THEME_PREFERENCE_OPTION_LIST: ThemePreferenceOptionProps['value'][] = Object.keys( |
| 46 | + THEME_PREFERENCE_OPTION_MAP, |
| 47 | +) as ThemePreferenceOptionProps['value'][] |
| 48 | + |
| 49 | +const BaseLabelFigure = ({ isSelected, value, noLeftRadius = false }: BaseLabelFigureProps) => ( |
| 50 | + <div |
| 51 | + className={`${isSelected ? 'br-8' : 'br-12'} ${noLeftRadius ? 'dc__no-left-radius' : ''} ${getComponentSpecificThemeClass(value)} h-100 pt-16 pl-16 border__secondary-translucent bg__tertiary dc__overflow-hidden`} |
| 52 | + > |
| 53 | + <div className="py-8 px-16 bg__primary border__primary--top border__primary--left dc__top-left-radius-8 h-100"> |
| 54 | + <span className="cn-9 fs-24 fw-6 lh-36">Aa</span> |
| 55 | + </div> |
| 56 | + </div> |
| 57 | +) |
| 58 | + |
| 59 | +const ThemePreferenceLabelFigure = ({ value, isSelected }: ThemePreferenceLabelFigureProps) => { |
| 60 | + switch (value) { |
| 61 | + case AppThemeType.light: |
| 62 | + case AppThemeType.dark: |
| 63 | + return <BaseLabelFigure isSelected={isSelected} value={value} /> |
| 64 | + case 'auto': |
| 65 | + return ( |
| 66 | + <div className="theme-preference-option__auto-figure dc__grid h-100"> |
| 67 | + <div className="theme-preference-option__auto-figure--light"> |
| 68 | + <BaseLabelFigure isSelected={isSelected} value={AppThemeType.light} /> |
| 69 | + </div> |
| 70 | + |
| 71 | + <div className="theme-preference-option__auto-figure--dark dc__zi-1"> |
| 72 | + <BaseLabelFigure isSelected={isSelected} value={AppThemeType.dark} noLeftRadius /> |
| 73 | + </div> |
| 74 | + </div> |
| 75 | + ) |
| 76 | + default: |
| 77 | + return null |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +const ThemePreferenceOption = ({ |
| 82 | + selectedThemePreference, |
| 83 | + value, |
| 84 | + handleChangedThemePreference, |
| 85 | +}: ThemePreferenceOptionProps) => { |
| 86 | + const handleChange = () => { |
| 87 | + handleChangedThemePreference(value) |
| 88 | + } |
| 89 | + |
| 90 | + const inputId = `theme-preference-option__input-${value}` |
| 91 | + const isSelected = value === selectedThemePreference |
| 92 | + |
| 93 | + return ( |
| 94 | + <div> |
| 95 | + <input |
| 96 | + type="radio" |
| 97 | + id={inputId} |
| 98 | + name="theme-preference-option-input" |
| 99 | + value={value} |
| 100 | + checked={isSelected} |
| 101 | + onChange={handleChange} |
| 102 | + // eslint-disable-next-line jsx-a11y/no-autofocus |
| 103 | + autoFocus={isSelected} |
| 104 | + className="theme-preference-option__input m-0 dc__position-abs dc__opacity-0 dc__disable-click" |
| 105 | + /> |
| 106 | + |
| 107 | + <label htmlFor={inputId} className="m-0 cursor w-100"> |
| 108 | + <div className="flexbox-col dc__gap-6 w-100"> |
| 109 | + <div className="h-100px-imp w-100"> |
| 110 | + <div |
| 111 | + className={`br-12 h-100 theme-preference-option__label-container ${isSelected ? 'eb-5 bw-2 p-4' : 'theme-preference-option__label-container--hover'}`} |
| 112 | + > |
| 113 | + <ThemePreferenceLabelFigure value={value} isSelected={isSelected} /> |
| 114 | + </div> |
| 115 | + </div> |
| 116 | + |
| 117 | + <span className={`${isSelected ? 'cb-5' : 'cn-9'} fs-13 fw-6 lh-20`}> |
| 118 | + {getThemePreferenceText(value)} |
| 119 | + </span> |
| 120 | + </div> |
| 121 | + </label> |
| 122 | + </div> |
| 123 | + ) |
| 124 | +} |
| 125 | + |
| 126 | +const SwitchThemeDialog = ({ |
| 127 | + initialThemePreference, |
| 128 | + handleClose, |
| 129 | + handleUpdateUserThemePreference, |
| 130 | + disableAPICalls = false, |
| 131 | +}: SwitchThemeDialogProps) => { |
| 132 | + const { handleShowSwitchThemeLocationTippyChange, handleThemePreferenceChange, appTheme } = useTheme() |
| 133 | + const [themePreference, setThemePreference] = useState<typeof initialThemePreference>( |
| 134 | + !initialThemePreference ? 'auto' : initialThemePreference, |
| 135 | + ) |
| 136 | + const [isSaving, setIsSaving] = useState<boolean>(false) |
| 137 | + |
| 138 | + const handleSuccess = (updatedThemePreference: typeof themePreference = themePreference) => { |
| 139 | + handleShowSwitchThemeLocationTippyChange(!initialThemePreference) |
| 140 | + handleUpdateUserThemePreference(updatedThemePreference) |
| 141 | + handleThemePreferenceChange(updatedThemePreference) |
| 142 | + handleClose() |
| 143 | + } |
| 144 | + |
| 145 | + useEffect(() => { |
| 146 | + // Watching every 10s local storage for theme preference, if present in localStorage and no initial theme preference is provided would close the modal |
| 147 | + let interval: ReturnType<typeof setInterval> | null = null |
| 148 | + |
| 149 | + if (!initialThemePreference) { |
| 150 | + interval = setInterval(() => { |
| 151 | + const currentThemePreference = localStorage.getItem( |
| 152 | + THEME_PREFERENCE_STORAGE_KEY, |
| 153 | + ) as typeof themePreference |
| 154 | + |
| 155 | + if (currentThemePreference) { |
| 156 | + handleSuccess(currentThemePreference) |
| 157 | + } |
| 158 | + }, 10000) |
| 159 | + } |
| 160 | + |
| 161 | + return () => { |
| 162 | + clearInterval(interval) |
| 163 | + } |
| 164 | + }, []) |
| 165 | + |
| 166 | + const handleSaveThemePreference = async () => { |
| 167 | + setIsSaving(true) |
| 168 | + |
| 169 | + if (!disableAPICalls) { |
| 170 | + const isSuccessful = await updateUserPreferences({ |
| 171 | + path: 'themePreference', |
| 172 | + value: { themePreference, appTheme }, |
| 173 | + }) |
| 174 | + |
| 175 | + if (isSuccessful) { |
| 176 | + handleSuccess() |
| 177 | + } |
| 178 | + } else { |
| 179 | + handleSuccess() |
| 180 | + } |
| 181 | + setIsSaving(false) |
| 182 | + } |
| 183 | + |
| 184 | + const handleChangedThemePreference: ThemePreferenceOptionProps['handleChangedThemePreference'] = (value) => { |
| 185 | + handleThemePreferenceChange(value, true) |
| 186 | + setThemePreference(value) |
| 187 | + } |
| 188 | + |
| 189 | + const handleCloseModal = () => { |
| 190 | + handleThemePreferenceChange(initialThemePreference, true) |
| 191 | + handleClose() |
| 192 | + } |
| 193 | + |
| 194 | + return ( |
| 195 | + <ConfirmationModal |
| 196 | + title="Customize your theme" |
| 197 | + subtitle="Select a theme that suits your preference" |
| 198 | + variant={ConfirmationModalVariantType.custom} |
| 199 | + handleClose={!initialThemePreference ? null : handleCloseModal} |
| 200 | + shouldCloseOnEscape={!!initialThemePreference} |
| 201 | + Icon={<Icon name="ic-medium-paintbucket" color={null} size={48} />} |
| 202 | + buttonConfig={{ |
| 203 | + primaryButtonConfig: { |
| 204 | + isLoading: isSaving, |
| 205 | + text: 'Save Preference', |
| 206 | + onClick: handleSaveThemePreference, |
| 207 | + disabled: !themePreference, |
| 208 | + }, |
| 209 | + }} |
| 210 | + isLandscapeView |
| 211 | + > |
| 212 | + <div className="dc__grid dc__column-gap-16 theme-preference-option__container"> |
| 213 | + {THEME_PREFERENCE_OPTION_LIST.map((value) => ( |
| 214 | + <ThemePreferenceOption |
| 215 | + key={value} |
| 216 | + value={value} |
| 217 | + selectedThemePreference={themePreference} |
| 218 | + handleChangedThemePreference={handleChangedThemePreference} |
| 219 | + /> |
| 220 | + ))} |
| 221 | + </div> |
| 222 | + </ConfirmationModal> |
| 223 | + ) |
| 224 | +} |
| 225 | + |
| 226 | +export default SwitchThemeDialog |
0 commit comments