From a297737ca32555d5b4c5ffbcf656b20ac0303ccb Mon Sep 17 00:00:00 2001 From: devrk96 Date: Tue, 22 Sep 2020 17:26:11 +0530 Subject: [PATCH] Feature: Theme switching between light and dark mode (#578) * Theme switching feature integrated * Condition removed * menu height bugs fixed * warnings removed --- app/client/src/assets/icons/ads/logout.svg | 15 ++ .../src/components/ads/EditableText.tsx | 2 +- app/client/src/components/ads/Icon.tsx | 5 + app/client/src/components/ads/Menu.tsx | 1 - app/client/src/components/ads/MenuItem.tsx | 11 +- .../components/ads/RectangularSwitcher.tsx | 129 ++++++++++++++++++ app/client/src/components/ads/TextInput.tsx | 6 +- app/client/src/pages/common/PageHeader.tsx | 11 +- .../src/pages/common/ProfileDropdown.tsx | 67 +++++++++ app/client/src/pages/common/ThemeSwitcher.tsx | 33 +++-- .../src/reducers/uiReducers/themeReducer.ts | 2 +- app/client/src/sagas/ThemeSaga.tsx | 11 ++ app/client/src/sagas/index.tsx | 2 + 13 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 app/client/src/assets/icons/ads/logout.svg create mode 100644 app/client/src/components/ads/RectangularSwitcher.tsx create mode 100644 app/client/src/pages/common/ProfileDropdown.tsx create mode 100644 app/client/src/sagas/ThemeSaga.tsx diff --git a/app/client/src/assets/icons/ads/logout.svg b/app/client/src/assets/icons/ads/logout.svg new file mode 100644 index 0000000000..f301b2b502 --- /dev/null +++ b/app/client/src/assets/icons/ads/logout.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/app/client/src/components/ads/EditableText.tsx b/app/client/src/components/ads/EditableText.tsx index 5632662b38..0b855cc4ca 100644 --- a/app/client/src/components/ads/EditableText.tsx +++ b/app/client/src/components/ads/EditableText.tsx @@ -154,7 +154,7 @@ export const EditableText = (props: EditableTextProps) => { const bgColor = useMemo( () => editModeBgcolor(!!isInvalid, isEditing, savingState, themeDetails.theme), - [isInvalid, isEditing, savingState], + [isInvalid, isEditing, savingState, themeDetails], ); const editMode = useCallback( diff --git a/app/client/src/components/ads/Icon.tsx b/app/client/src/components/ads/Icon.tsx index dbefb9df31..7550f46194 100644 --- a/app/client/src/components/ads/Icon.tsx +++ b/app/client/src/components/ads/Icon.tsx @@ -17,6 +17,7 @@ import { ReactComponent as InviteUserIcon } from "assets/icons/ads/invite-users. import { ReactComponent as ViewAllIcon } from "assets/icons/ads/view-all.svg"; import { ReactComponent as ContextMenuIcon } from "assets/icons/ads/context-menu.svg"; import { ReactComponent as DuplicateIcon } from "assets/icons/ads/duplicate.svg"; +import { ReactComponent as LogoutIcon } from "assets/icons/ads/logout.svg"; import styled from "styled-components"; import { CommonComponentProps, Classes } from "./common"; import { noop } from "lodash"; @@ -86,6 +87,7 @@ export const IconCollection = [ "downArrow", "context-menu", "duplicate", + "logout", ] as const; export type IconName = typeof IconCollection[number]; @@ -184,6 +186,9 @@ const Icon = (props: IconProps & CommonComponentProps) => { case "duplicate": returnIcon = ; break; + case "logout": + returnIcon = ; + break; default: returnIcon = null; break; diff --git a/app/client/src/components/ads/Menu.tsx b/app/client/src/components/ads/Menu.tsx index b264540bf5..d19971d87c 100644 --- a/app/client/src/components/ads/Menu.tsx +++ b/app/client/src/components/ads/Menu.tsx @@ -16,7 +16,6 @@ const MenuWrapper = styled.div` width: 234px; background: ${props => props.theme.colors.blackShades[3]}; box-shadow: 0px 12px 28px rgba(0, 0, 0, 0.75); - padding: ${props => props.theme.spaces[5]}px 0px; `; const MenuOption = styled.div` diff --git a/app/client/src/components/ads/MenuItem.tsx b/app/client/src/components/ads/MenuItem.tsx index 2f040eaaf7..917107f0e4 100644 --- a/app/client/src/components/ads/MenuItem.tsx +++ b/app/client/src/components/ads/MenuItem.tsx @@ -17,15 +17,15 @@ const ItemRow = styled.a<{ disabled?: boolean }>` align-items: center; justify-content: space-between; text-decoration: none; - padding: ${props => props.theme.spaces[4]}px - ${props => props.theme.spaces[6]}px; + padding: 0px ${props => props.theme.spaces[6]}px; + height: 38px; ${props => !props.disabled ? ` &:hover { - text-decoration: none; cursor: pointer; + text-decoration: none; background-color: ${props.theme.colors.blackShades[4]}; .${Classes.TEXT} { color: ${props.theme.colors.blackShades[9]}; @@ -38,7 +38,8 @@ const ItemRow = styled.a<{ disabled?: boolean }>` }` : ` &:hover { - cursor: not-allowed; + text-decoration: none; + cursor: default; } `} `; @@ -68,7 +69,7 @@ function MenuItem(props: MenuItemProps) { ) : null} - {props.label ? {props.label} : null} + {props.label ? props.label : null} ); } diff --git a/app/client/src/components/ads/RectangularSwitcher.tsx b/app/client/src/components/ads/RectangularSwitcher.tsx new file mode 100644 index 0000000000..03dd1ce2cf --- /dev/null +++ b/app/client/src/components/ads/RectangularSwitcher.tsx @@ -0,0 +1,129 @@ +import { CommonComponentProps, Classes } from "./common"; +import React, { useState, useEffect } from "react"; +import styled from "styled-components"; +import Text, { TextType } from "./Text"; + +type SwitchProps = CommonComponentProps & { + onSwitch: (value: boolean) => void; + value: boolean; +}; + +const StyledSwitch = styled.label<{ + isLoading?: boolean; + value: boolean; + firstRender: boolean; +}>` + position: relative; + display: block; + width: 78px; + height: 26px; + cursor: pointer; + + input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + border: 1px solid ${props => props.theme.colors.blackShades[5]}; + background-color: ${props => props.theme.colors.info.main}; + width: 78px; + height: 26px; + } + + ${props => + `.slider:before { + position: absolute; + content: ""; + width: 36px; + height: 20px; + top: 2px; + background-color: ${props.theme.colors.blackShades[0]}; + left: ${props.value && !props.firstRender ? "38px" : "2px"}; + transition: ${props.firstRender ? "0.4s" : "none"}; + } + `} + + input:checked + .slider:before { + transform: ${props => (props.firstRender ? "translateX(36px)" : "none")}; + } + + input:checked + .slider:before { + background-color: ${props => props.theme.colors.blackShades[0]}; + } + + input:hover + .slider { + border: 1px solid ${props => props.theme.colors.blackShades[7]}; + } +`; + +const Light = styled.div<{ value: boolean }>` + .${Classes.TEXT} { + color: ${props => (props.value ? "#FFFFFF" : "#939090")}; + font-size: 10px; + line-height: 12px; + letter-spacing: -0.171429px; + } + position: absolute; + top: 3px; + left: 10px; +`; + +const Dark = styled.div<{ value: boolean }>` + .${Classes.TEXT} { + font-size: 10px; + line-height: 12px; + letter-spacing: -0.171429px; + color: ${props => (!props.value ? "#FFFFFF" : "#939090")}; + } + position: absolute; + top: 3px; + left: 46px; +`; + +export default function Switch(props: SwitchProps) { + const [value, setValue] = useState(false); + const [firstRender, setFirstRender] = useState(false); + + useEffect(() => { + setValue(props.value); + }, [props.value]); + + const onChangeHandler = (value: boolean) => { + setValue(value); + props.onSwitch && props.onSwitch(value); + }; + + return ( + + ) => { + if (!firstRender) { + setFirstRender(true); + } + onChangeHandler(e.target.checked); + }} + /> + + + Light + + + Dark + + + ); +} diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index f19f2b1440..92c040cef1 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -113,8 +113,8 @@ const InputWrapper = styled.div` `; const ErrorWrapper = styled.div` - position absolute; - bottom: -17px; + position: absolute; + bottom: -17px; `; const TextInput = forwardRef( (props: TextInputProps, ref: Ref) => { @@ -135,7 +135,7 @@ const TextInput = forwardRef( const inputStyle = useMemo( () => boxStyles(props, validation.isValid, theme), - [props, validation.isValid], + [props, validation.isValid, theme], ); const memoizedChangeHandler = useCallback( diff --git a/app/client/src/pages/common/PageHeader.tsx b/app/client/src/pages/common/PageHeader.tsx index 41a095c9f4..8efbb04401 100644 --- a/app/client/src/pages/common/PageHeader.tsx +++ b/app/client/src/pages/common/PageHeader.tsx @@ -5,15 +5,13 @@ import { getCurrentUser } from "selectors/usersSelectors"; import styled from "styled-components"; import StyledHeader from "components/designSystems/appsmith/StyledHeader"; import AppsmithLogo from "assets/images/appsmith_logo_white.png"; -import CustomizedDropdown from "./CustomizedDropdown"; -import DropdownProps from "./CustomizedDropdown/HeaderDropdownData"; import { AppState } from "reducers"; import { User, ANONYMOUS_USERNAME } from "constants/userConstants"; import { AUTH_LOGIN_URL, APPLICATIONS_URL } from "constants/routes"; import Button from "components/editorComponents/Button"; import history from "utils/history"; import { Colors } from "constants/Colors"; -// import ThemeSwitcher from "./ThemeSwitcher"; +import ProfileDropdown from "./ProfileDropdown"; const StyledPageHeader = styled(StyledHeader)` background: ${Colors.BALTIC_SEA}; @@ -42,10 +40,6 @@ type PageHeaderProps = { user?: User; }; -// const StyledSwitcher = styled(ThemeSwitcher)` -// flex: 1; -// `; - export const PageHeader = (props: PageHeaderProps) => { const { user } = props; const location = useLocation(); @@ -62,7 +56,6 @@ export const PageHeader = (props: PageHeaderProps) => { - {/* */} {user && ( {user.username === ANONYMOUS_USERNAME ? ( @@ -74,7 +67,7 @@ export const PageHeader = (props: PageHeaderProps) => { onClick={() => history.push(loginUrl)} /> ) : ( - + )} )} diff --git a/app/client/src/pages/common/ProfileDropdown.tsx b/app/client/src/pages/common/ProfileDropdown.tsx new file mode 100644 index 0000000000..d663794afa --- /dev/null +++ b/app/client/src/pages/common/ProfileDropdown.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { CommonComponentProps } from "components/ads/common"; +import { getInitialsAndColorCode } from "utils/AppsmithUtils"; +import { useSelector } from "react-redux"; +import { getThemeDetails } from "selectors/themeSelectors"; +import Text, { TextType } from "components/ads/Text"; +import styled from "styled-components"; +import { Position } from "@blueprintjs/core"; +import Menu from "components/ads/Menu"; +import ThemeSwitcher from "./ThemeSwitcher"; +import MenuDivider from "components/ads/MenuDivider"; +import MenuItem from "components/ads/MenuItem"; +import { + getOnSelectAction, + DropdownOnSelectActions, +} from "./CustomizedDropdown/dropdownHelpers"; +import { ReduxActionTypes } from "constants/ReduxActionConstants"; + +type TagProps = CommonComponentProps & { + onClick?: (text: string) => void; + userName?: string; +}; + +const ProfileImage = styled.div<{ backgroundColor?: string }>` + width: 30px; + height: 30px; + display: flex; + align-items: center; + border-radius: 50%; + justify-content: center; + cursor: pointer; + background-color: ${props => props.backgroundColor}; +`; + +export default function ProfileDropdown(props: TagProps) { + const themeDetails = useSelector(getThemeDetails); + + const initialsAndColorCode = getInitialsAndColorCode( + props.userName, + themeDetails.theme.colors.appCardColors, + ); + + return ( + + + {initialsAndColorCode[0]} + + + } + > + + + + getOnSelectAction(DropdownOnSelectActions.DISPATCH, { + type: ReduxActionTypes.LOGOUT_USER_INIT, + }) + } + /> + + ); +} diff --git a/app/client/src/pages/common/ThemeSwitcher.tsx b/app/client/src/pages/common/ThemeSwitcher.tsx index 4b784da2dd..a8bb831c51 100644 --- a/app/client/src/pages/common/ThemeSwitcher.tsx +++ b/app/client/src/pages/common/ThemeSwitcher.tsx @@ -1,18 +1,31 @@ -import Toggle from "components/ads/Toggle"; -import React from "react"; -import { useDispatch } from "react-redux"; +import React, { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import { setThemeMode } from "actions/themeActions"; import { ThemeMode } from "reducers/uiReducers/themeReducer"; +import Switch from "components/ads/RectangularSwitcher"; +import MenuItem from "components/ads/MenuItem"; +import { getThemeDetails } from "selectors/themeSelectors"; export default function ThemeSwitcher(props: { className?: string }) { const dispatch = useDispatch(); + const themeDetails = useSelector(getThemeDetails); + const [switchedOn, setSwitchOn] = useState( + themeDetails.mode === ThemeMode.DARK, + ); + return ( - { - dispatch(setThemeMode(value ? ThemeMode.LIGHT : ThemeMode.DARK)); - }} - > + { + setSwitchOn(value); + dispatch(setThemeMode(value ? ThemeMode.DARK : ThemeMode.LIGHT)); + }} + > + } + /> ); } diff --git a/app/client/src/reducers/uiReducers/themeReducer.ts b/app/client/src/reducers/uiReducers/themeReducer.ts index 4ea9089a64..e3fcd17d83 100644 --- a/app/client/src/reducers/uiReducers/themeReducer.ts +++ b/app/client/src/reducers/uiReducers/themeReducer.ts @@ -7,7 +7,7 @@ export enum ThemeMode { DARK = "DARK", } const initialState: ThemeState = { - mode: ThemeMode.LIGHT, + mode: ThemeMode.DARK, theme: { ...theme, colors: { diff --git a/app/client/src/sagas/ThemeSaga.tsx b/app/client/src/sagas/ThemeSaga.tsx new file mode 100644 index 0000000000..8105d62da0 --- /dev/null +++ b/app/client/src/sagas/ThemeSaga.tsx @@ -0,0 +1,11 @@ +import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants"; +import { takeLatest } from "redux-saga/effects"; +import { ThemeMode } from "reducers/uiReducers/themeReducer"; + +export function* setThemeSaga(actionPayload: ReduxAction) { + yield localStorage.setItem("THEME", actionPayload.payload); +} + +export default function* themeSagas() { + yield takeLatest(ReduxActionTypes.SET_THEME, setThemeSaga); +} diff --git a/app/client/src/sagas/index.tsx b/app/client/src/sagas/index.tsx index 917e1ff069..a246e6922f 100644 --- a/app/client/src/sagas/index.tsx +++ b/app/client/src/sagas/index.tsx @@ -18,6 +18,7 @@ import curlImportSagas from "./CurlImportSagas"; import queryPaneSagas from "./QueryPaneSagas"; import modalSagas from "./ModalSagas"; import batchSagas from "./BatchSagas"; +import themeSagas from "./ThemeSaga"; export function* rootSaga() { yield all([ @@ -40,5 +41,6 @@ export function* rootSaga() { spawn(queryPaneSagas), spawn(modalSagas), spawn(batchSagas), + spawn(themeSagas), ]); }