mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-18 02:21:49 +02:00
♻️ Standard avatar sizes (#1375)
This commit is contained in:
parent
b0e0a8f09c
commit
3c340bdf90
16 changed files with 120 additions and 275 deletions
|
@ -5,7 +5,7 @@ import React, { useState } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { useTranslation } from "@/app/i18n/client";
|
import { useTranslation } from "@/app/i18n/client";
|
||||||
import { CurrentUserAvatar } from "@/components/current-user-avatar";
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useUser } from "@/components/user-provider";
|
import { useUser } from "@/components/user-provider";
|
||||||
import { useAvatarsEnabled } from "@/features/avatars";
|
import { useAvatarsEnabled } from "@/features/avatars";
|
||||||
|
@ -189,9 +189,14 @@ function Upload() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfilePicture() {
|
export function ProfilePicture() {
|
||||||
|
const { user } = useUser();
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<CurrentUserAvatar size={56} />
|
<OptimizedAvatarImage
|
||||||
|
src={user.image ?? undefined}
|
||||||
|
name={user.name}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
<Upload />
|
<Upload />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { CurrentUserAvatar } from "@/components/current-user-avatar";
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { PayWallDialog } from "@/components/pay-wall-dialog";
|
import { PayWallDialog } from "@/components/pay-wall-dialog";
|
||||||
import { ProBadge } from "@/components/pro-badge";
|
import { ProBadge } from "@/components/pro-badge";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -171,7 +171,11 @@ export function Sidebar() {
|
||||||
>
|
>
|
||||||
<Link href="/settings/profile">
|
<Link href="/settings/profile">
|
||||||
<div>
|
<div>
|
||||||
<CurrentUserAvatar size={40} />
|
<OptimizedAvatarImage
|
||||||
|
src={user.image ?? undefined}
|
||||||
|
name={user.name}
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-1 grid grow">
|
<span className="ml-1 grid grow">
|
||||||
<span className="font-semibold">{user.name}</span>
|
<span className="font-semibold">{user.name}</span>
|
||||||
|
|
|
@ -57,7 +57,7 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
|
||||||
<OptimizedAvatarImage
|
<OptimizedAvatarImage
|
||||||
src={data?.image ?? undefined}
|
src={data?.image ?? undefined}
|
||||||
name={data?.name ?? ""}
|
name={data?.name ?? ""}
|
||||||
size={56}
|
size="xl"
|
||||||
/>
|
/>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="mb-1 h-6 font-medium">
|
<div className="mb-1 h-6 font-medium">
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
|
||||||
import { useUser } from "@/components/user-provider";
|
|
||||||
|
|
||||||
export const CurrentUserAvatar = ({
|
|
||||||
size,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
size: number;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const { user } = useUser();
|
|
||||||
return (
|
|
||||||
<OptimizedAvatarImage
|
|
||||||
className={className}
|
|
||||||
src={user.image ?? undefined}
|
|
||||||
name={user.name}
|
|
||||||
size={size}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@rallly/ui/dropdown-menu";
|
} from "@rallly/ui/dropdown-menu";
|
||||||
|
import { Flex } from "@rallly/ui/flex";
|
||||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
import { useToast } from "@rallly/ui/hooks/use-toast";
|
||||||
import { Icon } from "@rallly/ui/icon";
|
import { Icon } from "@rallly/ui/icon";
|
||||||
import { Input } from "@rallly/ui/input";
|
import { Input } from "@rallly/ui/input";
|
||||||
|
@ -29,11 +30,8 @@ import { useTranslation } from "next-i18next";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import {
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
Participant,
|
import { ParticipantName } from "@/components/participant";
|
||||||
ParticipantAvatar,
|
|
||||||
ParticipantName,
|
|
||||||
} from "@/components/participant";
|
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { usePermissions } from "@/contexts/permissions";
|
import { usePermissions } from "@/contexts/permissions";
|
||||||
|
@ -220,15 +218,18 @@ function DiscussionInner() {
|
||||||
<div className="" key={comment.id}>
|
<div className="" key={comment.id}>
|
||||||
<div data-testid="comment">
|
<div data-testid="comment">
|
||||||
<div className="mb-1 flex items-center space-x-2">
|
<div className="mb-1 flex items-center space-x-2">
|
||||||
<Participant>
|
<Flex gap="sm">
|
||||||
<ParticipantAvatar name={comment.authorName} />
|
<OptimizedAvatarImage
|
||||||
|
name={comment.authorName}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
<ParticipantName>{comment.authorName}</ParticipantName>
|
<ParticipantName>{comment.authorName}</ParticipantName>
|
||||||
{session.ownsObject(comment) ? (
|
{session.ownsObject(comment) ? (
|
||||||
<Badge>
|
<Badge>
|
||||||
<Trans i18nKey="you" />
|
<Trans i18nKey="you" />
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</Participant>
|
</Flex>
|
||||||
<div className="flex items-center gap-2 text-sm ">
|
<div className="flex items-center gap-2 text-sm ">
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
{dayjs(comment.createdAt).fromNow()}
|
{dayjs(comment.createdAt).fromNow()}
|
||||||
|
@ -257,7 +258,7 @@ function DiscussionInner() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-0.5 w-fit whitespace-pre-wrap pl-7 text-sm leading-relaxed">
|
<div className="w-fit whitespace-pre-wrap pl-7 text-sm leading-relaxed">
|
||||||
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,13 @@ import { Avatar, AvatarFallback, AvatarImage } from "@rallly/ui/avatar";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useAvatarsEnabled } from "@/features/avatars";
|
const sizeToWidth = {
|
||||||
|
xs: 20,
|
||||||
|
sm: 24,
|
||||||
|
md: 36,
|
||||||
|
lg: 48,
|
||||||
|
xl: 56,
|
||||||
|
};
|
||||||
|
|
||||||
export function OptimizedAvatarImage({
|
export function OptimizedAvatarImage({
|
||||||
size,
|
size,
|
||||||
|
@ -12,19 +18,21 @@ export function OptimizedAvatarImage({
|
||||||
src,
|
src,
|
||||||
name,
|
name,
|
||||||
}: {
|
}: {
|
||||||
size: number;
|
size: "xs" | "sm" | "md" | "lg" | "xl";
|
||||||
src?: string;
|
src?: string;
|
||||||
name: string;
|
name: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const isAvatarsEnabled = useAvatarsEnabled();
|
|
||||||
const [isLoaded, setLoaded] = React.useState(false);
|
const [isLoaded, setLoaded] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
<Avatar className={className} style={{ width: size, height: size }}>
|
<Avatar
|
||||||
|
className={className}
|
||||||
|
style={{ width: sizeToWidth[size], height: sizeToWidth[size] }}
|
||||||
|
>
|
||||||
{src ? (
|
{src ? (
|
||||||
src.startsWith("https") ? (
|
src.startsWith("https") ? (
|
||||||
<AvatarImage src={src} alt={name} />
|
<AvatarImage src={src} alt={name} />
|
||||||
) : isAvatarsEnabled ? (
|
) : (
|
||||||
<Image
|
<Image
|
||||||
src={`/api/storage/${src}`}
|
src={`/api/storage/${src}`}
|
||||||
width={128}
|
width={128}
|
||||||
|
@ -35,14 +43,17 @@ export function OptimizedAvatarImage({
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null
|
)
|
||||||
) : null}
|
) : null}
|
||||||
{!src || !isLoaded ? (
|
{!src || !isLoaded ? (
|
||||||
<AvatarFallback
|
<AvatarFallback
|
||||||
seed={name}
|
seed={name}
|
||||||
className={cn({
|
className={cn("shrink-0", {
|
||||||
"text-xs": size <= 24,
|
"text-xs": size === "xs",
|
||||||
"text-lg": size >= 48,
|
"text-sm": size === "sm",
|
||||||
|
"text-md": size === "md",
|
||||||
|
"text-lg": size === "lg",
|
||||||
|
"text-xl": size === "xl",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{name[0]?.toUpperCase()}
|
{name[0]?.toUpperCase()}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||||
|
|
||||||
import { ParticipantAvatar } from "@/components/participant";
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
|
|
||||||
interface ParticipantAvatarBarProps {
|
interface ParticipantAvatarBarProps {
|
||||||
participants: { name: string }[];
|
participants: { name: string }[];
|
||||||
|
@ -20,7 +20,7 @@ export const ParticipantAvatarBar = ({
|
||||||
<Tooltip key={index}>
|
<Tooltip key={index}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<li className="z-10 inline-flex items-center justify-center rounded-full ring-2 ring-white">
|
<li className="z-10 inline-flex items-center justify-center rounded-full ring-2 ring-white">
|
||||||
<ParticipantAvatar name={participant.name} />
|
<OptimizedAvatarImage name={participant.name} size="xs" />
|
||||||
</li>
|
</li>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{participant.name}</TooltipContent>
|
<TooltipContent>{participant.name}</TooltipContent>
|
||||||
|
|
|
@ -34,6 +34,7 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { useMount } from "react-use";
|
import { useMount } from "react-use";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { useDeleteParticipantMutation } from "@/components/poll/mutations";
|
import { useDeleteParticipantMutation } from "@/components/poll/mutations";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { useFormValidation } from "@/utils/form-validation";
|
import { useFormValidation } from "@/utils/form-validation";
|
||||||
|
@ -75,6 +76,10 @@ export const ParticipantDropdown = ({
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align={align}>
|
<DropdownMenuContent align={align}>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<div>
|
||||||
|
<OptimizedAvatarImage name={participant.name} size="md" />
|
||||||
|
</div>
|
||||||
<div className="grid gap-0.5">
|
<div className="grid gap-0.5">
|
||||||
<div>{participant.name}</div>
|
<div>{participant.name}</div>
|
||||||
{participant.email ? (
|
{participant.email ? (
|
||||||
|
@ -83,6 +88,7 @@ export const ParticipantDropdown = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={onEdit}>
|
<DropdownMenuItem onClick={onEdit}>
|
||||||
|
|
|
@ -6,16 +6,31 @@ export function Participant({ children }: { children: React.ReactNode }) {
|
||||||
return <div className="flex min-w-0 items-center gap-x-2">{children}</div>;
|
return <div className="flex min-w-0 items-center gap-x-2">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sizeToWidth = {
|
||||||
|
xs: 20,
|
||||||
|
sm: 24,
|
||||||
|
md: 32,
|
||||||
|
lg: 48,
|
||||||
|
};
|
||||||
|
|
||||||
export const ParticipantAvatar = ({
|
export const ParticipantAvatar = ({
|
||||||
size = 20,
|
size = "md",
|
||||||
name,
|
name,
|
||||||
}: {
|
}: {
|
||||||
size?: number;
|
size?: "xs" | "sm" | "md" | "lg";
|
||||||
name: string;
|
name: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Avatar size={size}>
|
<Avatar size={sizeToWidth[size]}>
|
||||||
<AvatarFallback className="text-xs" seed={name}>
|
<AvatarFallback
|
||||||
|
className={cn({
|
||||||
|
"text-xs": size === "xs",
|
||||||
|
"text-sm": size === "sm",
|
||||||
|
"text-md": size === "md",
|
||||||
|
"text-lg": size === "lg",
|
||||||
|
})}
|
||||||
|
seed={name}
|
||||||
|
>
|
||||||
{name[0]?.toUpperCase()}
|
{name[0]?.toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
|
@ -62,7 +62,7 @@ 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 ? (
|
{name ? (
|
||||||
<OptimizedAvatarImage name={participantName} size={20} />
|
<OptimizedAvatarImage name={participantName} size="xs" />
|
||||||
) : (
|
) : (
|
||||||
<YouAvatar />
|
<YouAvatar />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,15 +2,13 @@ import type { VoteType } from "@rallly/database";
|
||||||
import { cn } from "@rallly/ui";
|
import { cn } from "@rallly/ui";
|
||||||
import { Badge } from "@rallly/ui/badge";
|
import { Badge } from "@rallly/ui/badge";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
|
import { Flex } from "@rallly/ui/flex";
|
||||||
import { Icon } from "@rallly/ui/icon";
|
import { Icon } from "@rallly/ui/icon";
|
||||||
import { MoreHorizontalIcon } from "lucide-react";
|
import { MoreHorizontalIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import {
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
Participant,
|
import { ParticipantName } from "@/components/participant";
|
||||||
ParticipantAvatar,
|
|
||||||
ParticipantName,
|
|
||||||
} from "@/components/participant";
|
|
||||||
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
||||||
import { usePoll } from "@/components/poll-context";
|
import { usePoll } from "@/components/poll-context";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
|
@ -53,16 +51,18 @@ export const ParticipantRowView: React.FunctionComponent<{
|
||||||
className="sticky left-0 z-10 h-12 bg-white px-4"
|
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">
|
<div className="flex max-w-full items-center justify-between gap-x-4">
|
||||||
<Participant>
|
<div>
|
||||||
<ParticipantAvatar name={name} />
|
<Flex gap="sm">
|
||||||
|
<OptimizedAvatarImage size="xs" name={name} />
|
||||||
<ParticipantName>{name}</ParticipantName>
|
<ParticipantName>{name}</ParticipantName>
|
||||||
{isYou ? (
|
{isYou ? (
|
||||||
<Badge>
|
<Badge>
|
||||||
<Trans i18nKey="you" />
|
<Trans i18nKey="you" />
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</Participant>
|
</Flex>
|
||||||
{action}
|
</div>
|
||||||
|
<div>{action}</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{votes.map((vote, i) => {
|
{votes.map((vote, i) => {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Badge } from "@rallly/ui/badge";
|
import { Badge } from "@rallly/ui/badge";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import { Card, CardFooter, CardHeader, CardTitle } from "@rallly/ui/card";
|
import { Card, CardFooter, CardHeader, CardTitle } from "@rallly/ui/card";
|
||||||
|
import { Flex } from "@rallly/ui/flex";
|
||||||
import { Icon } from "@rallly/ui/icon";
|
import { Icon } from "@rallly/ui/icon";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
@ -101,20 +102,15 @@ const MobilePoll: React.FunctionComponent = () => {
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{visibleParticipants.map((participant) => (
|
{visibleParticipants.map((participant) => (
|
||||||
<SelectItem key={participant.id} value={participant.id}>
|
<SelectItem key={participant.id} value={participant.id}>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<Flex gap="sm">
|
||||||
<Participant>
|
<OptimizedAvatarImage name={participant.name} size="xs" />
|
||||||
<OptimizedAvatarImage
|
|
||||||
name={participant.name}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<ParticipantName>{participant.name}</ParticipantName>
|
<ParticipantName>{participant.name}</ParticipantName>
|
||||||
{session.ownsObject(participant) && (
|
{session.ownsObject(participant) && (
|
||||||
<Badge>
|
<Badge>
|
||||||
<Trans i18nKey="you" />
|
<Trans i18nKey="you" />
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</Participant>
|
</Flex>
|
||||||
</div>
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import * as React from "react";
|
||||||
import { useToggle } from "react-use";
|
import { useToggle } from "react-use";
|
||||||
|
|
||||||
import { useTranslation } from "@/app/i18n/client";
|
import { useTranslation } from "@/app/i18n/client";
|
||||||
import { ParticipantAvatar } from "@/components/participant";
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { useParticipants } from "@/components/participants-provider";
|
import { useParticipants } from "@/components/participants-provider";
|
||||||
import { usePoll } from "@/contexts/poll";
|
import { usePoll } from "@/contexts/poll";
|
||||||
import { useRole } from "@/contexts/role";
|
import { useRole } from "@/contexts/role";
|
||||||
|
@ -51,7 +51,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
|
||||||
{participantsWhoVotedYes.map(({ name }, i) => (
|
{participantsWhoVotedYes.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">
|
||||||
<ParticipantAvatar size={20} name={name} />
|
<OptimizedAvatarImage size="xs" name={name} />
|
||||||
<VoteIcon
|
<VoteIcon
|
||||||
type="yes"
|
type="yes"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -64,7 +64,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
|
||||||
{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">
|
||||||
<ParticipantAvatar size={20} name={name} />
|
<OptimizedAvatarImage size="xs" name={name} />
|
||||||
<VoteIcon
|
<VoteIcon
|
||||||
type="ifNeedBe"
|
type="ifNeedBe"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -79,7 +79,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({
|
||||||
{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">
|
||||||
<ParticipantAvatar size={20} name={name} />
|
<OptimizedAvatarImage size="xs" name={name} />
|
||||||
<VoteIcon
|
<VoteIcon
|
||||||
type="no"
|
type="no"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
import { cn } from "@rallly/ui";
|
|
||||||
import { Button } from "@rallly/ui/button";
|
|
||||||
import { Flex } from "@rallly/ui/flex";
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
OnChangeFn,
|
|
||||||
PaginationState,
|
|
||||||
SortingState,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { Trans } from "@/components/trans";
|
|
||||||
|
|
||||||
export const Table = <TData extends Record<string, unknown>>(props: {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
columns: ColumnDef<TData, any>[];
|
|
||||||
data: TData[];
|
|
||||||
footer?: React.ReactNode;
|
|
||||||
pageCount?: number;
|
|
||||||
enableTableFooter?: boolean;
|
|
||||||
enableTableHeader?: boolean;
|
|
||||||
layout?: "fixed" | "auto";
|
|
||||||
onPaginationChange?: OnChangeFn<PaginationState>;
|
|
||||||
sortingState?: SortingState;
|
|
||||||
onSortingChange?: OnChangeFn<SortingState>;
|
|
||||||
paginationState: PaginationState | undefined;
|
|
||||||
className?: string;
|
|
||||||
}) => {
|
|
||||||
const table = useReactTable<TData>({
|
|
||||||
data: props.data,
|
|
||||||
columns: props.columns,
|
|
||||||
pageCount: props.pageCount,
|
|
||||||
state: {
|
|
||||||
pagination: props.paginationState,
|
|
||||||
sorting: props.sortingState,
|
|
||||||
},
|
|
||||||
onSortingChange: props.onSortingChange,
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
manualPagination: true,
|
|
||||||
onPaginationChange: props.onPaginationChange,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
props.className,
|
|
||||||
"scrollbar-thin max-w-full overflow-x-auto",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<table
|
|
||||||
className={cn(
|
|
||||||
"border-collapse",
|
|
||||||
props.layout === "auto" ? "w-full table-auto" : "table-fixed",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{props.enableTableHeader ? (
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<tr key={headerGroup.id}>
|
|
||||||
{headerGroup.headers.map((header) => (
|
|
||||||
<th
|
|
||||||
key={header.id}
|
|
||||||
style={{
|
|
||||||
width: header.getSize(),
|
|
||||||
maxWidth:
|
|
||||||
props.layout === "auto"
|
|
||||||
? header.getSize()
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
className="text-muted-foreground h-9 whitespace-nowrap border-b px-2.5 text-left text-xs font-normal"
|
|
||||||
>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.header,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
) : null}
|
|
||||||
<tbody>
|
|
||||||
{table.getRowModel().rows.map((row, i) => (
|
|
||||||
<tr key={row.id}>
|
|
||||||
{row.getVisibleCells().map((cell) => (
|
|
||||||
<td
|
|
||||||
style={{
|
|
||||||
width: cell.column.getSize(),
|
|
||||||
maxWidth:
|
|
||||||
props.layout === "auto"
|
|
||||||
? cell.column.getSize()
|
|
||||||
: undefined,
|
|
||||||
}}
|
|
||||||
key={cell.id}
|
|
||||||
className={cn(
|
|
||||||
"relative h-14 overflow-hidden border-gray-100 px-2.5 align-middle",
|
|
||||||
{
|
|
||||||
"border-b": table.getRowModel().rows.length !== i + 1,
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
{props.enableTableFooter ? (
|
|
||||||
<tfoot>
|
|
||||||
{table.getFooterGroups().map((footerGroup) => (
|
|
||||||
<tr key={footerGroup.id} className="relative">
|
|
||||||
{footerGroup.headers.map((header) => (
|
|
||||||
<th className="border-t" key={header.id}>
|
|
||||||
{header.isPlaceholder
|
|
||||||
? null
|
|
||||||
: flexRender(
|
|
||||||
header.column.columnDef.footer,
|
|
||||||
header.getContext(),
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tfoot>
|
|
||||||
) : null}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{table.getPageCount() > 1 ? (
|
|
||||||
<div className="flex items-center justify-between space-x-2 border-t px-4 py-3 lg:px-5">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground text-sm">
|
|
||||||
<Trans
|
|
||||||
i18nKey="pageXOfY"
|
|
||||||
defaults="Page {currentPage} of {pageCount}"
|
|
||||||
values={{
|
|
||||||
currentPage: table.getState().pagination.pageIndex + 1,
|
|
||||||
pageCount: table.getPageCount(),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Flex>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon
|
|
||||||
className={cn("size-4", {
|
|
||||||
"text-gray-400": !table.getCanPreviousPage(),
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<ArrowRightIcon className="size-4 text-gray-500" />
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -26,8 +26,8 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { CurrentUserAvatar } from "@/components/current-user-avatar";
|
|
||||||
import { LoginLink } from "@/components/login-link";
|
import { LoginLink } from "@/components/login-link";
|
||||||
|
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||||
import { RegisterLink } from "@/components/register-link";
|
import { RegisterLink } from "@/components/register-link";
|
||||||
import { Trans } from "@/components/trans";
|
import { Trans } from "@/components/trans";
|
||||||
import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment";
|
import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment";
|
||||||
|
@ -57,7 +57,11 @@ export const UserDropdown = ({ className }: { className?: string }) => {
|
||||||
className={cn("group min-w-0", className)}
|
className={cn("group min-w-0", className)}
|
||||||
>
|
>
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<CurrentUserAvatar size={24} />
|
<OptimizedAvatarImage
|
||||||
|
src={user.image ?? undefined}
|
||||||
|
name={user.name}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
<span className="truncate">{user.name}</span>
|
<span className="truncate">{user.name}</span>
|
||||||
<Icon>
|
<Icon>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
|
|
|
@ -39,8 +39,11 @@ const flexVariants = cva("box-border flex justify-start", {
|
||||||
},
|
},
|
||||||
gap: {
|
gap: {
|
||||||
none: "gap-0",
|
none: "gap-0",
|
||||||
md: "gap-2.5",
|
xs: "gap-1",
|
||||||
lg: "gap-4",
|
sm: "gap-2",
|
||||||
|
md: "gap-4",
|
||||||
|
lg: "gap-6",
|
||||||
|
xl: "gap-8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
@ -48,7 +51,7 @@ const flexVariants = cva("box-border flex justify-start", {
|
||||||
align: "center",
|
align: "center",
|
||||||
justify: "start",
|
justify: "start",
|
||||||
wrap: "noWrap",
|
wrap: "noWrap",
|
||||||
gap: "md",
|
gap: "none",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue