mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-11 07:01: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 { 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 { useUser } from "@/components/user-provider";
|
||||
import { useAvatarsEnabled } from "@/features/avatars";
|
||||
|
@ -189,9 +189,14 @@ function Upload() {
|
|||
}
|
||||
|
||||
export function ProfilePicture() {
|
||||
const { user } = useUser();
|
||||
return (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<CurrentUserAvatar size={56} />
|
||||
<OptimizedAvatarImage
|
||||
src={user.image ?? undefined}
|
||||
name={user.name}
|
||||
size="lg"
|
||||
/>
|
||||
<Upload />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import Link from "next/link";
|
||||
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 { ProBadge } from "@/components/pro-badge";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
@ -171,7 +171,11 @@ export function Sidebar() {
|
|||
>
|
||||
<Link href="/settings/profile">
|
||||
<div>
|
||||
<CurrentUserAvatar size={40} />
|
||||
<OptimizedAvatarImage
|
||||
src={user.image ?? undefined}
|
||||
name={user.name}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-1 grid grow">
|
||||
<span className="font-semibold">{user.name}</span>
|
||||
|
|
|
@ -57,7 +57,7 @@ export const LoginPage = ({ magicLink, email }: PageProps) => {
|
|||
<OptimizedAvatarImage
|
||||
src={data?.image ?? undefined}
|
||||
name={data?.name ?? ""}
|
||||
size={56}
|
||||
size="xl"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<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,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
import { Flex } from "@rallly/ui/flex";
|
||||
import { useToast } from "@rallly/ui/hooks/use-toast";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { Input } from "@rallly/ui/input";
|
||||
|
@ -29,11 +30,8 @@ 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 { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||
import { ParticipantName } from "@/components/participant";
|
||||
import { useParticipants } from "@/components/participants-provider";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { usePermissions } from "@/contexts/permissions";
|
||||
|
@ -220,15 +218,18 @@ function DiscussionInner() {
|
|||
<div className="" key={comment.id}>
|
||||
<div data-testid="comment">
|
||||
<div className="mb-1 flex items-center space-x-2">
|
||||
<Participant>
|
||||
<ParticipantAvatar name={comment.authorName} />
|
||||
<Flex gap="sm">
|
||||
<OptimizedAvatarImage
|
||||
name={comment.authorName}
|
||||
size="xs"
|
||||
/>
|
||||
<ParticipantName>{comment.authorName}</ParticipantName>
|
||||
{session.ownsObject(comment) ? (
|
||||
<Badge>
|
||||
<Trans i18nKey="you" />
|
||||
</Badge>
|
||||
) : null}
|
||||
</Participant>
|
||||
</Flex>
|
||||
<div className="flex items-center gap-2 text-sm ">
|
||||
<div className="text-gray-500">
|
||||
{dayjs(comment.createdAt).fromNow()}
|
||||
|
@ -257,7 +258,7 @@ function DiscussionInner() {
|
|||
)}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,13 @@ import { Avatar, AvatarFallback, AvatarImage } from "@rallly/ui/avatar";
|
|||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
import { useAvatarsEnabled } from "@/features/avatars";
|
||||
const sizeToWidth = {
|
||||
xs: 20,
|
||||
sm: 24,
|
||||
md: 36,
|
||||
lg: 48,
|
||||
xl: 56,
|
||||
};
|
||||
|
||||
export function OptimizedAvatarImage({
|
||||
size,
|
||||
|
@ -12,19 +18,21 @@ export function OptimizedAvatarImage({
|
|||
src,
|
||||
name,
|
||||
}: {
|
||||
size: number;
|
||||
size: "xs" | "sm" | "md" | "lg" | "xl";
|
||||
src?: string;
|
||||
name: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const isAvatarsEnabled = useAvatarsEnabled();
|
||||
const [isLoaded, setLoaded] = React.useState(false);
|
||||
return (
|
||||
<Avatar className={className} style={{ width: size, height: size }}>
|
||||
<Avatar
|
||||
className={className}
|
||||
style={{ width: sizeToWidth[size], height: sizeToWidth[size] }}
|
||||
>
|
||||
{src ? (
|
||||
src.startsWith("https") ? (
|
||||
<AvatarImage src={src} alt={name} />
|
||||
) : isAvatarsEnabled ? (
|
||||
) : (
|
||||
<Image
|
||||
src={`/api/storage/${src}`}
|
||||
width={128}
|
||||
|
@ -35,14 +43,17 @@ export function OptimizedAvatarImage({
|
|||
setLoaded(true);
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
) : null}
|
||||
{!src || !isLoaded ? (
|
||||
<AvatarFallback
|
||||
seed={name}
|
||||
className={cn({
|
||||
"text-xs": size <= 24,
|
||||
"text-lg": size >= 48,
|
||||
className={cn("shrink-0", {
|
||||
"text-xs": size === "xs",
|
||||
"text-sm": size === "sm",
|
||||
"text-md": size === "md",
|
||||
"text-lg": size === "lg",
|
||||
"text-xl": size === "xl",
|
||||
})}
|
||||
>
|
||||
{name[0]?.toUpperCase()}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { cn } from "@rallly/ui";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@rallly/ui/tooltip";
|
||||
|
||||
import { ParticipantAvatar } from "@/components/participant";
|
||||
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||
|
||||
interface ParticipantAvatarBarProps {
|
||||
participants: { name: string }[];
|
||||
|
@ -20,7 +20,7 @@ export const ParticipantAvatarBar = ({
|
|||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<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>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{participant.name}</TooltipContent>
|
||||
|
|
|
@ -34,6 +34,7 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||
import { useMount } from "react-use";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||
import { useDeleteParticipantMutation } from "@/components/poll/mutations";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { useFormValidation } from "@/utils/form-validation";
|
||||
|
@ -75,13 +76,18 @@ export const ParticipantDropdown = ({
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align={align}>
|
||||
<DropdownMenuLabel>
|
||||
<div className="grid gap-0.5">
|
||||
<div>{participant.name}</div>
|
||||
{participant.email ? (
|
||||
<div className="text-muted-foreground text-xs font-normal">
|
||||
{participant.email}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div>
|
||||
<OptimizedAvatarImage name={participant.name} size="md" />
|
||||
</div>
|
||||
<div className="grid gap-0.5">
|
||||
<div>{participant.name}</div>
|
||||
{participant.email ? (
|
||||
<div className="text-muted-foreground text-xs font-normal">
|
||||
{participant.email}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
const sizeToWidth = {
|
||||
xs: 20,
|
||||
sm: 24,
|
||||
md: 32,
|
||||
lg: 48,
|
||||
};
|
||||
|
||||
export const ParticipantAvatar = ({
|
||||
size = 20,
|
||||
size = "md",
|
||||
name,
|
||||
}: {
|
||||
size?: number;
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
name: string;
|
||||
}) => {
|
||||
return (
|
||||
<Avatar size={size}>
|
||||
<AvatarFallback className="text-xs" seed={name}>
|
||||
<Avatar size={sizeToWidth[size]}>
|
||||
<AvatarFallback
|
||||
className={cn({
|
||||
"text-xs": size === "xs",
|
||||
"text-sm": size === "sm",
|
||||
"text-md": size === "md",
|
||||
"text-lg": size === "lg",
|
||||
})}
|
||||
seed={name}
|
||||
>
|
||||
{name[0]?.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
|
@ -62,7 +62,7 @@ const ParticipantRowForm = ({
|
|||
<div className="flex items-center justify-between gap-x-2.5">
|
||||
<Participant>
|
||||
{name ? (
|
||||
<OptimizedAvatarImage name={participantName} size={20} />
|
||||
<OptimizedAvatarImage name={participantName} size="xs" />
|
||||
) : (
|
||||
<YouAvatar />
|
||||
)}
|
||||
|
|
|
@ -2,15 +2,13 @@ import type { VoteType } from "@rallly/database";
|
|||
import { cn } from "@rallly/ui";
|
||||
import { Badge } from "@rallly/ui/badge";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Flex } from "@rallly/ui/flex";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
Participant,
|
||||
ParticipantAvatar,
|
||||
ParticipantName,
|
||||
} from "@/components/participant";
|
||||
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||
import { ParticipantName } from "@/components/participant";
|
||||
import { ParticipantDropdown } from "@/components/participant-dropdown";
|
||||
import { usePoll } from "@/components/poll-context";
|
||||
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"
|
||||
>
|
||||
<div className="flex max-w-full items-center justify-between gap-x-4">
|
||||
<Participant>
|
||||
<ParticipantAvatar name={name} />
|
||||
<ParticipantName>{name}</ParticipantName>
|
||||
{isYou ? (
|
||||
<Badge>
|
||||
<Trans i18nKey="you" />
|
||||
</Badge>
|
||||
) : null}
|
||||
</Participant>
|
||||
{action}
|
||||
<div>
|
||||
<Flex gap="sm">
|
||||
<OptimizedAvatarImage size="xs" name={name} />
|
||||
<ParticipantName>{name}</ParticipantName>
|
||||
{isYou ? (
|
||||
<Badge>
|
||||
<Trans i18nKey="you" />
|
||||
</Badge>
|
||||
) : null}
|
||||
</Flex>
|
||||
</div>
|
||||
<div>{action}</div>
|
||||
</div>
|
||||
</td>
|
||||
{votes.map((vote, i) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Badge } from "@rallly/ui/badge";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import { Card, CardFooter, CardHeader, CardTitle } from "@rallly/ui/card";
|
||||
import { Flex } from "@rallly/ui/flex";
|
||||
import { Icon } from "@rallly/ui/icon";
|
||||
import {
|
||||
Select,
|
||||
|
@ -101,20 +102,15 @@ const MobilePoll: React.FunctionComponent = () => {
|
|||
</SelectItem>
|
||||
{visibleParticipants.map((participant) => (
|
||||
<SelectItem key={participant.id} value={participant.id}>
|
||||
<div className="flex items-center gap-x-2.5">
|
||||
<Participant>
|
||||
<OptimizedAvatarImage
|
||||
name={participant.name}
|
||||
size={20}
|
||||
/>
|
||||
<ParticipantName>{participant.name}</ParticipantName>
|
||||
{session.ownsObject(participant) && (
|
||||
<Badge>
|
||||
<Trans i18nKey="you" />
|
||||
</Badge>
|
||||
)}
|
||||
</Participant>
|
||||
</div>
|
||||
<Flex gap="sm">
|
||||
<OptimizedAvatarImage name={participant.name} size="xs" />
|
||||
<ParticipantName>{participant.name}</ParticipantName>
|
||||
{session.ownsObject(participant) && (
|
||||
<Badge>
|
||||
<Trans i18nKey="you" />
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
@ -8,7 +8,7 @@ import * as React from "react";
|
|||
import { useToggle } from "react-use";
|
||||
|
||||
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 { usePoll } from "@/contexts/poll";
|
||||
import { useRole } from "@/contexts/role";
|
||||
|
@ -51,7 +51,7 @@ 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">
|
||||
<ParticipantAvatar size={20} name={name} />
|
||||
<OptimizedAvatarImage size="xs" name={name} />
|
||||
<VoteIcon
|
||||
type="yes"
|
||||
size="sm"
|
||||
|
@ -64,7 +64,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">
|
||||
<ParticipantAvatar size={20} name={name} />
|
||||
<OptimizedAvatarImage size="xs" 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">
|
||||
<ParticipantAvatar size={20} name={name} />
|
||||
<OptimizedAvatarImage size="xs" name={name} />
|
||||
<VoteIcon
|
||||
type="no"
|
||||
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";
|
||||
import Link from "next/link";
|
||||
|
||||
import { CurrentUserAvatar } from "@/components/current-user-avatar";
|
||||
import { LoginLink } from "@/components/login-link";
|
||||
import { OptimizedAvatarImage } from "@/components/optimized-avatar-image";
|
||||
import { RegisterLink } from "@/components/register-link";
|
||||
import { Trans } from "@/components/trans";
|
||||
import { IfCloudHosted, IfSelfHosted } from "@/contexts/environment";
|
||||
|
@ -57,7 +57,11 @@ export const UserDropdown = ({ className }: { className?: string }) => {
|
|||
className={cn("group min-w-0", className)}
|
||||
>
|
||||
<Button variant="ghost">
|
||||
<CurrentUserAvatar size={24} />
|
||||
<OptimizedAvatarImage
|
||||
src={user.image ?? undefined}
|
||||
name={user.name}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="truncate">{user.name}</span>
|
||||
<Icon>
|
||||
<ChevronDownIcon />
|
||||
|
|
|
@ -39,8 +39,11 @@ const flexVariants = cva("box-border flex justify-start", {
|
|||
},
|
||||
gap: {
|
||||
none: "gap-0",
|
||||
md: "gap-2.5",
|
||||
lg: "gap-4",
|
||||
xs: "gap-1",
|
||||
sm: "gap-2",
|
||||
md: "gap-4",
|
||||
lg: "gap-6",
|
||||
xl: "gap-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
@ -48,7 +51,7 @@ const flexVariants = cva("box-border flex justify-start", {
|
|||
align: "center",
|
||||
justify: "start",
|
||||
wrap: "noWrap",
|
||||
gap: "md",
|
||||
gap: "none",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue