🎨 Improve participant definition (#1337)

This commit is contained in:
Luke Vella 2024-09-14 16:17:48 +01:00 committed by GitHub
parent 12110be7c7
commit 4e7b391a9f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 269 additions and 238 deletions

View file

@ -15,7 +15,7 @@ export default async function Layout({
<div className="flex flex-col pb-16 md:pb-0">
<div
className={cn(
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-4 overflow-y-auto p-6 py-5 md:flex",
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-4 overflow-y-auto p-6 md:flex",
)}
>
<div className="flex w-full items-center justify-between gap-4">
@ -24,8 +24,8 @@ export default async function Layout({
</div>
<Sidebar />
</div>
<div className={cn("grow space-y-4 p-3 md:ml-72 md:p-4 lg:px-8 lg:pb-8")}>
{children}
<div className={cn("grow space-y-4 p-3 md:ml-72 md:p-4 lg:p-6")}>
<div className="max-w-5xl">{children}</div>
</div>
<div className="fixed bottom-0 z-20 flex h-16 w-full flex-col justify-center bg-gray-100/90 backdrop-blur-md md:hidden">
<MobileNavigation />

View file

@ -1,10 +1,11 @@
"use client";
import { PollStatus } from "@rallly/database";
import { cn } from "@rallly/ui";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { Icon } from "@rallly/ui/icon";
import { RadioCards, RadioCardsItem } from "@rallly/ui/radio-pills";
import { getCoreRowModel, useReactTable } from "@tanstack/react-table";
import dayjs from "dayjs";
import { CalendarPlusIcon, CheckIcon, LinkIcon, UserIcon } from "lucide-react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
@ -132,31 +133,33 @@ function CopyLinkButton({ pollId }: { pollId: string }) {
const [, copy] = useCopyToClipboard();
const [didCopy, setDidCopy] = React.useState(false);
if (didCopy) {
return (
<div className="inline-flex items-center gap-x-1.5 text-sm font-medium text-green-600">
<CheckIcon className="size-4" />
<Trans i18nKey="copied" />
</div>
);
}
return (
<button
<Button
type="button"
onClick={() => {
disabled={didCopy}
onClick={(e) => {
e.stopPropagation();
copy(`${window.location.origin}/invite/${pollId}`);
setDidCopy(true);
setTimeout(() => {
setDidCopy(false);
}, 1000);
}}
className="text-foreground inline-flex items-center gap-x-1.5 text-sm hover:underline"
className="relative z-20 w-full"
>
<LinkIcon className="size-4" />
{didCopy ? (
<>
<CheckIcon className="size-4" />
<Trans i18nKey="copyLink" defaults="Copy Link" />
</button>
<Trans i18nKey="copied" defaults="Copied" />
</>
) : (
<>
<LinkIcon className="size-4" />
<Trans i18nKey="copyLink" defaults="Copy Link" />
</>
)}
</Button>
);
}
@ -208,38 +211,36 @@ function PollsListView({
}
return (
<div className="grid gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="grid gap-3 sm:gap-4 md:grid-cols-2">
{table.getRowModel().rows.map((row) => (
<div
className={cn("overflow-hidden rounded-lg border bg-white p-1")}
className={cn(
"group relative space-y-4 overflow-hidden rounded-lg border bg-white p-4 focus-within:bg-gray-50",
)}
key={row.id}
>
<div className="relative space-y-4 p-3 focus-within:bg-gray-100">
<div className="flex items-start justify-between">
<GroupPollIcon size="sm" />
<PollStatusBadge status={row.original.status} />
</div>
<div className="space-y-2">
<h2 className="truncate text-base font-medium">
<div className="space-y-4">
<div className="flex items-center gap-3">
<GroupPollIcon size="xs" />
<h2 className="truncate text-base font-medium group-hover:underline">
<Link
href={`/poll/${row.original.id}`}
className="absolute inset-0 z-10"
/>
{row.original.title}
</h2>
<ParticipantCount count={row.original.participants.length} />
</div>
<div className="flex items-center gap-2">
<Badge size="lg">
<PollStatusBadge status={row.original.status} />
</Badge>
<Badge size="lg">
<ParticipantCount count={row.original.participants.length} />
</Badge>
</div>
</div>
<div className="flex items-end justify-between p-3">
<div className="flex items-end justify-between">
<CopyLinkButton pollId={row.original.id} />
<p className="text-muted-foreground whitespace-nowrap text-sm">
<Trans
i18nKey="createdTime"
values={{
relativeTime: dayjs(row.original.createdAt).fromNow(),
}}
/>
</p>
</div>
</div>
))}

View file

@ -50,7 +50,7 @@ export function PageHeader({
className?: string;
variant?: "default" | "ghost";
}) {
return <div className={cn("mb-4 md:mt-2", className)}>{children}</div>;
return <div className={cn("mb-6", className)}>{children}</div>;
}
export function PageSection({ children }: { children?: React.ReactNode }) {

View file

@ -16,6 +16,7 @@ import {
DropdownMenuTrigger,
} from "@rallly/ui/dropdown-menu";
import { Icon } from "@rallly/ui/icon";
import { Input } from "@rallly/ui/input";
import { Textarea } from "@rallly/ui/textarea";
import dayjs from "dayjs";
import {
@ -27,6 +28,11 @@ import { useTranslation } from "next-i18next";
import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import {
Participant,
ParticipantAvatar,
ParticipantName,
} from "@/components/participant";
import { useParticipants } from "@/components/participants-provider";
import { Trans } from "@/components/trans";
import { usePermissions } from "@/contexts/permissions";
@ -36,9 +42,7 @@ import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
import { requiredString } from "../../utils/form-validation";
import NameInput from "../name-input";
import TruncatedLinkify from "../poll/truncated-linkify";
import UserAvatar from "../poll/user-avatar";
import { useUser } from "../user-provider";
interface CommentForm {
@ -119,7 +123,13 @@ function NewCommentForm({
control={control}
rules={{ validate: requiredString }}
render={({ field }) => (
<NameInput error={!!formState.errors.authorName} {...field} />
<Input
placeholder={t("yourName")}
className="lg:w-48"
data-1p-ignore="true"
error={!!formState.errors.authorName}
{...field}
/>
)}
/>
</div>
@ -203,11 +213,15 @@ function DiscussionInner() {
<div className="" key={comment.id}>
<div data-testid="comment">
<div className="mb-1 flex items-center space-x-2">
<UserAvatar
name={comment.authorName}
showName={true}
isYou={session.ownsObject(comment)}
/>
<Participant>
<ParticipantAvatar name={comment.authorName} />
<ParticipantName>{comment.authorName}</ParticipantName>
{session.ownsObject(comment) ? (
<Badge>
<Trans i18nKey="you" />
</Badge>
) : null}
</Participant>
<div className="flex items-center gap-2 text-sm ">
<div className="text-gray-500">
{dayjs(comment.createdAt).fromNow()}

View file

@ -1,47 +0,0 @@
import { cn } from "@rallly/ui";
import { Input, InputProps } from "@rallly/ui/input";
import { useTranslation } from "next-i18next";
import * as React from "react";
import UserAvatar from "./poll/user-avatar";
interface NameInputProps extends InputProps {
value?: string;
defaultValue?: string;
error?: boolean;
}
const NameInput = React.forwardRef<HTMLInputElement, NameInputProps>(function (
{ value, defaultValue, className, error, ...forwardProps },
ref,
) {
const { t } = useTranslation();
return (
<div className="relative flex items-center">
{value ? (
<UserAvatar
name={value ?? defaultValue ?? ""}
className="absolute left-2"
/>
) : null}
<Input
ref={ref}
className={cn(
"input text-sm",
{
"pl-9": value || defaultValue,
"ring-destructive ring-1": error,
},
className,
)}
placeholder={t("yourName")}
value={value}
{...forwardProps}
/>
</div>
);
});
NameInput.displayName = "NameInput";
export default NameInput;

View file

@ -1,4 +1,5 @@
"use client";
import { cn } from "@rallly/ui";
import { Avatar, AvatarFallback, AvatarImage } from "@rallly/ui/avatar";
import Image from "next/image";
import React from "react";
@ -36,7 +37,16 @@ export function OptimizedAvatarImage({
/>
) : null
) : null}
{!src || !isLoaded ? <AvatarFallback>{name[0]}</AvatarFallback> : null}
{!src || !isLoaded ? (
<AvatarFallback
className={cn({
"text-xs": size <= 24,
"text-lg": size >= 48,
})}
>
{name[0]}
</AvatarFallback>
) : null}
</Avatar>
);
}

View file

@ -1,7 +1,7 @@
import { cn } from "@rallly/ui";
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
import { ColoredAvatar } from "@/components/poll/user-avatar";
import { ParticipantAvatar } from "@/components/participant";
interface ParticipantAvatarBarProps {
participants: { name: string }[];
@ -15,25 +15,25 @@ export const ParticipantAvatarBar = ({
const visibleCount = participants.length > max ? max - 1 : max;
const hiddenCount = participants.length - visibleCount;
return (
<ul className="flex items-center -space-x-1 rounded-full border p-0.5">
<ul className="flex items-center -space-x-1">
{participants.slice(0, visibleCount).map((participant, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<li className="inline-flex items-center justify-center rounded-full ring-2 ring-white">
<ColoredAvatar name={participant.name} />
<li className="z-10 inline-flex items-center justify-center rounded-full ring-2 ring-white">
<ParticipantAvatar name={participant.name} />
</li>
</TooltipTrigger>
<TooltipContent>{participant.name}</TooltipContent>
</Tooltip>
))}
{hiddenCount > 1 ? (
<li className="inline-flex items-center justify-center rounded-full ring-2 ring-white">
<li className="relative z-20 inline-flex items-center justify-center rounded-full ring-2 ring-white">
<Tooltip>
<TooltipTrigger asChild>
<span
className={cn(
"select-none",
"rounded-full bg-gray-200 px-1.5 text-xs font-semibold",
"rounded-full bg-gray-100 px-1.5 text-xs font-semibold",
"inline-flex h-5 items-center justify-center",
)}
>

View file

@ -40,8 +40,6 @@ import { useFormValidation } from "@/utils/form-validation";
import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc/client";
import { Participant } from ".prisma/client";
export const ParticipantDropdown = ({
participant,
onEdit,
@ -50,7 +48,12 @@ export const ParticipantDropdown = ({
align,
}: {
disabled?: boolean;
participant: Participant;
participant: {
name: string;
userId?: string;
email?: string;
id: string;
};
align?: "start" | "end";
onEdit: () => void;
children: React.ReactNode;

View file

@ -0,0 +1,55 @@
import { cn } from "@rallly/ui";
import { Avatar, AvatarFallback, getColor } from "@rallly/ui/avatar";
import React from "react";
export function Participant({ children }: { children: React.ReactNode }) {
return <div className="flex min-w-0 items-center gap-x-2">{children}</div>;
}
export const ParticipantAvatar = ({
size = 20,
name,
}: {
size?: number;
name: string;
}) => {
const color = getColor(name);
return (
<Avatar size={size}>
<AvatarFallback className="text-xs" color={color}>
{name[0]}
</AvatarFallback>
</Avatar>
);
};
export const ParticipantName = ({
children,
}: {
children: React.ReactNode;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
const [isTruncated, setIsTruncated] = React.useState(false);
return (
<div
ref={ref}
onMouseEnter={() => {
if (ref.current) {
setIsTruncated(ref.current.scrollWidth > ref.current.clientWidth);
}
}}
onMouseLeave={() => {
if (isTruncated) {
setIsTruncated(false);
}
}}
className={cn("truncate text-sm font-medium", {
"hover:-translate-x-2 hover:cursor-pointer hover:overflow-visible hover:whitespace-nowrap hover:rounded-md hover:bg-white hover:p-2":
isTruncated,
})}
>
{children}
</div>
);
};

View file

@ -304,7 +304,13 @@ const DesktopPoll: React.FunctionComponent = () => {
return (
<ParticipantRow
key={i}
participant={participant}
participant={{
id: participant.id,
name: participant.name,
userId: participant.userId ?? undefined,
email: participant.email ?? undefined,
votes: participant.votes,
}}
editMode={
votingForm.watch("mode") === "edit" &&
votingForm.watch("participantId") ===

View file

@ -12,11 +12,12 @@ import { useTranslation } from "next-i18next";
import * as React from "react";
import { Controller } from "react-hook-form";
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Participant, ParticipantName } from "@/components/participant";
import { useVotingForm } from "@/components/poll/voting-form";
import { Trans } from "@/components/trans";
import { usePoll } from "../../poll-context";
import UserAvatar, { YouAvatar } from "../user-avatar";
import { toggleVote, VoteSelector } from "../vote-selector";
export interface ParticipantRowFormProps {
@ -30,7 +31,6 @@ export interface ParticipantRowFormProps {
const ParticipantRowForm = ({
name,
isNew,
isYou,
className,
}: ParticipantRowFormProps) => {
const { t } = useTranslation();
@ -50,6 +50,8 @@ const ParticipantRowForm = ({
};
}, [form]);
const participantName = name ?? t("you");
return (
<tr className={cn("group", className)}>
<td
@ -57,11 +59,10 @@ const ParticipantRowForm = ({
className="sticky left-0 z-10 h-12 bg-white px-4"
>
<div className="flex items-center justify-between gap-x-2.5">
{name ? (
<UserAvatar name={name ?? t("you")} isYou={isYou} showName={true} />
) : (
<YouAvatar />
)}
<Participant>
<OptimizedAvatarImage name={participantName} size={20} />
<ParticipantName>{participantName}</ParticipantName>
</Participant>
{!isNew ? (
<div className="flex items-center gap-1">
<Tooltip>

View file

@ -1,22 +1,34 @@
import { Participant, VoteType } from "@rallly/database";
import type { VoteType } from "@rallly/database";
import { cn } from "@rallly/ui";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { Icon } from "@rallly/ui/icon";
import { MoreHorizontalIcon } from "lucide-react";
import * as React from "react";
import {
Participant,
ParticipantAvatar,
ParticipantName,
} from "@/components/participant";
import { ParticipantDropdown } from "@/components/participant-dropdown";
import { usePoll } from "@/components/poll-context";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
import { usePermissions } from "@/contexts/permissions";
import { Vote } from "@/utils/trpc/types";
import UserAvatar from "../user-avatar";
import VoteIcon from "../vote-icon";
import ParticipantRowForm from "./participant-row-form";
export interface ParticipantRowProps {
participant: Participant & { votes: Vote[] };
participant: {
id: string;
name: string;
userId?: string;
email?: string;
votes: Vote[];
};
className?: string;
editMode?: boolean;
onChangeEditMode?: (editMode: boolean) => void;
@ -41,7 +53,15 @@ export const ParticipantRowView: React.FunctionComponent<{
className="sticky left-0 z-10 h-12 bg-white px-4"
>
<div className="flex max-w-full items-center justify-between gap-x-4">
<UserAvatar name={name} showName={true} isYou={isYou} />
<Participant>
<ParticipantAvatar name={name} />
<ParticipantName>{name}</ParticipantName>
{isYou ? (
<Badge>
<Trans i18nKey="you" />
</Badge>
) : null}
</Participant>
{action}
</div>
</td>

View file

@ -16,6 +16,8 @@ import * as React from "react";
import smoothscroll from "smoothscroll-polyfill";
import { TimesShownIn } from "@/components/clock";
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
import { Participant, ParticipantName } from "@/components/participant";
import { ParticipantDropdown } from "@/components/participant-dropdown";
import { useVotingForm } from "@/components/poll/voting-form";
import { useOptions, usePoll } from "@/components/poll-context";
@ -25,7 +27,6 @@ import { usePermissions } from "@/contexts/permissions";
import { useVisibleParticipants } from "../participants-provider";
import { useUser } from "../user-provider";
import GroupedOptions from "./mobile-poll/grouped-options";
import UserAvatar, { YouAvatar } from "./user-avatar";
if (typeof window !== "undefined") {
smoothscroll.polyfill();
@ -100,11 +101,18 @@ const MobilePoll: React.FunctionComponent = () => {
{visibleParticipants.map((participant) => (
<SelectItem key={participant.id} value={participant.id}>
<div className="flex items-center gap-x-2.5">
<UserAvatar
name={participant.name}
showName={true}
isYou={session.ownsObject(participant)}
/>
<Participant>
<OptimizedAvatarImage
name={participant.name}
size={20}
/>
<ParticipantName>{participant.name}</ParticipantName>
{session.ownsObject(participant) && (
<Badge>
<Trans i18nKey="you" />
</Badge>
)}
</Participant>
</div>
</SelectItem>
))}
@ -112,7 +120,10 @@ const MobilePoll: React.FunctionComponent = () => {
</Select>
) : (
<div className="flex grow items-center px-1">
<YouAvatar />
<Participant>
<OptimizedAvatarImage name={t("you")} size={20} />
<ParticipantName>{t("you")}</ParticipantName>
</Participant>
</div>
)}
{isEditing ? (
@ -131,7 +142,12 @@ const MobilePoll: React.FunctionComponent = () => {
<ParticipantDropdown
align="end"
disabled={!canEditParticipant(selectedParticipant.id)}
participant={selectedParticipant}
participant={{
name: selectedParticipant.name,
userId: selectedParticipant.userId ?? undefined,
email: selectedParticipant.email ?? undefined,
id: selectedParticipant.id,
}}
onEdit={() => {
votingForm.setEditingParticipantId(selectedParticipant.id);
}}

View file

@ -8,8 +8,8 @@ import * as React from "react";
import { useToggle } from "react-use";
import { useTranslation } from "@/app/i18n/client";
import { ParticipantAvatar } from "@/components/participant";
import { useParticipants } from "@/components/participants-provider";
import { UserAvatar } from "@/components/user";
import { usePoll } from "@/contexts/poll";
import { useRole } from "@/contexts/role";
@ -51,11 +51,11 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
{participantsWhoVotedYes.map(({ name }, i) => (
<div key={i} className="flex">
<div className="relative mr-2.5 flex size-5 items-center justify-center">
<UserAvatar size="xs" name={name} />
<ParticipantAvatar size={20} name={name} />
<VoteIcon
type="yes"
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 className="truncate text-sm">{name}</div>
@ -66,7 +66,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
{participantsWhoVotedIfNeedBe.map(({ name }, i) => (
<div key={i} className="flex">
<div className="relative mr-2.5 flex size-5 items-center justify-center">
<UserAvatar size="xs" name={name} />
<ParticipantAvatar size={20} name={name} />
<VoteIcon
type="ifNeedBe"
size="sm"
@ -79,7 +79,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
{participantsWhoVotedNo.map(({ name }, i) => (
<div key={i} className="flex">
<div className="relative mr-2.5 flex size-5 items-center justify-center">
<UserAvatar size="xs" name={name} />
<ParticipantAvatar size={20} name={name} />
<VoteIcon
type="no"
size="sm"

View file

@ -1,5 +1,7 @@
"use client";
import { CalendarIcon } from "lucide-react";
import { AddToCalendarButton } from "@/components/add-to-calendar-button";
import { ParticipantAvatarBar } from "@/components/participant-avatar-bar";
import { useVisibleParticipants } from "@/components/participants-provider";
@ -11,7 +13,9 @@ import { useDayjs } from "@/utils/dayjs";
function FinalDate({ start }: { start: Date }) {
const poll = usePoll();
const { adjustTimeZone } = useDayjs();
return <span>{adjustTimeZone(start, !poll.timeZone).format("LL")}</span>;
return (
<span>{adjustTimeZone(start, !poll.timeZone).format("dddd, LL")}</span>
);
}
function DateIcon({ start }: { start: Date }) {
@ -20,7 +24,7 @@ function DateIcon({ start }: { start: Date }) {
const d = adjustTimeZone(start, !poll.timeZone);
return (
<time
className="inline-flex size-12 flex-col rounded-lg border border-green-600/10 bg-green-600/5 text-center text-green-800/75"
className="inline-flex size-12 flex-col rounded-lg border text-center"
dateTime={d.toISOString()}
>
<div className="border-b border-green-600/10 p-px text-xs">
@ -74,9 +78,10 @@ export function ScheduledEvent() {
return (
<>
<div className="rounded-lg border border-green-400/20 bg-gradient-to-r from-green-200/15 to-green-200/5 p-0.5 shadow-sm">
<div className="flex items-center gap-x-2 rounded-md border-b bg-green-500/10 p-3">
<h2 className="text-sm font-medium text-green-800">
<div className="rounded-lg border bg-white p-0.5 shadow-sm">
<div className="flex h-9 items-center gap-x-2 rounded-md bg-gray-100 px-2">
<CalendarIcon className="size-4" />
<h2 className="text-sm font-medium">
<Trans i18nKey="schedulateDate" defaults="Scheduled Date" />
</h2>
</div>
@ -85,7 +90,7 @@ export function ScheduledEvent() {
<div>
<DateIcon start={event.start} />
</div>
<div className="items-center gap-x-4 text-green-800">
<div className="items-center gap-x-4">
<div className="space-y-1">
<div className="text-sm font-medium">
<FinalDate start={event.start} />

View file

@ -1,88 +0,0 @@
import { Badge } from "@rallly/ui/badge";
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import * as React from "react";
import { getRandomAvatarColor } from "@/utils/color-hash";
interface UserAvatarProps {
name: string;
className?: string;
size?: "default" | "large";
color?: string;
showName?: boolean;
isYou?: boolean;
}
export const ColoredAvatar = (props: {
seed?: string;
name: string;
className?: string;
}) => {
const { color, requiresDarkText } = getRandomAvatarColor(
props.seed ?? props.name,
);
return (
<div
className={clsx(
"inline-flex size-5 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold uppercase",
requiresDarkText ? "text-gray-800" : "text-white",
props.className,
)}
style={{
backgroundColor: color,
}}
>
{props.name[0]}
</div>
);
};
const UserAvatarInner: React.FunctionComponent<UserAvatarProps> = ({
name,
className,
}) => {
return <ColoredAvatar name={name} className={className} />;
};
const UserAvatar: React.FunctionComponent<UserAvatarProps> = ({
showName,
isYou,
className,
...forwardedProps
}) => {
const { t } = useTranslation();
if (!showName) {
return <UserAvatarInner className={className} {...forwardedProps} />;
}
return (
<div
className={clsx(
"inline-flex items-center gap-x-2.5 overflow-hidden",
className,
)}
>
<UserAvatarInner {...forwardedProps} />
<div className="min-w-0 truncate text-sm font-medium">
{forwardedProps.name}
</div>
{isYou ? <Badge>{t("you")}</Badge> : null}
</div>
);
};
export default UserAvatar;
export const YouAvatar = () => {
const { t } = useTranslation();
const you = t("you");
return (
<span className="inline-flex items-center gap-x-2.5 text-sm">
<span className="inline-flex size-5 items-center justify-center rounded-full bg-gray-200 text-xs font-semibold uppercase">
{you[0]}
</span>
{t("you")}
</span>
);
};

View file

@ -28,7 +28,7 @@ const userSchema = z.object({
export const UserContext = React.createContext<{
user: z.infer<typeof userSchema>;
refresh: (data?: Record<string, unknown>) => Promise<Session | null>;
ownsObject: (obj: { userId: string | null }) => boolean;
ownsObject: (obj: { userId?: string | null }) => boolean;
} | null>(null);
export const useUser = () => {

View file

@ -1,8 +0,0 @@
import { useTranslation } from "next-i18next";
import UserAvatar from "./poll/user-avatar";
export const You = () => {
const { t } = useTranslation();
return <UserAvatar name={t("you")} showName={true} />;
};

View file

@ -2,9 +2,24 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cva, type VariantProps } from "class-variance-authority";
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<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> & {
@ -35,16 +50,44 @@ const AvatarImage = React.forwardRef<
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const avatarFallbackVariants = cva(
"flex h-full w-full items-center justify-center rounded-full font-medium",
{
variants: {
color: {
indigo: "bg-indigo-50 text-indigo-600",
green: "bg-green-50 text-green-600",
blue: "bg-blue-50 text-blue-600",
purple: "bg-purple-50 text-purple-600",
emerald: "bg-emerald-50 text-emerald-600",
violet: "bg-violet-50 text-violet-600",
sky: "bg-sky-50 text-sky-600",
cyan: "bg-cyan-50 text-cyan-600",
pink: "bg-pink-50 text-pink-600",
},
},
defaultVariants: {
color: "indigo",
},
},
);
export function getColor(seed: string): AvatarColor {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
}
return avatarColors[Math.abs(hash) % avatarColors.length];
}
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> &
VariantProps<typeof avatarFallbackVariants>
>(({ className, color, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"bg-primary-100 text-primary-800 flex h-full w-full items-center justify-center rounded-full font-medium",
className,
)}
className={cn(avatarFallbackVariants({ color }), className)}
{...props}
/>
));