From 4e7b391a9f3d494f58451bc1e0554e4972b6aa5b Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Sat, 14 Sep 2024 16:17:48 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Improve=20participant=20definiti?= =?UTF-8?q?on=20(#1337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/app/[locale]/(admin)/layout.tsx | 6 +- .../app/[locale]/(admin)/polls/user-polls.tsx | 71 +++++++-------- apps/web/src/app/components/page-layout.tsx | 2 +- .../src/components/discussion/discussion.tsx | 30 +++++-- apps/web/src/components/name-input.tsx | 47 ---------- .../src/components/optimized-avatar-image.tsx | 12 ++- .../src/components/participant-avatar-bar.tsx | 12 +-- .../src/components/participant-dropdown.tsx | 9 +- apps/web/src/components/participant.tsx | 55 ++++++++++++ apps/web/src/components/poll/desktop-poll.tsx | 8 +- .../desktop-poll/participant-row-form.tsx | 15 ++-- .../poll/desktop-poll/participant-row.tsx | 28 +++++- apps/web/src/components/poll/mobile-poll.tsx | 32 +++++-- .../poll/mobile-poll/poll-option.tsx | 10 +-- .../src/components/poll/scheduled-event.tsx | 17 ++-- apps/web/src/components/poll/user-avatar.tsx | 88 ------------------- apps/web/src/components/user-provider.tsx | 2 +- apps/web/src/components/you.tsx | 8 -- packages/ui/src/avatar.tsx | 55 ++++++++++-- 19 files changed, 269 insertions(+), 238 deletions(-) delete mode 100644 apps/web/src/components/name-input.tsx create mode 100644 apps/web/src/components/participant.tsx delete mode 100644 apps/web/src/components/poll/user-avatar.tsx delete mode 100644 apps/web/src/components/you.tsx diff --git a/apps/web/src/app/[locale]/(admin)/layout.tsx b/apps/web/src/app/[locale]/(admin)/layout.tsx index acb49b304..9aa545898 100644 --- a/apps/web/src/app/[locale]/(admin)/layout.tsx +++ b/apps/web/src/app/[locale]/(admin)/layout.tsx @@ -15,7 +15,7 @@ export default async function Layout({
-
- {children} +
+
{children}
diff --git a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx index a7e72b515..3081cd21c 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/user-polls.tsx @@ -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 ( -
- - -
- ); - } - return ( - + + + ) : ( + <> + + + + )} + ); } @@ -208,38 +211,36 @@ function PollsListView({ } return ( -
+
{table.getRowModel().rows.map((row) => (
-
-
- - -
-
-

+
+
+ +

{row.original.title}

- +
+
+ + + + + +
-
+
-

- -

))} diff --git a/apps/web/src/app/components/page-layout.tsx b/apps/web/src/app/components/page-layout.tsx index d09dc71fa..fc6f47541 100644 --- a/apps/web/src/app/components/page-layout.tsx +++ b/apps/web/src/app/components/page-layout.tsx @@ -50,7 +50,7 @@ export function PageHeader({ className?: string; variant?: "default" | "ghost"; }) { - return
{children}
; + return
{children}
; } export function PageSection({ children }: { children?: React.ReactNode }) { diff --git a/apps/web/src/components/discussion/discussion.tsx b/apps/web/src/components/discussion/discussion.tsx index 2232a4a99..c3443e6d2 100644 --- a/apps/web/src/components/discussion/discussion.tsx +++ b/apps/web/src/components/discussion/discussion.tsx @@ -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 }) => ( - + )} />

@@ -203,11 +213,15 @@ function DiscussionInner() {
- + + + {comment.authorName} + {session.ownsObject(comment) ? ( + + + + ) : null} +
{dayjs(comment.createdAt).fromNow()} diff --git a/apps/web/src/components/name-input.tsx b/apps/web/src/components/name-input.tsx deleted file mode 100644 index 867d64bcd..000000000 --- a/apps/web/src/components/name-input.tsx +++ /dev/null @@ -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(function ( - { value, defaultValue, className, error, ...forwardProps }, - ref, -) { - const { t } = useTranslation(); - return ( -
- {value ? ( - - ) : null} - -
- ); -}); - -NameInput.displayName = "NameInput"; - -export default NameInput; diff --git a/apps/web/src/components/optimized-avatar-image.tsx b/apps/web/src/components/optimized-avatar-image.tsx index dfe657fce..0764172d9 100644 --- a/apps/web/src/components/optimized-avatar-image.tsx +++ b/apps/web/src/components/optimized-avatar-image.tsx @@ -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 ? {name[0]} : null} + {!src || !isLoaded ? ( + = 48, + })} + > + {name[0]} + + ) : null} ); } diff --git a/apps/web/src/components/participant-avatar-bar.tsx b/apps/web/src/components/participant-avatar-bar.tsx index 13a3bde1c..cb436dff1 100644 --- a/apps/web/src/components/participant-avatar-bar.tsx +++ b/apps/web/src/components/participant-avatar-bar.tsx @@ -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 ( -
    +
      {participants.slice(0, visibleCount).map((participant, index) => ( -
    • - +
    • +
    • {participant.name}
      ))} {hiddenCount > 1 ? ( -
    • +
    • diff --git a/apps/web/src/components/participant-dropdown.tsx b/apps/web/src/components/participant-dropdown.tsx index 81272867b..76b09148f 100644 --- a/apps/web/src/components/participant-dropdown.tsx +++ b/apps/web/src/components/participant-dropdown.tsx @@ -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; diff --git a/apps/web/src/components/participant.tsx b/apps/web/src/components/participant.tsx new file mode 100644 index 000000000..c19ee50eb --- /dev/null +++ b/apps/web/src/components/participant.tsx @@ -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
      {children}
      ; +} + +export const ParticipantAvatar = ({ + size = 20, + name, +}: { + size?: number; + name: string; +}) => { + const color = getColor(name); + + return ( + + + {name[0]} + + + ); +}; + +export const ParticipantName = ({ + children, +}: { + children: React.ReactNode; +}) => { + const ref = React.useRef(null); + const [isTruncated, setIsTruncated] = React.useState(false); + return ( +
      { + 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} +
      + ); +}; diff --git a/apps/web/src/components/poll/desktop-poll.tsx b/apps/web/src/components/poll/desktop-poll.tsx index 351a6ce5a..6d03e3c3c 100644 --- a/apps/web/src/components/poll/desktop-poll.tsx +++ b/apps/web/src/components/poll/desktop-poll.tsx @@ -304,7 +304,13 @@ const DesktopPoll: React.FunctionComponent = () => { return ( { const { t } = useTranslation(); @@ -50,6 +50,8 @@ const ParticipantRowForm = ({ }; }, [form]); + const participantName = name ?? t("you"); + return (
      - {name ? ( - - ) : ( - - )} + + + {participantName} + {!isNew ? (
      diff --git a/apps/web/src/components/poll/desktop-poll/participant-row.tsx b/apps/web/src/components/poll/desktop-poll/participant-row.tsx index 371b07a19..4cb5b0b21 100644 --- a/apps/web/src/components/poll/desktop-poll/participant-row.tsx +++ b/apps/web/src/components/poll/desktop-poll/participant-row.tsx @@ -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" >
      - + + + {name} + {isYou ? ( + + + + ) : null} + {action}
      diff --git a/apps/web/src/components/poll/mobile-poll.tsx b/apps/web/src/components/poll/mobile-poll.tsx index 342159ec3..7b6aee7dd 100644 --- a/apps/web/src/components/poll/mobile-poll.tsx +++ b/apps/web/src/components/poll/mobile-poll.tsx @@ -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) => (
      - + + + {participant.name} + {session.ownsObject(participant) && ( + + + + )} +
      ))} @@ -112,7 +120,10 @@ const MobilePoll: React.FunctionComponent = () => { ) : (
      - + + + {t("you")} +
      )} {isEditing ? ( @@ -131,7 +142,12 @@ const MobilePoll: React.FunctionComponent = () => { { votingForm.setEditingParticipantId(selectedParticipant.id); }} diff --git a/apps/web/src/components/poll/mobile-poll/poll-option.tsx b/apps/web/src/components/poll/mobile-poll/poll-option.tsx index 00eb8f55a..b179f2dbf 100644 --- a/apps/web/src/components/poll/mobile-poll/poll-option.tsx +++ b/apps/web/src/components/poll/mobile-poll/poll-option.tsx @@ -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) => (
      - +
      {name}
      @@ -66,7 +66,7 @@ const PollOptionVoteSummary: React.FunctionComponent<{ optionId: string }> = ({ {participantsWhoVotedIfNeedBe.map(({ name }, i) => (
      - + = ({ {participantsWhoVotedNo.map(({ name }, i) => (
      - + {adjustTimeZone(start, !poll.timeZone).format("LL")}; + return ( + {adjustTimeZone(start, !poll.timeZone).format("dddd, LL")} + ); } function DateIcon({ start }: { start: Date }) { @@ -20,7 +24,7 @@ function DateIcon({ start }: { start: Date }) { const d = adjustTimeZone(start, !poll.timeZone); return (