mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
gravatar and profile menu
This commit is contained in:
1 parent
c651d70178
commit
09df78993b
3 files changed
+222
-3
No files matched your search
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user