gravatar and profile menu

This commit is contained in:
wil committed 2024-08-29 14:05:51 -04:00
1 parent c651d70178
commit 09df78993b
3 files changed
+222 -3

No files matched your search

+106
View File
@@ -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 (
<div
className={twMerge(
"relative aspect-square overflow-hidden rounded-full border-2 border-white",
props.className,
)}
>
{showLoader && (
<div className="absolute flex h-full w-full items-center justify-center bg-secondary">
<FontAwesomeIcon icon={faSpinnerThird} spin size={"lg"} />
</div>
)}
<img
crossOrigin="anonymous"
alt={altText}
src={imgUrl}
height={props.size}
width={props.size}
style={{ backgroundColor: getBackgroundColor(props.backgroundIndex) }}
onLoad={() => {
setState((curr) => ({
imgUrl: curr.imgUrl,
showLoader: false,
}));
}}
onError={() => {
setState((curr) => ({
imgUrl:
curr.imgUrl !== localDefaultUrl ? localDefaultUrl : curr.imgUrl,
showLoader: false,
}));
}}
/>
</div>
);
}
export { Gravatar };
+109
View File
@@ -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 (
<Menu as="div" className="relative">
<MenuButton
as="div"
className="group flex size-12 cursor-pointer items-center gap-1.5 transition-opacity duration-150 hover:opacity-90"
>
<Gravatar
size={34}
username={username}
email_hash={emailHash}
avatarIndex={avatarIndex}
backgroundIndex={backgroundColorIndex}
/>
<FontAwesomeIcon icon={faCaretDown} />
</MenuButton>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems
static
className="absolute right-[-5px] mt-2.5 w-36 overflow-hidden rounded-lg bg-secondary shadow-lg focus:outline-none"
>
<MenuItem key={0}>
{({ active }) => (
<a
className={twMerge(
"duration-50 bg-action/60 group flex w-full items-center gap-2 px-3 py-[10px] text-start text-sm font-medium text-white transition-all",
active && "bg-action-500/80",
)}
href={profileUrl}
target="_blank"
rel="noreferrer"
>
<FontAwesomeIcon icon={faUser} />
My Profile
</a>
)}
</MenuItem>
{options.map((option, index) => (
<MenuItem key={index + 1}>
{({ active }) => (
<button
className={twMerge(
"duration-50 bg-action/60 group flex w-full items-center gap-2 px-3 py-[10px] text-start text-sm font-medium text-white transition-all",
active && "bg-action-500/80",
)}
onClick={() => option.onClick()}
>
<FontAwesomeIcon icon={option.icon} />
{option.label}
</button>
)}
</MenuItem>
))}
</MenuItems>
</Transition>
</Menu>
);
}
+7 -3
View File
@@ -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<HTMLDivElement> {}
@@ -44,10 +45,13 @@ export const RightSidePanel = ({
)}
{...props}
>
<div className="flex p-2">
<img src="/brand/Storyteller-Logo.png" alt="Storyteller Logo" />
<div className="flex gap-4">
<div className="grow">
<img src="/brand/Storyteller-Logo.png" alt="Storyteller Logo" />
</div>
<ProfileDropdown />
</div>
<hr className="border-ui-divider" />
<hr className="border-ui-divider my-2" />
{children}
<button