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 && (
+
+
+
+ )}
+

{
+ 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 (
+
+ );
+}
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}
>
-
-

+
+
+

+
+
-
+
{children}