diff --git a/components/atoms/Select/multi-select.tsx b/components/atoms/Select/multi-select.tsx index d05fa938b7..ef6fb43ccf 100644 --- a/components/atoms/Select/multi-select.tsx +++ b/components/atoms/Select/multi-select.tsx @@ -11,13 +11,17 @@ import { Command, CommandGroup, CommandInput, CommandItem } from "../Cmd/command export type OptionKeys = Record<"value" | "label", string>; interface MultiSelectProps { + open: boolean; + setOpen: React.Dispatch>; options: OptionKeys[]; selected: OptionKeys[]; + setSelected?: React.Dispatch>; handleSelect: (value: OptionKeys) => void; placeholder?: string; inputPlaceholder?: string; className?: string; handleKeyDown?: (e: React.KeyboardEvent) => void; + emptyState?: React.ReactNode; } const MultiSelect = ({ @@ -28,30 +32,20 @@ const MultiSelect = ({ placeholder, handleKeyDown, inputPlaceholder, + setSelected, + open, + setOpen, + emptyState, }: MultiSelectProps) => { const inputRef = useRef(null); - const [open, setOpen] = React.useState(false); const [inputValue, setInputValue] = useState(""); - const [dummySelected, setDummySelected] = useState([]); - - // For testing purposes, this component is meant to be stateless. - - const toggleFramework = (option: OptionKeys) => { - const isOptionSelected = dummySelected.some((s) => s.value === option.value); - if (isOptionSelected) { - setDummySelected((prev) => prev.filter((s) => s.value !== option.value)); - } else { - setDummySelected((prev) => [...prev, option]); - } - inputRef?.current?.focus(); - }; return ( setOpen(value)}> -
+
- - - - - {open && options.length > 0 - ? options.map((option) => ( - { - e.preventDefault(); - e.stopPropagation(); - }} - onSelect={(value) => { - setInputValue(""); - toggleFramework(option); - }} - onClick={() => toggleFramework(option)} - className={clsx( - "!cursor-pointer flex justify-between items-center !px-1 rounded-md truncate break-words w-full", - selected.some((s) => s.value === option.value) && "bg-gray-100" - )} - > - {option.label} - {selected.some((s) => s.value === option.value) && ( - - )} - - )) - : null} - - + + {options.length > 0 && ( + + + + {open && options.length > 0 + ? options.map((option) => ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onSelect={(value) => { + setInputValue(""); + // toggleFramework(option); + handleSelect(option); + }} + onClick={() => handleSelect(option)} + className={clsx( + "!cursor-pointer flex justify-between items-center !px-1 rounded-md truncate break-words w-full", + selected.some((s) => s.value === option.value) && "" + )} + > + {option.label} + {selected.some((s) => s.value === option.value) && ( + + )} + + )) + : null} + + + )} + {options.length === 0 && emptyState ? emptyState : null}
diff --git a/components/molecules/ContributorProfileHeader/contributor-profile-header.tsx b/components/molecules/ContributorProfileHeader/contributor-profile-header.tsx index f1d5372e10..761650b4de 100644 --- a/components/molecules/ContributorProfileHeader/contributor-profile-header.tsx +++ b/components/molecules/ContributorProfileHeader/contributor-profile-header.tsx @@ -1,14 +1,15 @@ import React, { useState, useEffect } from "react"; +import Link from "next/link"; import { useRouter } from "next/router"; import Image from "next/image"; import { TfiMoreAlt } from "react-icons/tfi"; import { HiUserAdd } from "react-icons/hi"; -import { FaIdCard } from "react-icons/fa"; import { SignInWithOAuthCredentials, User } from "@supabase/supabase-js"; import { usePostHog } from "posthog-js/react"; import { clsx } from "clsx"; +import dynamic from "next/dynamic"; import { DropdownMenu, DropdownMenuContent, @@ -23,9 +24,13 @@ import Text from "components/atoms/Typography/text"; import { Textarea } from "components/atoms/Textarea/text-area"; import { useUserConnections } from "lib/hooks/useUserConnections"; import { useToast } from "lib/hooks/useToast"; -import { cardPageUrl } from "lib/utils/urls"; +import { OptionKeys } from "components/atoms/Select/multi-select"; +import { addListContributor, useFetchAllLists } from "lib/hooks/useList"; +import { useFetchUser } from "lib/hooks/useFetchUser"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../Dialog/dialog"; +const MultiSelect = dynamic(() => import("components/atoms/Select/multi-select"), { ssr: false }); + interface ContributorProfileHeaderProps { avatarUrl?: string; githubName: string; @@ -156,7 +161,7 @@ const ContributorProfileHeader = ({
{isConnected && (
-
+
{user ? ( !isOwner && ( <> @@ -166,8 +171,8 @@ const ContributorProfileHeader = ({ variant="primary" className="group w-[6.25rem] justify-center items-center" > - Following - Unfollow + Following + Unfollow ) : ( - + {user && !isOwner && } + -
+
{!isOwner && ( { + const [selectListOpen, setSelectListOpen] = useState(false); + const [selectedList, setSelectedList] = useState([]); + const { data } = useFetchAllLists(); + const { data: contributor } = useFetchUser(username ?? ""); + const { toast } = useToast(); + + const listOptions = data ? data.map((list) => ({ label: list.name, value: list.id })) : []; + + const handleSelectList = (value: OptionKeys) => { + const isOptionSelected = selectedList.some((s) => s.value === value.value); + if (isOptionSelected) { + setSelectedList((prev) => prev.filter((s) => s.value !== value.value)); + } else { + setSelectedList((prev) => [...prev, value]); + } + }; + + const handleAddToList = async () => { + if (selectedList.length > 0 && contributor) { + const listIds = selectedList.map((list) => list.value); + const response = Promise.all(listIds.map((listIds) => addListContributor(listIds, [contributor.id]))); + + response + .then((res) => { + toast({ + description: ` + You've added ${username} to ${selectedList.length} list${selectedList.length > 1 ? "s" : ""}!`, + variant: "success", + }); + }) + .catch((res) => { + const failedList = listOptions.filter((list) => res.some((r: any) => r.error?.list_id === list.value)); + toast({ + description: ` + Failed to add ${username} to ${failedList[0].label} ${ + failedList.length > 1 && `and ${failedList.length - 1} other lists` + } ! + `, + variant: "danger", + }); + }); + } + }; + + useEffect(() => { + if (!selectListOpen && selectedList.length > 0) { + handleAddToList(); + setSelectedList([]); + } + }, [selectListOpen]); + + return ( + + You have no lists.
+ + Create a list + +
+ } + className="w-10 px-4" + placeholder="Add to list" + options={listOptions} + selected={selectedList} + setSelected={setSelectedList} + handleSelect={(option) => handleSelectList(option)} + /> + ); +}; + export default ContributorProfileHeader; diff --git a/lib/hooks/useList.ts b/lib/hooks/useList.ts index c1cb56cdb6..83caeb60a9 100644 --- a/lib/hooks/useList.ts +++ b/lib/hooks/useList.ts @@ -2,6 +2,7 @@ import useSWR, { Fetcher } from "swr"; import { useState } from "react"; import publicApiFetcher from "lib/utils/public-api-fetcher"; +import { supabase } from "lib/utils/supabase"; interface PaginatedListResponse { data: DbUserList[]; @@ -69,6 +70,42 @@ const useFetchListContributors = (id: string, range = 30) => { }; }; +const addListContributor = async (listId: string, contributors: number[]) => { + const sessionResponse = await supabase.auth.getSession(); + const sessionToken = sessionResponse?.data.session?.access_token; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/lists/${listId}/contributors`, { + method: "POST", + body: JSON.stringify({ contributors }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${sessionToken}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return { + data, + error: null, + }; + } else { + const error = await response.json(); + return { + data: null, + error: { message: error.message, listId }, + }; + } + } catch (error: any) { + console.log(error); + return { + data: null, + error: { message: error.message, listId }, + }; + } +}; + const useList = (listId: string) => { const { data, error, mutate } = useSWR(`lists/${listId}`, publicApiFetcher as Fetcher); @@ -80,4 +117,4 @@ const useList = (listId: string) => { }; }; -export { useList, useFetchAllLists, useFetchListContributors }; +export { useList, useFetchAllLists, useFetchListContributors, addListContributor };