diff --git a/app/scenes/Settings/Details.tsx b/app/scenes/Settings/Details.tsx index a8007381b..0a3e83239 100644 --- a/app/scenes/Settings/Details.tsx +++ b/app/scenes/Settings/Details.tsx @@ -3,28 +3,26 @@ import { TeamIcon } from "outline-icons"; import { useRef, useState } from "react"; import * as React from "react"; import { useTranslation, Trans } from "react-i18next"; -import styled from "styled-components"; import Button from "~/components/Button"; -import Flex from "~/components/Flex"; import Heading from "~/components/Heading"; import HelpText from "~/components/HelpText"; -import Input, { LabelText } from "~/components/Input"; +import Input from "~/components/Input"; import Scene from "~/components/Scene"; import env from "~/env"; import useCurrentTeam from "~/hooks/useCurrentTeam"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; -import ImageUpload from "./components/ImageUpload"; +import ImageInput from "./components/ImageInput"; function Details() { const { auth } = useStores(); const { showToast } = useToasts(); - const team = useCurrentTeam(); const { t } = useTranslation(); + const team = useCurrentTeam(); const form = useRef(null); const [name, setName] = useState(team.name); const [subdomain, setSubdomain] = useState(team.subdomain); - const [avatarUrl, setAvatarUrl] = useState(); + const [avatarUrl, setAvatarUrl] = useState(team.avatarUrl); const handleSubmit = React.useCallback( async (event?: React.SyntheticEvent) => { @@ -64,13 +62,15 @@ function Details() { [] ); - const handleAvatarUpload = React.useCallback( - (avatarUrl: string) => { - setAvatarUrl(avatarUrl); - handleSubmit(); - }, - [handleSubmit] - ); + const handleAvatarUpload = async (avatarUrl: string) => { + setAvatarUrl(avatarUrl); + await auth.updateTeam({ + avatarUrl, + }); + showToast(t("Logo updated"), { + type: "success", + }); + }; const handleAvatarError = React.useCallback( (error: string | null | undefined) => { @@ -90,22 +90,14 @@ function Details() { - - {t("Logo")} - - - - - Upload - - - - + +
props.theme.white}; - - div div { - ${avatarStyles}; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - opacity: 0; - cursor: pointer; - transition: all 250ms; - } - - &:hover div { - opacity: 1; - background: rgba(0, 0, 0, 0.75); - color: ${(props) => props.theme.white}; - } -`; - -const Avatar = styled.img` - ${avatarStyles}; -`; - export default observer(Details); diff --git a/app/scenes/Settings/Profile.tsx b/app/scenes/Settings/Profile.tsx index c8965e617..5bb2015ad 100644 --- a/app/scenes/Settings/Profile.tsx +++ b/app/scenes/Settings/Profile.tsx @@ -6,36 +6,44 @@ import styled from "styled-components"; import { languageOptions } from "@shared/i18n"; import UserDelete from "~/scenes/UserDelete"; import Button from "~/components/Button"; -import Flex from "~/components/Flex"; import Heading from "~/components/Heading"; import HelpText from "~/components/HelpText"; -import Input, { LabelText } from "~/components/Input"; +import Input from "~/components/Input"; import InputSelect from "~/components/InputSelect"; import Scene from "~/components/Scene"; +import useCurrentUser from "~/hooks/useCurrentUser"; import useStores from "~/hooks/useStores"; import useToasts from "~/hooks/useToasts"; -import ImageUpload from "./components/ImageUpload"; +import ImageInput from "./components/ImageInput"; const Profile = () => { const { auth } = useStores(); + const user = useCurrentUser(); const form = React.useRef(null); - const [name, setName] = React.useState(auth.user?.name || ""); - const [avatarUrl, setAvatarUrl] = React.useState(); + const [name, setName] = React.useState(user.name || ""); + const [avatarUrl, setAvatarUrl] = React.useState(user.avatarUrl); const [showDeleteModal, setShowDeleteModal] = React.useState(false); - const [language, setLanguage] = React.useState(auth.user?.language); + const [language, setLanguage] = React.useState(user.language); const { showToast } = useToasts(); const { t } = useTranslation(); const handleSubmit = async (ev: React.SyntheticEvent) => { ev.preventDefault(); - await auth.updateUser({ - name, - avatarUrl, - language, - }); - showToast(t("Profile saved"), { - type: "success", - }); + + try { + await auth.updateUser({ + name, + avatarUrl, + language, + }); + showToast(t("Profile saved"), { + type: "success", + }); + } catch (err) { + showToast(err.message, { + type: "error", + }); + } }; const handleNameChange = (ev: React.ChangeEvent) => { @@ -67,26 +75,19 @@ const Profile = () => { }; const isValid = form.current && form.current.checkValidity(); - const { user, isSaving } = auth; - if (!user) return null; + const { isSaving } = auth; return ( }> {t("Profile")} - - {t("Photo")} - - - - - {t("Upload")} - - - - + + + props.theme.white}; - } -`; - -const Avatar = styled.img` - ${avatarStyles}; -`; - export default observer(Profile); diff --git a/app/scenes/Settings/components/ImageInput.tsx b/app/scenes/Settings/components/ImageInput.tsx new file mode 100644 index 000000000..4ce1c07fa --- /dev/null +++ b/app/scenes/Settings/components/ImageInput.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import Flex from "~/components/Flex"; +import { LabelText } from "~/components/Input"; +import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload"; + +type Props = ImageUploadProps & { + label: string; + src?: string; +}; + +export default function ImageInput({ label, src, ...rest }: Props) { + const { t } = useTranslation(); + + return ( + + {label} + + + + + {t("Upload")} + + + + + ); +} + +const InputWrapper = styled(Flex)` + margin-bottom: 24px; +`; + +const avatarStyles = ` + width: 80px; + height: 80px; +`; + +const Avatar = styled.img` + ${avatarStyles}; +`; + +const ImageBox = styled(Flex)` + ${avatarStyles}; + position: relative; + font-size: 14px; + border-radius: 8px; + box-shadow: 0 0 0 1px ${(props) => props.theme.secondaryBackground}; + background: ${(props) => props.theme.background}; + overflow: hidden; + + div div { + ${avatarStyles}; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + opacity: 0; + cursor: pointer; + transition: all 250ms; + } + + &:hover div { + opacity: 1; + background: rgba(0, 0, 0, 0.75); + color: ${(props) => props.theme.white}; + } +`; diff --git a/app/scenes/Settings/components/ImageUpload.tsx b/app/scenes/Settings/components/ImageUpload.tsx index f9d82d642..7ab5e0f52 100644 --- a/app/scenes/Settings/components/ImageUpload.tsx +++ b/app/scenes/Settings/components/ImageUpload.tsx @@ -16,16 +16,16 @@ import { uploadFile, dataUrlToBlob } from "~/utils/uploadFile"; const EMPTY_OBJECT = {}; -type Props = RootStore & { +export type Props = { children?: React.ReactNode; - onSuccess: (arg0: string) => void | Promise; - onError: (arg0: string) => void; + onSuccess: (url: string) => void | Promise; + onError: (error: string) => void; submitText?: string; borderRadius?: number; }; @observer -class ImageUpload extends React.Component { +class ImageUpload extends React.Component { @observable isUploading = false; @@ -41,7 +41,7 @@ class ImageUpload extends React.Component { avatarEditorRef = React.createRef(); static defaultProps = { - submitText: "Crop Picture", + submitText: "Crop Image", borderRadius: 150, }; diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 97e735614..0f176305c 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -517,6 +517,7 @@ "Error": "Error", "All collections": "All collections", "{{userName}} requested": "{{userName}} requested", + "Upload": "Upload", "Last active": "Last active", "Suspended": "Suspended", "Shared": "Shared", @@ -534,11 +535,10 @@ "Active": "Active", "Everyone": "Everyone", "Admins": "Admins", + "Logo updated": "Logo updated", "Unable to upload new logo": "Unable to upload new logo", "These details affect the way that your Outline appears to everyone on the team.": "These details affect the way that your Outline appears to everyone on the team.", "Logo": "Logo", - "Crop logo": "Crop logo", - "Upload": "Upload", "Subdomain": "Subdomain", "Your knowledge base will be accessible at": "Your knowledge base will be accessible at", "Manage optional and beta features. Changing these settings will affect the experience for all team members.": "Manage optional and beta features. Changing these settings will affect the experience for all team members.",