diff --git a/src/components/ui/Gravatar.tsx b/src/components/ui/Gravatar.tsx new file mode 100644 index 0000000000..223814fe89 --- /dev/null +++ b/src/components/ui/Gravatar.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { faSpinnerThird } from "@fortawesome/pro-thin-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +interface Props { + size: number; + email_hash?: string; + username?: string; + avatarIndex?: number; + backgroundIndex?: number; + className?: string; +} + +function Gravatar(props: Props) { + //url to the local fallback image + const localDefaultUrl = + props.avatarIndex === undefined + ? "/resources/avatars/0.webp" + : `/resources/avatars/${props.avatarIndex}.webp`; + + // submitted fallback need to be a url served from our server for gravatar to grab it + const defaultImageUrl = + props.avatarIndex === undefined + ? "https://storyteller.ai/images/avatars/2000x2000/0.webp" + : `https://storyteller.ai/images/avatars/2000x2000/${props.avatarIndex}.webp`; + + // NB: Gravatar suggests URI encoding these: + // https://en.gravatar.com/site/implement/images/ + const encondedDefaultImage = encodeURIComponent(defaultImageUrl); + + const gravatarUrl = `https://www.gravatar.com/avatar/${props.email_hash}?s=${props.size}&d=${encondedDefaultImage}`; + + const [{ imgUrl, showLoader }, setState] = useState<{ + imgUrl: string; + showLoader: boolean; + }>({ + imgUrl: gravatarUrl, + showLoader: true, + }); + + const altText = props.username ? `${props.username}'s gravatar` : "gravatar"; + + function getBackgroundColor(backgroundIndex?: number): string { + switch (backgroundIndex) { + case 0: + return "#E66462"; + case 1: + return "#FD881B"; + case 2: + return "#E7C13C"; + case 3: + return "#4BA905"; + case 4: + return "#25B8A0"; + case 5: + return "#0078D1"; + case 6: + return "#7F52C1"; + case 7: + return "#FF66AC"; + case 8: + return "#259FEC"; + default: + return `#1a1a27`; + } + } + + return ( +
+ {showLoader && ( +
+ +
+ )} + {altText} { + setState((curr) => ({ + imgUrl: curr.imgUrl, + showLoader: false, + })); + }} + onError={() => { + setState((curr) => ({ + imgUrl: + curr.imgUrl !== localDefaultUrl ? localDefaultUrl : curr.imgUrl, + showLoader: false, + })); + }} + /> +
+ ); +} + +export { Gravatar }; diff --git a/src/components/ui/ProfileDropdown.tsx b/src/components/ui/ProfileDropdown.tsx new file mode 100644 index 0000000000..726da2de97 --- /dev/null +++ b/src/components/ui/ProfileDropdown.tsx @@ -0,0 +1,109 @@ +import { Fragment } from "react"; +import { useSignals } from "@preact/signals-react/runtime"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faCaretDown, + faRightFromBracket, + faUser, +} from "@fortawesome/pro-thin-svg-icons"; +import { + Menu, + MenuButton, + MenuItem, + MenuItems, + Transition, +} from "@headlessui/react"; +import { Gravatar } from "./Gravatar"; +import { twMerge } from "tailwind-merge"; + +import { authentication, logout } from "~/signals"; + +export default function ProfileDropdown() { + useSignals(); + const { userInfo } = authentication; + + if (!userInfo.value) { + return null; + } + const username = userInfo.value.core_info.username; + const emailHash = userInfo.value.core_info.gravatar_hash; + const profileUrl = `https://storyteller.ai/profile/${userInfo.value.core_info.display_name}`; + const avatarIndex = userInfo.value.core_info.default_avatar.image_index; + const backgroundColorIndex = + userInfo.value.core_info.default_avatar.color_index; + + const options = [ + { + label: "Logout", + icon: faRightFromBracket, + onClick: () => { + logout(); + }, + }, + ]; + + return ( + + + + + + + + + {({ active }) => ( + + + My Profile + + )} + + {options.map((option, index) => ( + + {({ active }) => ( + + )} + + ))} + + + + ); +} diff --git a/src/components/ui/RightSidePanel.tsx b/src/components/ui/RightSidePanel.tsx index e15bf58ff8..68d21597d1 100644 --- a/src/components/ui/RightSidePanel.tsx +++ b/src/components/ui/RightSidePanel.tsx @@ -3,6 +3,7 @@ import { faChevronLeft, faChevronRight } from "@fortawesome/pro-thin-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import { Transition } from "@headlessui/react"; +import ProfileDropdown from "./ProfileDropdown"; export interface LeftSidePanelProps extends HTMLAttributes {} @@ -44,10 +45,13 @@ export const RightSidePanel = ({ )} {...props} > -
- Storyteller Logo +
+
+ Storyteller Logo +
+
-
+
{children}