diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index 97cccdad2..e9058b6dd 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -1,11 +1,14 @@ import { FunctionalComponent, ComponentChild } from 'preact'; +import { HtmlInputTypes } from '@/enums'; interface Props { + type?: HtmlInputTypes; className?: string; disabled?: boolean; left?: ComponentChild[]; right?: ComponentChild[]; text?: string; + placeholder?: string; onChange?: (text: string) => void; } @@ -13,11 +16,13 @@ interface Props { * Input that can be decorated on the left and right side */ export const DecoratedInput: FunctionalComponent = ({ + type = 'text', className = '', disabled = false, left, right, text, + placeholder = '', onChange, }) => { const base = @@ -32,10 +37,11 @@ export const DecoratedInput: FunctionalComponent = ({ {left}
onChange && onChange((e.target as HTMLInputElement).value) } diff --git a/app/assets/javascripts/components/shared/HorizontalSeparator.tsx b/app/assets/javascripts/components/shared/HorizontalSeparator.tsx new file mode 100644 index 000000000..7e107d75d --- /dev/null +++ b/app/assets/javascripts/components/shared/HorizontalSeparator.tsx @@ -0,0 +1,10 @@ +import { FunctionalComponent } from 'preact'; + +type Props = { + classes?: string; +} +export const HorizontalSeparator: FunctionalComponent = ({ + classes = '' +}) => { + return
; +}; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/components/shared/ModalDialog.tsx similarity index 62% rename from app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx rename to app/assets/javascripts/components/shared/ModalDialog.tsx index 8c0b45987..a37ad2822 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/components/shared/ModalDialog.tsx @@ -1,19 +1,9 @@ -import { ComponentChildren, FunctionComponent } from 'preact'; -import { IconButton } from '../../../components/IconButton'; -import { - AlertDialog, - AlertDialogDescription, - AlertDialogLabel, -} from '@reach/alert-dialog'; -import { useRef } from 'preact/hooks'; +import { FunctionComponent } from 'preact'; +import { AlertDialog, AlertDialogDescription, AlertDialogLabel } from '@node_modules/@reach/alert-dialog'; +import { useRef } from '@node_modules/preact/hooks'; +import { IconButton } from '@/components/IconButton'; -/** - * TwoFactorDialog is AlertDialog styled for 2FA - * Can be generalized but more use cases are needed - */ -export const TwoFactorDialog: FunctionComponent<{ - children: ComponentChildren; -}> = ({ children }) => { +export const ModalDialog: FunctionComponent = ({ children }) => { const ldRef = useRef(); return ( @@ -34,12 +24,14 @@ export const TwoFactorDialog: FunctionComponent<{ ); }; -export const TwoFactorDialogLabel: FunctionComponent<{ +export const ModalDialogLabel: FunctionComponent<{ closeDialog: () => void; }> = ({ children, closeDialog }) => (
-
{children}
+
+ {children} +
); -export const TwoFactorDialogDescription: FunctionComponent = ({ children }) => ( +export const ModalDialogDescription: FunctionComponent = ({ children }) => ( {children} ); -export const TwoFactorDialogButtons: FunctionComponent = ({ children }) => ( +export const ModalDialogButtons: FunctionComponent = ({ children }) => ( <>
{children}
); + +export default ModalDialog; diff --git a/app/assets/javascripts/enums.ts b/app/assets/javascripts/enums.ts new file mode 100644 index 000000000..199fab2ff --- /dev/null +++ b/app/assets/javascripts/enums.ts @@ -0,0 +1,24 @@ +export enum HtmlInputTypes { + Button = 'button', + Checkbox = 'checkbox', + Color = 'color', + Date = 'date', + DateTimeLocal = 'datetime-local', + Email = 'email', + File = 'file', + Hidden = 'hidden', + Image = 'image', + Month = 'month', + Number = 'number', + Password = 'password', + Radio = 'radio', + Range = 'range', + Reset = 'reset', + Search = 'search', + Submit = 'submit', + Tel = 'tel', + Text = 'text', + Time = 'time', + Url = 'url', + Week = 'week' +} diff --git a/app/assets/javascripts/hooks/useBeforeUnload.tsx b/app/assets/javascripts/hooks/useBeforeUnload.tsx new file mode 100644 index 000000000..23a078bde --- /dev/null +++ b/app/assets/javascripts/hooks/useBeforeUnload.tsx @@ -0,0 +1,11 @@ +import { useEffect } from '@node_modules/preact/hooks'; + +export const useBeforeUnload = (): void => { + useEffect(() => { + window.onbeforeunload = () => true; + + return () => { + window.onbeforeunload = null; + }; + }, []); +}; diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index 6efab9c48..75b794848 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -1,11 +1,12 @@ import { FunctionComponent } from 'preact'; +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ index, length, }) => index < length - 1 ? ( -
+ ) : null; export const PreferencesSegment: FunctionComponent = ({ children }) => ( @@ -30,7 +31,7 @@ export const PreferencesGroup: FunctionComponent = ({ children }) => ( ); export const PreferencesPane: FunctionComponent = ({ children }) => ( -
+
{children}
diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 6b034d7c6..b9ceb494d 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -1,11 +1,15 @@ -import { Sync } from '@/preferences/panes/account'; +import { Credentials, Sync } from '@/preferences/panes/account'; import { PreferencesPane } from '@/preferences/components'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; -export const AccountPreferences = observer(({application}: {application: WebApplication}) => { +type Props = { + application: WebApplication; +} +export const AccountPreferences = observer(({application}: Props) => { return ( + ); diff --git a/app/assets/javascripts/preferences/panes/account/ChangeEmail.tsx b/app/assets/javascripts/preferences/panes/account/ChangeEmail.tsx new file mode 100644 index 000000000..3754eb2ee --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/ChangeEmail.tsx @@ -0,0 +1,79 @@ +import { + ModalDialog, + ModalDialogButtons, + ModalDialogDescription, + ModalDialogLabel +} from '@/components/shared/ModalDialog'; +import { FunctionalComponent } from 'preact'; +import { DecoratedInput } from '@/components/DecoratedInput'; +import { Button } from '@/components/Button'; +import { useState } from 'preact/hooks'; +import { SNAlertService } from '@node_modules/@standardnotes/snjs'; +import { HtmlInputTypes } from '@/enums'; +import { isEmailValid } from '@/utils'; + +type Props = { + onCloseDialog: () => void; + snAlert: SNAlertService['alert'] +}; + +export const ChangeEmail: FunctionalComponent = ({ onCloseDialog, snAlert }) => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const handleSubmit = () => { + let errorMessage = ''; + if (email.trim() === '' || password.trim() === '') { + errorMessage = 'Some fields have not been filled out. Please fill out all fields and try again.'; + } else if (!isEmailValid(email)) { + errorMessage = 'The email you entered has an invalid format. Please review your input and try again.'; + } + + if (errorMessage) { + snAlert(errorMessage); + return; + } + }; + + return ( +
+ + + Change Email + + +
+ { + setEmail(newEmail); + }} + text={email} + placeholder={'New Email'} + /> +
+
+ setPassword(password)} + /> +
+
+ +
+ ); +}; diff --git a/app/assets/javascripts/preferences/panes/account/Credentials.tsx b/app/assets/javascripts/preferences/panes/account/Credentials.tsx new file mode 100644 index 000000000..0b3caa736 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/Credentials.tsx @@ -0,0 +1,73 @@ +import { PreferencesGroup, PreferencesSegment, Text, Title } from '@/preferences/components'; +import { Button } from '@/components/Button'; +import { WebApplication } from '@/ui_models/application'; +import { observer } from '@node_modules/mobx-react-lite'; +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; +import { dateToLocalizedString } from '@/utils'; +import { useState } from 'preact/hooks'; +import { ChangeEmail } from '@/preferences/panes/account/ChangeEmail'; +import { ChangePassword } from '@/preferences/panes/account/changePassword'; + +type Props = { + application: WebApplication; +}; + +export const Credentials = observer(({ application }: Props) => { + const [isChangePasswordDialogOpen, setIsChangePasswordDialogOpen] = useState(false); + const [isChangeEmailDialogOpen, setIsChangeEmailDialogOpen] = useState(false); + + const user = application.getUser(); + + const passwordCreatedAtTimestamp = application.getUserPasswordCreationDate() as Date; + const passwordCreatedOn = dateToLocalizedString(passwordCreatedAtTimestamp); + + return ( + + + Credentials +
+ Email +
+ + You're signed in as {user?.email} + +
+ ); +}; diff --git a/app/assets/javascripts/preferences/panes/account/index.ts b/app/assets/javascripts/preferences/panes/account/index.ts index 2225b5881..4910df077 100644 --- a/app/assets/javascripts/preferences/panes/account/index.ts +++ b/app/assets/javascripts/preferences/panes/account/index.ts @@ -1 +1,2 @@ -export { default as Sync } from './Sync'; +export { Sync } from './Sync'; +export { Credentials } from './Credentials'; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index 7244240ea..7a836f8aa 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -6,11 +6,11 @@ import { FunctionComponent } from 'preact'; import { downloadSecretKey } from './download-secret-key'; import { TwoFactorActivation } from './TwoFactorActivation'; import { - TwoFactorDialog, - TwoFactorDialogLabel, - TwoFactorDialogDescription, - TwoFactorDialogButtons, -} from './TwoFactorDialog'; + ModalDialog, + ModalDialogButtons, + ModalDialogDescription, + ModalDialogLabel +} from '@/components/shared/ModalDialog'; export const SaveSecretKey: FunctionComponent<{ activation: TwoFactorActivation; @@ -32,15 +32,15 @@ export const SaveSecretKey: FunctionComponent<{ /> ); return ( - - + { act.cancelActivation(); }} > Step 2 of 3 - Save secret key - - + +
@@ -70,8 +70,8 @@ export const SaveSecretKey: FunctionComponent<{
- - + +
-
- + +