mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-07 13:11:49 +02:00
💄 Update avatar colors (#1351)
This commit is contained in:
parent
554e4fe48f
commit
a6bb357acc
7 changed files with 65 additions and 57 deletions
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
11
apps/web/src/components/poll/you-avatar.tsx
Normal file
11
apps/web/src/components/poll/you-avatar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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 };
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue