Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions components/common/OverlappingAvatars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TOOLTIP_STYLES } from "@/helpers/tooltip.helpers";
import useIsTouchDevice from "@/hooks/useIsTouchDevice";
import Link from "next/link";
import type { MouseEvent, ReactNode } from "react";
import { useId } from "react";
import { useId, useState } from "react";
import { Tooltip } from "react-tooltip";

interface OverlappingAvatarItem {
Expand Down Expand Up @@ -34,6 +34,41 @@ const SIZE_CLASS = {
md: "tw-h-7 tw-w-7",
} as const;

function AvatarContent({
pfpUrl,
ariaLabel,
fallback,
avatarRing,
}: {
pfpUrl: string | null;
ariaLabel?: string;
fallback?: string;
avatarRing: string;
}) {
const [imgError, setImgError] = useState(false);

if (!pfpUrl || imgError) {
return (
<div className="tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-rounded-full tw-bg-iron-700 tw-ring-[1.5px] tw-ring-black">
<span className="tw-text-[0.625rem] tw-font-semibold tw-text-iron-300">
{fallback ?? "?"}
</span>
</div>
);
}

return (
<img
src={getScaledImageUri(pfpUrl, ImageScale.W_AUTO_H_50)}
alt={ariaLabel ?? "Profile"}
loading="lazy"
decoding="async"
onError={() => setImgError(true)}
className={`tw-h-full tw-w-full tw-flex-shrink-0 tw-rounded-full ${avatarRing}`}
/>
);
}

export default function OverlappingAvatars({
items,
maxCount = 5,
Expand All @@ -60,20 +95,13 @@ export default function OverlappingAvatars({
else if (index === slice.length - 1) transformOrigin = "right center";
else transformOrigin = "center center";

const content = item.pfpUrl ? (
<img
src={getScaledImageUri(item.pfpUrl, ImageScale.W_AUTO_H_50)}
alt={item.ariaLabel ?? "Profile"}
loading="lazy"
decoding="async"
className={`tw-h-full tw-w-full tw-flex-shrink-0 tw-rounded-full ${avatarRing}`}
const content = (
<AvatarContent
pfpUrl={item.pfpUrl}
ariaLabel={item.ariaLabel}
fallback={item.fallback}
avatarRing={avatarRing}
/>
) : (
<div className="tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center tw-rounded-full tw-bg-iron-700 tw-ring-[1.5px] tw-ring-black">
<span className="tw-text-[0.625rem] tw-font-semibold tw-text-iron-300">
{item.fallback ?? "?"}
</span>
</div>
);

const showTooltip =
Expand Down
4 changes: 2 additions & 2 deletions components/user/rep/header/UserPageRepHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ export default function UserPageRepHeader({
<button
type="button"
onClick={() => setIsGrantRepOpen(true)}
className="tw-group tw-inline-flex tw-h-11 tw-cursor-pointer tw-items-center tw-justify-center tw-gap-x-1.5 tw-rounded-lg tw-border tw-border-dashed tw-border-white/10 tw-bg-transparent tw-px-4 tw-text-sm tw-font-medium tw-text-iron-400 tw-transition-all tw-duration-300 tw-ease-out hover:tw-border-white/30 hover:tw-bg-white/5 hover:tw-text-white"
className="tw-group tw-inline-flex tw-h-11 tw-cursor-pointer tw-items-center tw-justify-center tw-gap-x-1.5 tw-rounded-lg tw-border tw-border-dashed tw-border-white/15 tw-bg-white/[0.03] tw-px-4 tw-text-sm tw-font-medium tw-text-iron-400 tw-transition-all tw-duration-300 tw-ease-out hover:tw-border-white/30 hover:tw-bg-white/10 hover:tw-text-white"
>
<svg
className="-tw-ml-1 tw-h-4 tw-w-4 tw-text-iron-500 tw-transition-colors group-hover:tw-text-white"
className="-tw-ml-1 tw-h-4 tw-w-4 tw-text-iron-400 tw-transition-colors group-hover:tw-text-white"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
Expand Down
4 changes: 2 additions & 2 deletions components/user/rep/new-rep/GrantRepDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export default function GrantRepDialog({
onClose={onClose}
tabletModal
>
<div className="tw-px-4 sm:tw-px-6">
<div>
<UserPageRateWrapper profile={profile} type={RateMatter.REP}>
<UserPageRepNewRep
profile={profile}
repRates={repRates}
onSuccess={onClose}
/>
</UserPageRateWrapper>
<div className="tw-mt-3">
<div className="tw-mt-4 tw-px-4 sm:tw-px-6">
<button
onClick={onClose}
type="button"
Expand Down
83 changes: 39 additions & 44 deletions components/user/rep/new-rep/UserPageRepNewRepSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { commonApiFetch, commonApiPost } from "@/services/api/common-api";
import { useContext, useEffect, useRef, useState } from "react";
import { useClickAway, useDebounce, useKeyPressEvent } from "react-use";
import { AnimatePresence, motion } from "framer-motion";
import { AnimatePresence } from "framer-motion";
import type { ApiProfileRepRatesState } from "@/entities/IProfile";
import UserPageRepNewRepSearchDropdown from "./UserPageRepNewRepSearchDropdown";
import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader";
Expand Down Expand Up @@ -222,10 +222,7 @@ export default function UserPageRepNewRepSearch({
}

setCategoriesToDisplay(items);
if (debouncedValue.length && repSearch.length && !selectedCategory) {
setIsOpen(true);
}
}, [debouncedValue, repSearch, categories, matchingSearchLength, selectedCategory]);
}, [debouncedValue, categories, matchingSearchLength]);

const [repSearchState, setRepSearchState] = useState<RepSearchState>(
RepSearchState.MIN_LENGTH_ERROR
Expand Down Expand Up @@ -264,7 +261,7 @@ export default function UserPageRepNewRepSearch({
<div ref={listRef} className="tw-w-full">
<div className="tw-w-full tw-relative tw-bg-iron-950">
<div
className="tw-flex tw-items-center tw-flex-wrap tw-justify-between tw-gap-y-1.5 tw-gap-x-4">
className="tw-px-4 sm:tw-px-6 tw-flex tw-items-center tw-flex-wrap tw-justify-between tw-gap-y-1.5 tw-gap-x-4">
<div className="tw-flex tw-flex-wrap tw-items-center tw-gap-x-3 tw-gap-y-2 tw-text-xs tw-text-iron-500 tw-font-medium">
<span>
<span>Your available Rep:</span>
Expand All @@ -286,7 +283,7 @@ export default function UserPageRepNewRepSearch({
</span>
</div>
</div>
<div className="tw-flex tw-flex-col tw-items-stretch tw-gap-3 tw-mt-3">
<div className="tw-px-4 sm:tw-px-6 tw-flex tw-flex-col tw-items-stretch tw-gap-3 tw-mt-3">
<form
onSubmit={onSearchSubmit}
className="tw-w-full tw-relative">
Expand Down Expand Up @@ -320,36 +317,42 @@ export default function UserPageRepNewRepSearch({
</div>
)}
</div>
<AnimatePresence mode="wait" initial={false}>
{isOpen && (
<motion.div
className="tw-origin-top tw-absolute tw-left-0 tw-top-full tw-z-10 tw-mt-1 tw-w-full tw-rounded-lg tw-shadow-xl tw-bg-iron-950 tw-ring-1 tw-ring-black tw-ring-opacity-5"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.2 }}
onClick={(e) => e.stopPropagation()}>
<UserPageRepNewRepSearchDropdown
categories={categoriesToDisplay}
state={repSearchState}
minSearchLength={SEARCH_LENGTH.MIN}
maxSearchLength={SEARCH_LENGTH.MAX}
onRepSelect={onRepSelect}
/>
</motion.div>
)}
</AnimatePresence>
{isOpen && !selectedCategory && (
<div
className="tw-absolute tw-left-0 tw-right-0 tw-top-full tw-z-10 tw-mt-1 tw-p-2 tw-rounded-lg tw-bg-iron-900 tw-shadow-xl tw-ring-1 tw-ring-white/10"
onClick={(e) => e.stopPropagation()}>
<UserPageRepNewRepSearchDropdown
categories={categoriesToDisplay}
state={repSearchState}
minSearchLength={SEARCH_LENGTH.MIN}
maxSearchLength={SEARCH_LENGTH.MAX}
onRepSelect={onRepSelect}
/>
</div>
)}
</form>
<div className="tw-relative tw-flex tw-w-full">
<UserPageRateInput
value={amountStr}
onChange={setAmountStr}
minMax={minMaxValues}
isProxy={!!activeProfileProxy}
spanClassName="tw-flex tw-flex-col tw-items-center tw-justify-center tw-rounded-l-lg tw-border tw-border-solid tw-border-white/10 tw-bg-[#0A0A0A]/80 tw-px-3"
inputClassName="tw-form-input tw-appearance-none -tw-ml-px tw-block tw-w-full tw-rounded-l-none tw-rounded-r-lg tw-border tw-border-solid tw-border-white/10 tw-py-3 tw-px-3 tw-bg-[#0A0A0A]/80 tw-text-white tw-text-sm tw-font-medium lg:tw-font-semibold tw-caret-primary-400 placeholder:tw-text-iron-500 lg:placeholder:tw-text-iron-400 focus:tw-outline-none focus:tw-border-blue-500/50 tw-transition tw-duration-300 tw-ease-out"
/>
<div>
<div className="tw-relative tw-flex tw-w-full">
<UserPageRateInput
value={amountStr}
onChange={setAmountStr}
minMax={minMaxValues}
isProxy={!!activeProfileProxy}
spanClassName="tw-flex tw-flex-col tw-items-center tw-justify-center tw-rounded-l-lg tw-border tw-border-solid tw-border-white/10 tw-bg-[#0A0A0A]/80 tw-px-3"
inputClassName="tw-form-input tw-appearance-none -tw-ml-px tw-block tw-w-full tw-rounded-l-none tw-rounded-r-lg tw-border tw-border-solid tw-border-white/10 tw-py-3 tw-px-3 tw-bg-[#0A0A0A]/80 tw-text-white tw-text-sm tw-font-medium lg:tw-font-semibold tw-caret-primary-400 placeholder:tw-text-iron-500 lg:placeholder:tw-text-iron-400 focus:tw-outline-none focus:tw-border-blue-500/50 tw-transition tw-duration-300 tw-ease-out"
/>
</div>
{selectedCategory && (
<UserRateAdjustmentHelper
inLineValues={true}
originalValue={repState?.rater_contribution ?? 0}
adjustedValue={newRating}
adjustmentType="Rep"
/>
)}
</div>
</div>
<div className="tw-border-t tw-border-solid tw-border-iron-800/60 tw-border-b-0 tw-pt-4 tw-mt-4 tw-px-4 sm:tw-px-6">
<button
type="button"
disabled={isGrantDisabled}
Expand All @@ -358,7 +361,7 @@ export default function UserPageRepNewRepSearch({
isGrantDisabled
? "tw-cursor-not-allowed tw-opacity-50"
: "tw-cursor-pointer hover:tw-bg-primary-600 hover:tw-border-primary-600"
} tw-flex-shrink-0 tw-bg-primary-500 tw-border-primary-500 tw-px-4 tw-py-3 tw-text-sm tw-font-semibold tw-text-white tw-border tw-border-solid tw-rounded-lg tw-transition tw-duration-300 tw-ease-out`}>
} tw-w-full tw-flex-shrink-0 tw-bg-primary-500 tw-border-primary-500 tw-px-4 tw-py-3 tw-text-sm tw-font-semibold tw-text-white tw-border tw-border-solid tw-rounded-lg tw-transition tw-duration-300 tw-ease-out`}>
{mutating ? (
<div className="tw-w-12">
<CircleLoader />
Expand All @@ -368,14 +371,6 @@ export default function UserPageRepNewRepSearch({
)}
</button>
</div>
{selectedCategory && (
<UserRateAdjustmentHelper
inLineValues={true}
originalValue={repState?.rater_contribution ?? 0}
adjustedValue={newRating}
adjustmentType="Rep"
/>
)}
</div>
</div>
</div>
Expand Down
83 changes: 44 additions & 39 deletions components/user/rep/new-rep/UserPageRepNewRepSearchDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CircleLoader from "@/components/distribution-plan-tool/common/CircleLoader";
import { RepSearchState } from "./UserPageRepNewRepSearch";

export default function UserPageRepNewRepSearchDropdown({
Expand All @@ -13,46 +14,50 @@ export default function UserPageRepNewRepSearchDropdown({
readonly minSearchLength: number;
readonly maxSearchLength: number;
}) {
return (
<div className="tw-absolute tw-z-10 tw-mt-1 tw-overflow-hidden tw-max-w-full tw-w-full tw-rounded-md tw-bg-iron-900 tw-shadow-2xl tw-ring-1 tw-ring-white/10">
<div className="tw-py-1 tw-flow-root tw-max-h-[calc(240px+_-5vh)] tw-overflow-x-hidden tw-overflow-y-auto tw-scrollbar-thin tw-scrollbar-thumb-iron-500 tw-scrollbar-track-iron-800 desktop-hover:hover:tw-scrollbar-thumb-iron-300">
<ul className="tw-flex tw-flex-col tw-px-2 tw-mx-0 tw-mb-0 tw-list-none">
{state === RepSearchState.MIN_LENGTH_ERROR && (
<li className="tw-group tw-justify-between tw-w-full tw-flex tw-rounded-lg tw-relative tw-select-none tw-p-2 tw-text-iron-200 tw-font-normal tw-text-sm">
Type at least {minSearchLength} characters
</li>
)}
{state === RepSearchState.MAX_LENGTH_ERROR && (
<li className="tw-group tw-justify-between tw-w-full tw-flex tw-rounded-lg tw-relative tw-select-none tw-p-2 tw-text-iron-200 tw-font-normal tw-text-sm">
Type at most {maxSearchLength} characters
</li>
)}
{state === RepSearchState.LOADING && (
<li className="tw-group tw-text-iron-200 tw-text-sm tw-font-normal tw-justify-between tw-w-full tw-flex tw-rounded-lg tw-relative tw-cursor-pointer tw-select-none tw-p-2 hover:tw-bg-iron-700 tw-transition tw-duration-300 tw-ease-out">
Loading...
</li>
)}
{state === RepSearchState.HAVE_RESULTS &&
categories.map((category) => (
<li
key={category}
className="tw-group tw-text-white tw-justify-between tw-w-full tw-flex tw-rounded-lg tw-relative tw-cursor-pointer tw-select-none tw-px-2 tw-py-1 hover:tw-bg-iron-800 tw-transition tw-duration-300 tw-ease-out"
>
<button
onClick={(e) => {
e.stopPropagation();
onRepSelect(category);
}}
className="tw-bg-transparent tw-border-none tw-w-full tw-h-full tw-text-left"
>
<span className="tw-inline-block tw-text-sm tw-font-medium tw-text-white">
{category}
</span>
</button>
</li>
))}
</ul>
if (state === RepSearchState.MIN_LENGTH_ERROR) {
return (
<p className="tw-mb-0 tw-text-xs tw-text-iron-500 tw-font-normal">
Type at least {minSearchLength} characters
</p>
);
}

if (state === RepSearchState.MAX_LENGTH_ERROR) {
return (
<p className="tw-mb-0 tw-text-xs tw-text-iron-500 tw-font-normal">
Type at most {maxSearchLength} characters
</p>
);
}

if (state === RepSearchState.LOADING) {
return (
<div className="tw-flex tw-items-center tw-gap-2 tw-text-xs tw-text-iron-400">
<CircleLoader />
<span>Searching...</span>
</div>
);
}

if (!categories.length) {
return null;
}

return (
<div className="tw-flex tw-flex-wrap tw-gap-2.5">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={(e) => {
e.stopPropagation();
onRepSelect(category);
}}
className="tw-px-3 tw-py-2 tw-rounded-lg tw-bg-white/5 tw-text-sm tw-font-medium tw-text-white tw-border tw-border-solid tw-border-white/10 tw-cursor-pointer hover:tw-bg-white/10 hover:tw-border-white/20 tw-transition tw-duration-200 tw-ease-out"
>
{category}
</button>
))}
</div>
);
}