💄 Update avatar colors (#1351)

This commit is contained in:
Luke Vella 2024-09-17 21:07:27 +01:00 committed by GitHub
parent 554e4fe48f
commit a6bb357acc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 65 additions and 57 deletions

View file

@ -39,6 +39,7 @@ export function OptimizedAvatarImage({
) : null} ) : null}
{!src || !isLoaded ? ( {!src || !isLoaded ? (
<AvatarFallback <AvatarFallback
seed={name}
className={cn({ className={cn({
"text-xs": size <= 24, "text-xs": size <= 24,
"text-lg": size >= 48, "text-lg": size >= 48,

View file

@ -1,5 +1,5 @@
import { cn } from "@rallly/ui"; import { cn } from "@rallly/ui";
import { Avatar, AvatarFallback, getColor } from "@rallly/ui/avatar"; import { Avatar, AvatarFallback } from "@rallly/ui/avatar";
import React from "react"; import React from "react";
export function Participant({ children }: { children: React.ReactNode }) { export function Participant({ children }: { children: React.ReactNode }) {
@ -13,11 +13,9 @@ export const ParticipantAvatar = ({
size?: number; size?: number;
name: string; name: string;
}) => { }) => {
const color = getColor(name);
return ( return (
<Avatar size={size}> <Avatar size={size}>
<AvatarFallback className="text-xs" color={color}> <AvatarFallback className="text-xs" seed={name}>
{name[0]?.toUpperCase()} {name[0]?.toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>

View file

@ -15,6 +15,7 @@ import { Controller } from "react-hook-form";
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image"; import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Participant, ParticipantName } from "@/components/participant"; import { Participant, ParticipantName } from "@/components/participant";
import { useVotingForm } from "@/components/poll/voting-form"; import { useVotingForm } from "@/components/poll/voting-form";
import { YouAvatar } from "@/components/poll/you-avatar";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { usePoll } from "../../poll-context"; import { usePoll } from "../../poll-context";
@ -60,7 +61,11 @@ const ParticipantRowForm = ({
> >
<div className="flex items-center justify-between gap-x-2.5"> <div className="flex items-center justify-between gap-x-2.5">
<Participant> <Participant>
{name ? (
<OptimizedAvatarImage name={participantName} size={20} /> <OptimizedAvatarImage name={participantName} size={20} />
) : (
<YouAvatar />
)}
<ParticipantName>{participantName}</ParticipantName> <ParticipantName>{participantName}</ParticipantName>
</Participant> </Participant>
{!isNew ? ( {!isNew ? (

View file

@ -20,6 +20,7 @@ import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Participant, ParticipantName } from "@/components/participant"; import { Participant, ParticipantName } from "@/components/participant";
import { ParticipantDropdown } from "@/components/participant-dropdown"; import { ParticipantDropdown } from "@/components/participant-dropdown";
import { useVotingForm } from "@/components/poll/voting-form"; import { useVotingForm } from "@/components/poll/voting-form";
import { YouAvatar } from "@/components/poll/you-avatar";
import { useOptions, usePoll } from "@/components/poll-context"; import { useOptions, usePoll } from "@/components/poll-context";
import { Trans } from "@/components/trans"; import { Trans } from "@/components/trans";
import { usePermissions } from "@/contexts/permissions"; import { usePermissions } from "@/contexts/permissions";
@ -121,7 +122,7 @@ const MobilePoll: React.FunctionComponent = () => {
) : ( ) : (
<div className="flex grow items-center px-1"> <div className="flex grow items-center px-1">
<Participant> <Participant>
<OptimizedAvatarImage name={t("you")} size={20} /> <YouAvatar />
<ParticipantName>{t("you")}</ParticipantName> <ParticipantName>{t("you")}</ParticipantName>
</Participant> </Participant>
</div> </div>

View file

@ -61,8 +61,6 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
<div className="truncate text-sm">{name}</div> <div className="truncate text-sm">{name}</div>
</div> </div>
))} ))}
</div>
<div className="col-span-1 space-y-2.5">
{participantsWhoVotedIfNeedBe.map(({ name }, i) => ( {participantsWhoVotedIfNeedBe.map(({ name }, i) => (
<div key={i} className="flex"> <div key={i} className="flex">
<div className="relative mr-2.5 flex size-5 items-center justify-center"> <div className="relative mr-2.5 flex size-5 items-center justify-center">
@ -70,12 +68,14 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
<VoteIcon <VoteIcon
type="ifNeedBe" type="ifNeedBe"
size="sm" size="sm"
className="absolute bottom-full left-full -translate-x-1/2 translate-y-1/2 rounded-full bg-white" className="absolute bottom-full left-full -translate-x-1.5 translate-y-2.5 rounded-full bg-white"
/> />
</div> </div>
<div className="truncate text-sm"> {name}</div> <div className="truncate text-sm"> {name}</div>
</div> </div>
))} ))}
</div>
<div className="col-span-1 space-y-2.5">
{participantsWhoVotedNo.map(({ name }, i) => ( {participantsWhoVotedNo.map(({ name }, i) => (
<div key={i} className="flex"> <div key={i} className="flex">
<div className="relative mr-2.5 flex size-5 items-center justify-center"> <div className="relative mr-2.5 flex size-5 items-center justify-center">
@ -83,7 +83,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
<VoteIcon <VoteIcon
type="no" type="no"
size="sm" size="sm"
className="absolute bottom-full left-full -translate-x-1/2 translate-y-1/2 rounded-full bg-white" className="absolute bottom-full left-full -translate-x-1.5 translate-y-2.5 rounded-full bg-white"
/> />
</div> </div>
<div className="truncate text-sm">{name}</div> <div className="truncate text-sm">{name}</div>

View file

@ -0,0 +1,11 @@
import { useTranslation } from "@/app/i18n/client";
export function YouAvatar() {
const { t } = useTranslation();
return (
<div className="inline-flex size-5 items-center justify-center rounded-full bg-gray-200 text-xs font-medium">
{t("you")[0]}
</div>
);
}

View file

@ -2,24 +2,9 @@
import * as React from "react"; import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@rallly/ui"; import { cn } from "@rallly/ui";
export const avatarColors = [
"indigo",
"green",
"blue",
"purple",
"emerald",
"violet",
"sky",
"cyan",
"pink",
] as const;
export type AvatarColor = (typeof avatarColors)[number];
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>, React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & { React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
@ -50,47 +35,54 @@ const AvatarImage = React.forwardRef<
)); ));
AvatarImage.displayName = AvatarPrimitive.Image.displayName; AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const avatarFallbackVariants = cva( const colorPairs = [
"flex h-full w-full items-center justify-center rounded-full font-medium", { bg: "#E6F4FF", text: "#0065BD" }, // Light blue
{ { bg: "#DCFCE7", text: "#15803D" }, // Light green
variants: { { bg: "#FFE6F4", text: "#BD007A" }, // Light pink
color: { { bg: "#F4E6FF", text: "#6200BD" }, // Light purple
indigo: "bg-indigo-50 text-indigo-600", { bg: "#FFE6E6", text: "#BD0000" }, // Light red
green: "bg-green-50 text-green-600", { bg: "#FFE6FF", text: "#A300A3" }, // Bright pink
blue: "bg-blue-50 text-blue-600", { bg: "#F0E6FF", text: "#5700BD" }, // Lavender
purple: "bg-purple-50 text-purple-600", { bg: "#FFE6F9", text: "#BD0066" }, // Rose
emerald: "bg-emerald-50 text-emerald-600", { bg: "#E6E6FF", text: "#0000BD" }, // Periwinkle
violet: "bg-violet-50 text-violet-600", { bg: "#FFE6EC", text: "#BD001F" }, // Salmon pink
sky: "bg-sky-50 text-sky-600", { bg: "#EBE6FF", text: "#4800BD" }, // Light indigo
cyan: "bg-cyan-50 text-cyan-600", ];
pink: "bg-pink-50 text-pink-600",
},
},
defaultVariants: {
color: "indigo",
},
},
);
export function getColor(seed: string): AvatarColor { export function getColor(seed?: string): {
bgColor: string;
textColor: string;
} {
if (!seed) {
return { bgColor: "#E6F4FF", textColor: "#0065BD" };
}
let hash = 0; let hash = 0;
for (let i = 0; i < seed.length; i++) { for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash); hash = seed.charCodeAt(i) + ((hash << 5) - hash);
} }
return avatarColors[Math.abs(hash) % avatarColors.length]; const colorPair = colorPairs[Math.abs(hash) % colorPairs.length];
return { bgColor: colorPair.bg, textColor: colorPair.text };
} }
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>, React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> & {
VariantProps<typeof avatarFallbackVariants> seed: string;
>(({ className, color, ...props }, ref) => ( }
>(({ className, seed, ...props }, ref) => {
const { bgColor, textColor } = getColor(seed);
return (
<AvatarPrimitive.Fallback <AvatarPrimitive.Fallback
ref={ref} ref={ref}
className={cn(avatarFallbackVariants({ color }), className)} className={cn(
"flex h-full w-full items-center justify-center rounded-full font-medium",
className,
)}
style={{ backgroundColor: bgColor, color: textColor }}
{...props} {...props}
/> />
)); );
});
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback }; export { Avatar, AvatarImage, AvatarFallback };