From 61f38498c91082ac1c5cdf3d97e1e18841ff8515 Mon Sep 17 00:00:00 2001 From: "caco.eth" <49823133+FrancoAguzzi@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:48:48 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Improve=20namegraph.dev=20(#549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: [SC-26100] :bug: Improve Ui and add Exact Match button * feat: [SC-26100] ✨ Wrap new exact match feature in collections search | Add changeset for fetchCollectionsByMember inclusion in namegraph-sdk * feat: [SC-26100] :bug: Update lock file to fix builds * feat: [SC-26100] ✨ Integrate Scramble method picker in collection details page --------- Co-authored-by: lightwalker.eth <126201998+lightwalker-eth@users.noreply.github.com> --- .changeset/many-ways-smell.md | 5 + .../[id]/explore-collection-page.tsx | 407 ++++++++------- apps/namegraph.dev/app/collections/page.tsx | 492 ++++++++++-------- .../collections/collection-card.tsx | 51 ++ .../collections/collections-grid-skeleton.tsx | 18 +- apps/namegraph.dev/components/ui/toggle.tsx | 45 ++ apps/namegraph.dev/lib/utils.ts | 18 + apps/namegraph.dev/package.json | 1 + packages/namegraph-sdk/src/index.ts | 25 +- packages/namegraph-sdk/src/utils.ts | 11 +- pnpm-lock.yaml | 27 + 11 files changed, 665 insertions(+), 435 deletions(-) create mode 100644 .changeset/many-ways-smell.md create mode 100644 apps/namegraph.dev/components/collections/collection-card.tsx create mode 100644 apps/namegraph.dev/components/ui/toggle.tsx diff --git a/.changeset/many-ways-smell.md b/.changeset/many-ways-smell.md new file mode 100644 index 000000000..3d266c1ce --- /dev/null +++ b/.changeset/many-ways-smell.md @@ -0,0 +1,5 @@ +--- +"@namehash/namegraph-sdk": minor +--- + +Add fetchCollectionsByMember method in NameGraph SDK package diff --git a/apps/namegraph.dev/app/collections/[id]/explore-collection-page.tsx b/apps/namegraph.dev/app/collections/[id]/explore-collection-page.tsx index ae29f7ca3..1f33ffe8e 100644 --- a/apps/namegraph.dev/app/collections/[id]/explore-collection-page.tsx +++ b/apps/namegraph.dev/app/collections/[id]/explore-collection-page.tsx @@ -1,4 +1,5 @@ "use client"; +/* eslint-disable react-hooks/exhaustive-deps */ import { fetchCollectionMembers, @@ -11,6 +12,7 @@ import { NameGraphCollection, NameGraphFindCollectionsResponse, NameGraphSuggestion, + ScrambleMethod, } from "@namehash/namegraph-sdk/utils"; import { useEffect, useState } from "react"; import { Noto_Emoji } from "next/font/google"; @@ -18,6 +20,15 @@ import { useQueryParams } from "@/components/use-query-params"; import { Button } from "@/components/ui/button"; import { ChevronLeft, ChevronRight, Loader } from "lucide-react"; import Skeleton from "@/components/skeleton"; +import { CollectionCard } from "@/components/collections/collection-card"; +import { Tooltip } from "@namehash/namekit-react/client"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; const notoBlack = Noto_Emoji({ preload: false }); @@ -30,6 +41,16 @@ interface SuggestionsData { suggestions: NameGraphSuggestion[]; } +const FromNameGraphScrambleMethodToDropdownTextContent: Record< + ScrambleMethod, + string +> = { + [ScrambleMethod["full-shuffle"]]: "Full Shuffle", + [ScrambleMethod["left-right-shuffle"]]: "Left - Right Shuffle", + [ScrambleMethod["left-right-shuffle-with-unigrams"]]: + "Left - Right Shuffle with Unigrams", +}; + export const ExploreCollectionPage = ({ id }: { id: string }) => { const [loadingCollection, setLoadingCollection] = useState(true); const [loadingCollectionMembers, setLoadingCollectionMembers] = @@ -84,9 +105,6 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { loadCollectionMembers(); loadCollectionRelatedCollections(); }, [id]); - useEffect(() => { - console.log(collectionMembers); - }, [collectionMembers]); const loadCollectionMembers = () => { if (!!collectionMembers?.[params.page]) { @@ -95,7 +113,7 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { setLoadingCollectionMembers(true); fetchCollectionMembers(id, { - offset: params.page - 1, + offset: (params.page - 1) * navigationConfig.itemsPerPage, }) .then((res) => { if (res) { @@ -155,12 +173,19 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { } }; + const [scrambleMethod, setScrambleMethod] = useState<ScrambleMethod>( + ScrambleMethod["left-right-shuffle-with-unigrams"], + ); + const scramble = () => { if (id) { const randomSeed = Number(Number(Math.random() * 10).toFixed(0)); setLoadingScramble(true); - scrambleNamesByCollectionId(id, { seed: randomSeed }) + scrambleNamesByCollectionId(id, { + seed: randomSeed, + method: scrambleMethod, + }) .then((res) => { setScrambledNameIdeas(res); }) @@ -221,9 +246,8 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { const isLastCollectionsPageForCurrentQuery = () => { if (navigationConfig.totalItems) { return ( - Number(params.page) !== 1 && - Number(params.page) * navigationConfig.itemsPerPage > - navigationConfig.totalItems + Number(params.page) * navigationConfig.itemsPerPage >= + navigationConfig.totalItems ); } else return false; }; @@ -280,7 +304,7 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { {/* Collections List */} <div className="w-full space-y-4"> {loadingCollection ? ( - <div className="flex flex-col space-y-7 my-8"> + <div className="w-full h-full flex flex-col justify-center items-center my-8 animate-spin"> <Loader /> </div> ) : collection ? ( @@ -289,17 +313,74 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { <div className="w-full"> <div className="w-full mb-8"> <div className="w-full flex flex-col space-y-4 p-3 rounded-xl border border-gray-200"> - <div className="w-full"> - {/* Collection Count and Sort */} - <div className="max-w-[756px] w-full flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0 mb-5"> - <div className="flex items-center"> - <div className="text-lg font-semibold mr-2.5"> - {getNavigationPageTextGuide()} + <div className="w-full h-[517px] flex flex-col justify-start"> + <div className="h-full"> + {/* Collection Count and Sort */} + <div className="max-w-[756px] w-full flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0 mb-5"> + <div className="flex items-center"> + <div className="text-lg font-semibold mr-2.5"> + {getNavigationPageTextGuide()} + </div> + {navigationConfig.totalItems ? ( + <div className="flex"> + <Button + className="cursor-pointer p-[9px] bg-white shadow-none hover:bg-gray-50 rounded-lg disabled:opacity-50 disabled:hover:bg-white" + disabled={isFirstCollectionsPageForCurrentQuery()} + onClick={() => + handlePageChange( + Number(params.page) - 1, + ) + } + > + <ChevronLeft className="w-6 h-6 text-black" /> + </Button> + <Button + className="cursor-pointer p-[9px] bg-white shadow-none hover:bg-gray-50 rounded-lg disabled:opacity-50" + disabled={isLastCollectionsPageForCurrentQuery()} + onClick={() => + handlePageChange( + Number(params.page) + 1, + ) + } + > + <ChevronRight className="w-6 h-6 text-black" /> + </Button> + </div> + ) : null} </div> - {navigationConfig.totalItems ? ( - <div className="flex"> + </div> + {/* Collections List */} + <div className="w-full h-full max-w-[756px] space-y-4"> + {loadingCollectionMembers ? ( + <div className="flex flex-col w-full mt-auto justify-center items-center animate-spin h-[370px]"> + <Loader /> + </div> + ) : collectionMembers ? ( + collectionMembers[ + params.page + ]?.suggestions.map((suggestion) => ( + <div + key={suggestion.name} + className="bg-gray-100 rounded-full groupp-2 px-4 flex items-start" + > + <div className="max-h-[20px] relative flex items-center justify-center overflow-hidden"> + {suggestion.name} + </div> + </div> + )) + ) : null} + </div> + </div> + <div className="mt-auto"> + {/* Pagination */} + {navigationConfig.totalItems ? ( + <div className="flex items-center justify-between border border-gray-200 border-l-0 border-r-0 border-b-0 mt-3 p-3"> + <div className="text-sm text-gray-500 mr-2.5"> + {getNavigationPageTextGuide()} + </div> + <div className="flex items-center gap-2"> <Button - className="cursor-pointer p-[9px] bg-white shadow-none hover:bg-gray-50 rounded-lg disabled:opacity-50 disabled:hover:bg-white" + className="bg-white text-black shadow-none hover:bg-gray-50 text-sm p-2.5" disabled={isFirstCollectionsPageForCurrentQuery()} onClick={() => handlePageChange( @@ -307,10 +388,11 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { ) } > - <ChevronLeft className="w-6 h-6 text-black" /> + <ChevronLeft /> + Prev </Button> <Button - className="cursor-pointer p-[9px] bg-white shadow-none hover:bg-gray-50 rounded-lg disabled:opacity-50" + className="bg-white text-black shadow-none hover:bg-gray-50 text-sm p-2.5" disabled={isLastCollectionsPageForCurrentQuery()} onClick={() => handlePageChange( @@ -318,90 +400,40 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { ) } > - <ChevronRight className="w-6 h-6 text-black" /> + Next + <ChevronRight /> </Button> </div> - ) : null} - </div> - </div> - {/* Collections List */} - <div className="w-full max-w-[756px] space-y-4"> - {loadingCollectionMembers ? ( - <div className="flex flex-col space-y-7 my-8"> - <Loader /> </div> - ) : collectionMembers ? ( - collectionMembers[params.page]?.suggestions.map( - (suggestion) => ( - <div - key={suggestion.name} - className="bg-gray-100 rounded-full groupp-2 px-4 flex items-start" - > - <div className="relative flex items-center justify-center overflow-hidden"> - {suggestion.name} - </div> - </div> - ), - ) ) : null} </div> - {/* Pagination */} - {navigationConfig.totalItems ? ( - <div className="flex items-center justify-between border border-gray-200 border-l-0 border-r-0 border-b-0 mt-3 pt-3"> - <div className="text-sm text-gray-500 mr-2.5"> - {getNavigationPageTextGuide()} - </div> - <div className="flex items-center gap-2"> - <Button - className="bg-white text-black shadow-none hover:bg-gray-50 text-sm p-2.5" - disabled={isFirstCollectionsPageForCurrentQuery()} - onClick={() => - handlePageChange(Number(params.page) - 1) - } - > - <ChevronLeft /> - Prev - </Button> - <Button - className="bg-white text-black shadow-none hover:bg-gray-50 text-sm p-2.5" - disabled={isLastCollectionsPageForCurrentQuery()} - onClick={() => - handlePageChange(Number(params.page) + 1) - } - > - Next - <ChevronRight /> - </Button> - </div> - </div> - ) : null} </div> </div> </div> - <div className="border border-gray-200 rounded-lg p-3"> - <div className="flex flex-col space-y-4"> - <p className="font-semibold text-lg"> - Interact with this collection - </p> - <div className="flex space-x-4"> - <Button onClick={scramble}> - Scramble name ideas - </Button> + <div className="w-full border border-gray-200 rounded-lg"> + <div className="w-full flex border border-l-0 border-t-0 border-r-0 px-3 py-3 justify-between items-center"> + <Tooltip + trigger={ + <p className="font-semibold text-lg w-full"> + Sample names + </p> + } + > + <p>Sampling names means creating </p> + </Tooltip> - <Button onClick={ideate}>Sample name ideas</Button> - </div> + <Button onClick={ideate}>Sample name ideas</Button> </div> <div className="flex flex-col space-y-8 mt-4"> - {scrambledNameIdeas ? ( - <div className="flex flex-col border border-gray-200 rounded-xl p-3"> - <div className="text-lg underline font-semibold mb-4 pl-2"> - Scrambled name ideas - </div> - {loadingScramble ? ( - <Loader /> + {sampledNameIdeas ? ( + <div className="flex flex-col rounded-xl py-3 h-[200px]"> + {loadingIdeate ? ( + <div className="flex flex-col w-full h-full justify-center items-center animate-spin"> + <Loader /> + </div> ) : ( - <div className="flex flex-wrap gap-3"> - {scrambledNameIdeas?.map((suggestion) => { + <div className="flex flex-wrap gap-3 pl-3"> + {sampledNameIdeas?.map((suggestion) => { return ( <div key={suggestion.name} @@ -417,17 +449,59 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { )} </div> ) : null} + </div> + </div> - {sampledNameIdeas ? ( - <div className="flex flex-col border border-gray-200 rounded-xl p-3"> - <div className="text-lg underline font-semibold mb-4 pl-2"> - Sampled name ideas - </div> - {loadingIdeate ? ( - <Loader /> + <div className="w-full border border-gray-200 rounded-lg mt-8"> + <div className="w-full flex border border-l-0 border-t-0 border-r-0 px-3 py-3 justify-between items-center"> + <Tooltip + trigger={ + <p className="font-semibold text-lg w-full"> + Scramble names + </p> + } + > + <p>Scrambling names means creating </p> + </Tooltip> + <Select + defaultValue={scrambleMethod} + onValueChange={(newValue) => + setScrambleMethod(newValue as ScrambleMethod) + } + > + <SelectTrigger className="w-[180px]"> + <SelectValue placeholder="Sort by" /> + </SelectTrigger> + <SelectContent> + {Object.entries( + FromNameGraphScrambleMethodToDropdownTextContent, + ).map(([key]) => { + return ( + <SelectItem key={key} value={key}> + { + FromNameGraphScrambleMethodToDropdownTextContent[ + key as ScrambleMethod + ] + } + </SelectItem> + ); + })} + </SelectContent> + </Select> + <Button onClick={scramble}> + Scramble name ideas + </Button> + </div> + <div className="flex flex-col space-y-8 mt-4"> + {scrambledNameIdeas ? ( + <div className="flex flex-col rounded-xl py-3 h-[200px]"> + {loadingScramble ? ( + <div className="flex flex-col w-full h-full justify-center items-center animate-spin"> + <Loader /> + </div> ) : ( - <div className="flex flex-wrap gap-3"> - {sampledNameIdeas?.map((suggestion) => { + <div className="flex flex-wrap gap-3 pl-3"> + {scrambledNameIdeas?.map((suggestion) => { return ( <div key={suggestion.name} @@ -452,111 +526,42 @@ export const ExploreCollectionPage = ({ id }: { id: string }) => { <h2 className="flex items-center text-lg font-semibold h-[47px] px-5 border border-t-0 border-r-0 border-l-0 border-gray-200"> Related collections </h2> - {relatedCollections ? ( - relatedCollections.related_collections.map( - (collection) => ( - <div - key={collection.collection_id} - className="!no-underline group rounded-lg cursor-pointer px-5 py-3 flex items-start gap-[18px]" - > - <div - style={{ - border: "1px solid rgba(0, 0, 0, 0.05)", - }} - className="group-hover:bg-gray-300 group-hover:transition flex justify-center items-center rounded-md bg-background h-[72px] w-[72px] bg-gray-100" - > - <div className="relative flex items-center justify-center overflow-hidden"> - <p - className={`text-3xl ${notoBlack.className}`} - > - {collection.avatar_emoji} - </p> - </div> - </div> - <div className="flex-1 overflow-hidden"> - <h3 className="text-sm font-semibold"> - {collection.title} - </h3> - <p className="text-xs text-gray-500 mb-2 truncate"> - by {collection.owner} - </p> - <div className="relative"> - <div className="flex gap-2"> - {collection.top_names.map((tag) => ( - <span - key={tag.namehash} - className="bg-gray-100 text-sm px-2 py-1 bg-muted rounded-full" - > - {tag.name} - </span> - ))} - </div> - <div className="bg-gradient-white-to-transparent absolute right-0 top-0 w-40 h-full"></div> - </div> - </div> - </div> - ), - ) - ) : ( - <div className="p-3 px-5"> - No name suggestions found - </div> - )} + + <div className="px-5"> + {relatedCollections ? ( + relatedCollections.related_collections.map( + (collection) => ( + <CollectionCard + key={collection.collection_id} + collection={collection} + /> + ), + ) + ) : ( + <div className="p-3 px-5"> + No name suggestions found + </div> + )} + </div> </div> <div className="z-40 border rounded-md border-gray-200 w-full h-fit"> <h2 className="flex items-center text-lg font-semibold h-[47px] px-5 border border-t-0 border-r-0 border-l-0 border-gray-200"> Other collections </h2> - {relatedCollections ? ( - relatedCollections.other_collections.map( - (collection) => ( - <div - key={collection.collection_id} - className="!no-underline group rounded-lg cursor-pointer px-5 py-3 flex items-start gap-[18px]" - > - <div - style={{ - border: "1px solid rgba(0, 0, 0, 0.05)", - }} - className="group-hover:bg-gray-300 group-hover:transition flex justify-center items-center rounded-md bg-background h-[72px] w-[72px] bg-gray-100" - > - <div className="relative flex items-center justify-center overflow-hidden"> - <p - className={`text-3xl ${notoBlack.className}`} - > - {collection.avatar_emoji} - </p> - </div> - </div> - <div className="flex-1 overflow-hidden"> - <h3 className="text-sm font-semibold"> - {collection.title} - </h3> - <p className="text-xs text-gray-500 mb-2 truncate"> - by {collection.owner} - </p> - <div className="relative"> - <div className="flex gap-2"> - {collection.top_names.map((tag) => ( - <span - key={tag.namehash} - className="bg-gray-100 text-sm px-2 py-1 bg-muted rounded-full" - > - {tag.name} - </span> - ))} - </div> - <div className="bg-gradient-white-to-transparent absolute right-0 top-0 w-40 h-full"></div> - </div> - </div> - </div> - ), - ) - ) : ( - <div className="p-3 px-5"> - No name suggestions found - </div> - )} + <div className="px-5"> + {relatedCollections ? ( + relatedCollections.other_collections.map( + (collection) => ( + <CollectionCard + key={collection.collection_id} + collection={collection} + /> + ), + ) + ) : ( + <div className="p-3">No name suggestions found</div> + )} + </div> </div> </div> </div> diff --git a/apps/namegraph.dev/app/collections/page.tsx b/apps/namegraph.dev/app/collections/page.tsx index 5d3c9e0ec..5b2802fbe 100644 --- a/apps/namegraph.dev/app/collections/page.tsx +++ b/apps/namegraph.dev/app/collections/page.tsx @@ -5,9 +5,10 @@ import { NameGraphCollection, NameGraphSortOrderOptions, } from "@namehash/namegraph-sdk/utils"; -import { findCollectionsByString } from "@/lib/utils"; +import { findCollectionsByMember, findCollectionsByString } from "@/lib/utils"; import { DebounceInput } from "react-debounce-input"; import { Suspense, useEffect, useState } from "react"; +import { Toggle } from "@/components/ui/toggle"; import { Button } from "@/components/ui/button"; import { Select, @@ -22,10 +23,8 @@ import { CollectionsGridSkeleton, } from "@/components/collections/collections-grid-skeleton"; import { useQueryParams } from "@/components/use-query-params"; -import { Noto_Emoji } from "next/font/google"; -import { Link } from "@namehash/namekit-react"; +import { CollectionCard } from "@/components/collections/collection-card"; -const notoBlack = Noto_Emoji({ preload: false }); interface NavigationConfig { itemsPerPage: number; totalItems?: number; @@ -33,7 +32,7 @@ interface NavigationConfig { interface CollectionsData { sort_order: NameGraphSortOrderOptions; - other_collections: NameGraphCollection[]; + other_collections: NameGraphCollection[] | null; related_collections: NameGraphCollection[]; } @@ -56,6 +55,7 @@ export default function ExploreCollectionsPage() { search: "", page: DEFAULT_PAGE_NUMBER, orderBy: NameGraphSortOrderOptions.AI, + exactMatch: false, }; type DefaultDomainFiltersType = typeof DEFAULT_COLLECTIONS_PARAMS; const { params, setParams } = useQueryParams<DefaultDomainFiltersType>( @@ -67,14 +67,45 @@ export default function ExploreCollectionsPage() { search: searchTerm, page: DEFAULT_PAGE_NUMBER, // Resets page when search changes }); + + setNavigationConfig({ + ...navigationConfig, + totalItems: undefined, + }); + + if (!params.search) { + return; + } + + setCollections(null); + setLoadingCollections(true); + + queryCollections({ + search: params.search || "", + orderBy: params.orderBy || NameGraphSortOrderOptions.AI, + page: params.page || 1, + exactMatch: params.exactMatch, + }); }; const handleOrderBy = (orderBy: NameGraphSortOrderOptions) => { setParams({ orderBy }); + queryCollections({ + search: params.search || "", + orderBy: orderBy || NameGraphSortOrderOptions.AI, + page: params.page || 1, + exactMatch: params.exactMatch, + }); }; const handlePageChange = (page: number) => { setParams({ page }); + queryCollections({ + search: params.search || "", + orderBy: params.orderBy || NameGraphSortOrderOptions.AI, + page: page || 1, + exactMatch: params.exactMatch, + }); }; /** @@ -87,8 +118,15 @@ export default function ExploreCollectionsPage() { const [collections, setCollections] = useState< undefined | null | Record<number, CollectionsData | null | undefined> >(undefined); - const [collectionsQueriedStandFor, setCollectionsQueriedStandFor] = - useState(""); + const [lastQueryDone, setLastQueryDone] = useState<{ + search: string; + exactMatch: boolean; + }>({ search: params.search || "", exactMatch: params.exactMatch || false }); + + useEffect(() => { + console.log("New values for collections results:"); + console.log(collections, params); + }, [collections]); const [loadingCollections, setLoadingCollections] = useState(true); @@ -98,13 +136,21 @@ export default function ExploreCollectionsPage() { totalItems: undefined, }); - const loadCollections = () => { + interface QueryCollectionsParam { + exactMatch: boolean; + orderBy: NameGraphSortOrderOptions; + search: string; + page: number; + } + + const queryCollections = (params: QueryCollectionsParam) => { if (params.search) { let query = params.search; if (params.search.includes(".")) { query = params.search.split(".")[0]; } + const MAX_COLLECTIONS_FOR_EXACT_MATCH = 10; const MAX_RELATED_COLLECTIONS = 20; const OTHER_COLLECTIONS_NUMBER = 5; @@ -117,103 +163,152 @@ export default function ExploreCollectionsPage() { * * Of course this is only true if both the query and the sorting * algorithm lastly used are the same. If any of these have changes, - * we do the loadCollections query once again and update the page's results. + * we do the queryCollections query once again and update the page's results. */ if ( - !!collectionsQueriedStandFor && - collectionsQueriedStandFor === params.search && + !!lastQueryDone && + lastQueryDone.search === params.search && !!collections?.[params.page] && - params.orderBy == collections?.[params.page]?.sort_order + params.orderBy == collections?.[params.page]?.sort_order && + params.exactMatch === lastQueryDone.exactMatch ) { return; } setLoadingCollections(true); - setCollectionsQueriedStandFor(query); - findCollectionsByString(query, { - offset: params.page - 1, - sort_order: params.orderBy, - max_total_collections: - MAX_RELATED_COLLECTIONS + OTHER_COLLECTIONS_NUMBER, - /** - * Please note how the number of collections one page show is - * strategically aligned with ITEMS_PER_PAGE_OPTIONS. - */ - max_related_collections: MAX_RELATED_COLLECTIONS, - max_other_collections: OTHER_COLLECTIONS_NUMBER, - min_other_collections: OTHER_COLLECTIONS_NUMBER, - }) - .then((res) => { - if (res) { - const MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT = 1000; - - setNavigationConfig({ - ...navigationConfig, - totalItems: - typeof res.metadata.total_number_of_matched_collections === - "number" - ? res.metadata.total_number_of_matched_collections - : /** - * NameAPI makes usage of a stringified "+1000" for - * res.metadata.total_number_of_matched_collections - * if there are more than 1000 collections this query - * is contained in. Since this is a different data type - * and we are handling number operations with totalItems, - * we are here normalizing this use case to use the number 1000 - */ - MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT, - }); - const moreCollections = res.other_collections; - const relatedCollections = res.related_collections; + if (params.exactMatch) { + findCollectionsByMember(query, { + offset: (params.page - 1) * navigationConfig.itemsPerPage, + sort_order: params.orderBy, + limit_names: MAX_COLLECTIONS_FOR_EXACT_MATCH, + /** + * Please note how the number of collections one page show is + * strategically aligned with ITEMS_PER_PAGE_OPTIONS. + */ + max_results: MAX_COLLECTIONS_FOR_EXACT_MATCH, + }) + .then((res) => { + if (res) { + const MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT = 1000; + + setNavigationConfig({ + ...navigationConfig, + totalItems: + typeof res.metadata.total_number_of_matched_collections === + "number" + ? res.metadata.total_number_of_matched_collections + : /** + * NameAPI makes usage of a stringified "+1000" for + * res.metadata.total_number_of_matched_collections + * if there are more than 1000 collections this query + * is contained in. Since this is a different data type + * and we are handling number operations with totalItems, + * we are here normalizing this use case to use the number 1000 + */ + MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT, + }); + + const relatedCollections = res.collections; + setCollections({ + ...collections, + [params.page]: { + sort_order: params.orderBy, + related_collections: relatedCollections, + other_collections: null, + }, + }); + } else { + setCollections({ + ...collections, + [params.page]: null, + }); + } + }) + .catch(() => { setCollections({ ...collections, - [params.page]: { - sort_order: params.orderBy, - related_collections: relatedCollections, - other_collections: moreCollections, - }, + [params.page]: null, }); + }) + .finally(() => { setLoadingCollections(false); - } else { + }); + } else { + findCollectionsByString(query, { + offset: (params.page - 1) * navigationConfig.itemsPerPage, + sort_order: params.orderBy, + max_total_collections: + MAX_RELATED_COLLECTIONS + OTHER_COLLECTIONS_NUMBER, + /** + * Please note how the number of collections one page show is + * strategically aligned with ITEMS_PER_PAGE_OPTIONS. + */ + max_related_collections: MAX_RELATED_COLLECTIONS, + max_other_collections: OTHER_COLLECTIONS_NUMBER, + min_other_collections: OTHER_COLLECTIONS_NUMBER, + }) + .then((res) => { + if (res) { + const MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT = 1000; + + setNavigationConfig({ + ...navigationConfig, + totalItems: + typeof res.metadata.total_number_of_matched_collections === + "number" + ? res.metadata.total_number_of_matched_collections + : /** + * NameAPI makes usage of a stringified "+1000" for + * res.metadata.total_number_of_matched_collections + * if there are more than 1000 collections this query + * is contained in. Since this is a different data type + * and we are handling number operations with totalItems, + * we are here normalizing this use case to use the number 1000 + */ + MAX_COLLECTIONS_NUMBER_NAME_API_CAN_DOCUMENT, + }); + + const moreCollections = res.other_collections; + const relatedCollections = res.related_collections; + + setCollections({ + ...collections, + [params.page]: { + sort_order: params.orderBy, + related_collections: relatedCollections, + other_collections: moreCollections, + }, + }); + } else { + setCollections({ + ...collections, + [params.page]: null, + }); + } + }) + .catch(() => { setCollections({ ...collections, [params.page]: null, }); - } - }) - .catch(() => { - setCollections({ - ...collections, - [params.page]: null, + }) + .finally(() => { + setLoadingCollections(false); }); - }); + } + + setLastQueryDone({ + search: params.search, + exactMatch: params.exactMatch, + }); } else { setCollections(null); setLoadingCollections(false); } }; - useEffect(() => { - setNavigationConfig({ - ...navigationConfig, - totalItems: undefined, - }); - - if (!params.search) { - return; - } - - setCollections(null); - setLoadingCollections(true); - loadCollections(); - }, [params.search]); - - useEffect(() => { - loadCollections(); - }, [params.page, params.orderBy]); - /** * Navigation helper functions */ @@ -223,9 +318,8 @@ export default function ExploreCollectionsPage() { const isLastCollectionsPageForCurrentQuery = () => { if (navigationConfig.totalItems) { return ( - Number(params.page) !== 1 && - Number(params.page) * navigationConfig.itemsPerPage > - navigationConfig.totalItems + Number(params.page) * navigationConfig.itemsPerPage >= + navigationConfig.totalItems ); } else return false; }; @@ -236,9 +330,15 @@ export default function ExploreCollectionsPage() { Number(params.page) * navigationConfig.itemsPerPage, navigationConfig.totalItems, )} of ${navigationConfig.totalItems} collections` - : "No collections found"; + : !loadingCollections + ? "No collections found" + : ""; }; + useEffect(() => { + handleSearch(params.search); + }, [params.search]); + return ( <Suspense fallback={<div>Loading...</div>}> <div className="mx-auto py-8 w-full"> @@ -284,7 +384,9 @@ export default function ExploreCollectionsPage() { <div className="w-full flex flex-col xl:flex-row"> <div className="w-full"> {/* Collection Count and Sort */} - <div className="max-w-[756px] w-full flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0 mb-5"> + <div + className={`w-full flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0 mb-5 ${!collections[params.page]?.other_collections ? "" : "max-w-[756px]"}`} + > <div className="flex items-center"> <div className="text-lg font-semibold mr-2.5"> {getNavigationPageTextGuide()} @@ -312,93 +414,86 @@ export default function ExploreCollectionsPage() { </div> ) : null} </div> - {collections[params.page] ? ( - <div className="flex space-x-3 items-center"> - <div className="text-sm text-gray-500"> - Sort by + + <div className="flex space-x-4"> + {collections[params.page] ? ( + <div className="flex text-gray-200 border rounded-lg"> + <Toggle + pressed={params.exactMatch} + onPressedChange={(pressed) => { + setNavigationConfig({ + ...navigationConfig, + totalItems: undefined, + }); + setParams({ exactMatch: pressed }); + queryCollections({ + search: params.search || "", + orderBy: + params.orderBy || + NameGraphSortOrderOptions.AI, + page: params.page || 1, + exactMatch: pressed, + }); + }} + > + Exact Match + </Toggle> </div> - <Select - defaultValue={ - params.orderBy || NameGraphSortOrderOptions.AI - } - onValueChange={(newValue) => - handleOrderBy( - newValue as NameGraphSortOrderOptions, - ) - } - > - <SelectTrigger className="w-[180px]"> - <SelectValue placeholder="Sort by" /> - </SelectTrigger> - <SelectContent> - {Object.entries( - FromNameGraphSortOrderToDropdownTextContent, - ).map(([key]) => { - return ( - <SelectItem key={key} value={key}> - { - FromNameGraphSortOrderToDropdownTextContent[ - key as NameGraphSortOrderOptions - ] - } - </SelectItem> - ); - })} - </SelectContent> - </Select> - </div> - ) : null} + ) : null} + {collections[params.page] ? ( + <div className="flex space-x-3 items-center"> + <div className="text-sm text-gray-500"> + Sort by + </div> + <Select + defaultValue={ + params.orderBy || + NameGraphSortOrderOptions.AI + } + onValueChange={(newValue) => + handleOrderBy( + newValue as NameGraphSortOrderOptions, + ) + } + > + <SelectTrigger className="w-[180px]"> + <SelectValue placeholder="Sort by" /> + </SelectTrigger> + <SelectContent> + {Object.entries( + FromNameGraphSortOrderToDropdownTextContent, + ).map(([key]) => { + return ( + <SelectItem key={key} value={key}> + { + FromNameGraphSortOrderToDropdownTextContent[ + key as NameGraphSortOrderOptions + ] + } + </SelectItem> + ); + })} + </SelectContent> + </Select> + </div> + ) : null} + </div> </div> {/* Collections List */} - <div className="w-full max-w-[756px] space-y-4"> + <div + className={`w-full ${!collections[params.page]?.other_collections ? "" : "max-w-[756px]"}`} + > {loadingCollections ? ( - <div className="flex flex-col space-y-7 my-8"> + <div className="flex flex-col"> <CollectionsCardsSkeleton /> </div> ) : collections[params.page] ? ( collections[params.page]?.related_collections.map( (collection) => ( - <Link + <CollectionCard key={collection.collection_id} - href={`/collections/${collection.collection_id}`} - className="!no-underline group cursor-pointer border border-l-0 border-r-0 border-b-0 pt-3 border-gray-200 flex items-start gap-[18px]" - > - <div - style={{ - border: "1px solid rgba(0, 0, 0, 0.05)", - }} - className="group-hover:bg-gray-300 group-hover:transition flex justify-center items-center rounded-md bg-background h-[72px] w-[72px] bg-gray-100" - > - <div className="relative flex items-center justify-center overflow-hidden"> - <p - className={`text-3xl ${notoBlack.className}`} - > - {collection.avatar_emoji} - </p> - </div> - </div> - <div className="flex-1 overflow-hidden"> - <h3 className="text-sm font-semibold"> - {collection.title} - </h3> - <p className="text-xs text-gray-500 mb-2 truncate"> - by {collection.owner} - </p> - <div className="relative"> - <div className="flex gap-2"> - {collection.top_names.map((tag) => ( - <span - key={tag.namehash} - className="bg-gray-100 text-sm px-2 py-1 bg-muted rounded-full" - > - {tag.name} - </span> - ))} - </div> - <div className="bg-gradient-white-to-transparent absolute right-0 top-0 w-40 h-full"></div> - </div> - </div> - </Link> + collection={collection} + /> ), ) ) : null} @@ -435,63 +530,26 @@ export default function ExploreCollectionsPage() { ) : null} </div> - <div className="z-40 xl:max-w-[400px] mt-10 xl:mt-0 xl:ml-[68px] border rounded-md border-gray-200 w-full h-fit"> - <h2 className="flex items-center text-lg font-semibold h-[47px] px-5 border border-t-0 border-r-0 border-l-0 border-gray-200"> - Other collections - </h2> - {collections[params.page] ? ( - collections[params.page]?.other_collections.map( - (collection) => ( - <Link - key={collection.collection_id} - href={`/collections/${collection.collection_id}`} - className="!no-underline group rounded-lg cursor-pointer px-5 py-3 flex items-start gap-[18px]" - > - <div - style={{ - border: "1px solid rgba(0, 0, 0, 0.05)", - }} - className="group-hover:bg-gray-300 group-hover:transition flex justify-center items-center rounded-md bg-background h-[72px] w-[72px] bg-gray-100" - > - <div className="relative flex items-center justify-center overflow-hidden"> - <p - className={`text-3xl ${notoBlack.className}`} - > - {collection.avatar_emoji} - </p> - </div> - </div> - <div className="flex-1 overflow-hidden"> - <h3 className="text-sm font-semibold"> - {collection.title} - </h3> - <p className="text-xs text-gray-500 mb-2 truncate"> - by {collection.owner} - </p> - <div className="relative"> - <div className="flex gap-2"> - {collection.top_names.map((tag) => ( - <span - key={tag.namehash} - className="bg-gray-100 text-sm px-2 py-1 bg-muted rounded-full" - > - {tag.name} - </span> - ))} - </div> - <div className="bg-gradient-white-to-transparent absolute right-0 top-0 w-40 h-full"></div> - </div> - </div> - </Link> - ), - ) - ) : ( - <div className="p-3 px-5">No collections found</div> - )} - </div> + {collections[params.page]?.other_collections && ( + <div className="z-40 xl:max-w-[400px] mt-10 xl:mt-0 xl:ml-[68px] border rounded-md border-gray-200 w-full h-fit"> + <h2 className="flex items-center text-lg font-semibold h-[47px] px-5 border border-t-0 border-r-0 border-l-0 border-gray-200"> + Other collections + </h2> + <div className="px-5"> + {collections[params.page]?.other_collections?.map( + (collection) => ( + <CollectionCard + key={collection.collection_id} + collection={collection} + /> + ), + )} + </div> + </div> + )} </div> </> - ) : !loadCollections && !params.search && !collections ? ( + ) : !params.search && !collections ? ( <>Error</> ) : null} </> diff --git a/apps/namegraph.dev/components/collections/collection-card.tsx b/apps/namegraph.dev/components/collections/collection-card.tsx new file mode 100644 index 000000000..684c2236a --- /dev/null +++ b/apps/namegraph.dev/components/collections/collection-card.tsx @@ -0,0 +1,51 @@ +import { NameGraphCollection } from "@namehash/namegraph-sdk/utils"; +import { Link } from "@namehash/namekit-react"; +import { Noto_Emoji } from "next/font/google"; + +const notoBlack = Noto_Emoji({ preload: false }); + +export const CollectionCard = ({ + collection, +}: { + collection: NameGraphCollection; +}) => { + return ( + <Link + key={collection.collection_id} + href={`/collections/${collection.collection_id}`} + className="!no-underline group rounded-lg py-3 cursor-pointer flex items-start space-x-[18px]" + > + <div + style={{ + border: "1px solid rgba(0, 0, 0, 0.05)", + }} + className="group-hover:bg-gray-300 group-hover:transition flex justify-center items-center rounded-md bg-background h-[72px] w-[72px] bg-gray-100" + > + <div className="relative flex items-center justify-center overflow-hidden"> + <p className={`text-3xl ${notoBlack.className}`}> + {collection.avatar_emoji} + </p> + </div> + </div> + <div className="flex-1 overflow-hidden"> + <h3 className="text-sm font-semibold truncate">{collection.title}</h3> + <p className="text-xs text-gray-500 mb-2 truncate"> + by {collection.owner} + </p> + <div className="relative"> + <div className="flex gap-2"> + {collection.top_names.map((tag) => ( + <span + key={tag.namehash} + className="max-h-[28px] w-max bg-gray-100 text-sm px-2 py-1 bg-muted rounded-full" + > + {tag.name} + </span> + ))} + </div> + <div className="bg-gradient-white-to-transparent absolute right-0 top-0 w-40 h-full"></div> + </div> + </div> + </Link> + ); +}; diff --git a/apps/namegraph.dev/components/collections/collections-grid-skeleton.tsx b/apps/namegraph.dev/components/collections/collections-grid-skeleton.tsx index 104468af3..18330f6ce 100644 --- a/apps/namegraph.dev/components/collections/collections-grid-skeleton.tsx +++ b/apps/namegraph.dev/components/collections/collections-grid-skeleton.tsx @@ -3,7 +3,7 @@ import Skeleton from "../skeleton"; export const CollectionsGridSkeleton = () => { return ( <div className="w-full flex flex-col xl:flex-row max-w-7xl xl:space-x-0"> - <div className="w-full flex flex-col space-y-6 items-start"> + <div className="w-full flex flex-col space-y-2.5 items-start"> <div className="mt-2 md:mt-0 flex flex-col md:flex-row justify-between w-full mb-2"> <div className="flex"> <div className="flex mr-2.5"> @@ -23,10 +23,10 @@ export const CollectionsGridSkeleton = () => { </div> </div> <div className="relative z-50 xl:max-w-[400px] mt-10 xl:mt-0 border rounded-md border-gray-200 w-full h-fit"> - <div className="flex items-center h-[47px] border border-gray-200 border-t-0 border-r-0 border-l-0"> - <Skeleton className="w-[260px] h-7 px-5" /> + <div className="px-5 py-[13px] border border-gray-200 border-r-0 border-l-0 border-t-0"> + <Skeleton className="w-[80px] h-5" /> </div> - <div className="px-5 py-3 gap-[18px] space-y-6 overflow-hidden"> + <div className="px-5 overflow-hidden pb-[10px]"> <CollectionsCardsSkeleton /> </div> <div className="bg-gradient-white-to-transparent absolute right-0 top-[47px] w-20 h-full"></div> @@ -39,7 +39,7 @@ const NUMBER_OF_SKELETONS_OF_COLLECTION_CARDS = 5; export const CollectionsCardsSkeleton = () => { return ( - <> + <div className="flex flex-col space-y-2.5"> {[...Array(NUMBER_OF_SKELETONS_OF_COLLECTION_CARDS).keys()].map( (collection) => { return ( @@ -49,17 +49,17 @@ export const CollectionsCardsSkeleton = () => { ); }, )} - </> + </div> ); }; const CollectionCardSkeleton = () => { return ( - <div className="flex"> + <div className="flex h-[86px] py-3"> <div> - <Skeleton className="w-[72px] h-[72px]" /> + <Skeleton className="w-[73px] h-[73px]" /> </div> - <div className="w-full flex flex-col justify-start space-y-2 ml-4 items-start"> + <div className="w-full flex flex-col justify-start space-y-2 ml-[18px] items-start"> <Skeleton className="w-[100px] md:w-[200px] h-4 mr-auto" /> <Skeleton className="w-[180px] md:w-[320px] h-3 mr-auto" /> <div className="flex gap-2 relative"> diff --git a/apps/namegraph.dev/components/ui/toggle.tsx b/apps/namegraph.dev/components/ui/toggle.tsx new file mode 100644 index 000000000..9c930a48d --- /dev/null +++ b/apps/namegraph.dev/components/ui/toggle.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as React from "react" +import * as TogglePrimitive from "@radix-ui/react-toggle" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const toggleVariants = cva( + "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-neutral-100 hover:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-neutral-100 data-[state=on]:text-neutral-900 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:hover:bg-neutral-800 dark:hover:text-neutral-400 dark:focus-visible:ring-neutral-300 dark:data-[state=on]:bg-neutral-800 dark:data-[state=on]:text-neutral-50", + { + variants: { + variant: { + default: "bg-transparent", + outline: + "border border-neutral-200 bg-transparent shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:hover:bg-neutral-800 dark:hover:text-neutral-50", + }, + size: { + default: "h-9 px-2 min-w-9", + sm: "h-8 px-1.5 min-w-8", + lg: "h-10 px-2.5 min-w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +const Toggle = React.forwardRef< + React.ElementRef<typeof TogglePrimitive.Root>, + React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & + VariantProps<typeof toggleVariants> +>(({ className, variant, size, ...props }, ref) => ( + <TogglePrimitive.Root + ref={ref} + className={cn(toggleVariants({ variant, size, className }))} + {...props} + /> +)) + +Toggle.displayName = TogglePrimitive.Root.displayName + +export { Toggle, toggleVariants } diff --git a/apps/namegraph.dev/lib/utils.ts b/apps/namegraph.dev/lib/utils.ts index f06d2775b..db52b3839 100644 --- a/apps/namegraph.dev/lib/utils.ts +++ b/apps/namegraph.dev/lib/utils.ts @@ -3,12 +3,14 @@ import { twMerge } from "tailwind-merge"; import { DEFAULT_FULL_MODE, NameGraphCollection, + NameGraphCollectionByMemberResponse, NameGraphFetchTopCollectionMembersResponse, NameGraphFindCollectionsResponse, NameGraphGroupedByCategoryResponse, NameGraphGroupingCategory, NameGraphSortOrderOptions, NameGraphSuggestion, + ScrambleMethod, } from "@namehash/namegraph-sdk/utils"; import { createNameGraphClient } from "@namehash/namegraph-sdk"; @@ -159,6 +161,21 @@ export const fetchCollectionMembers = async ( return nameGeneratorSuggestions; }; +export const findCollectionsByMember = async ( + query: string, + options?: { + offset?: number; + max_results?: number; + limit_names?: number; + sort_order?: NameGraphSortOrderOptions; + }, +): Promise<NameGraphCollectionByMemberResponse> => { + const nameGeneratorSuggestions = + await NameGraphClient.findCollectionsByMember(query, options); + + return nameGeneratorSuggestions; +}; + export const getCollectionById = async ( collection_id: string, ): Promise<NameGraphCollection> => { @@ -183,6 +200,7 @@ export const scrambleNamesByCollectionId = async ( collectionId: string, options?: { seed?: number; + method?: ScrambleMethod; }, ): Promise<NameGraphSuggestion[]> => { const nameGeneratorSuggestions = diff --git a/apps/namegraph.dev/package.json b/apps/namegraph.dev/package.json index 1e63021dc..780e7e7b2 100644 --- a/apps/namegraph.dev/package.json +++ b/apps/namegraph.dev/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-select": "2.1.4", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-toggle": "^1.1.1", "class-variance-authority": "^0.7.1", "classcat": "5.0.5", "clsx": "^2.1.1", diff --git a/packages/namegraph-sdk/src/index.ts b/packages/namegraph-sdk/src/index.ts index 5e71dc520..69af50b1b 100644 --- a/packages/namegraph-sdk/src/index.ts +++ b/packages/namegraph-sdk/src/index.ts @@ -21,6 +21,7 @@ import { DEFAULT_INSTANT_MODE, NameGraphSortOrderOptions, NameGraphCollection, + ScrambleMethod, } from "./utils"; export class NameGraph { @@ -118,7 +119,8 @@ export class NameGraph { }, ): Promise<NameGraphSuggestion[]> { const max_sample_size = 5; - const seed = options?.seed || 0; + const random_seed = Number(Number(Math.random() * 10).toFixed(0)); + const seed = options?.seed || random_seed; const payload = { collection_id, @@ -149,12 +151,15 @@ export class NameGraph { collection_id: string, options?: { seed?: number; + method?: ScrambleMethod; }, ): Promise<NameGraphSuggestion[]> { - const method = "left-right-shuffle-with-unigrams"; + const default_method = ScrambleMethod["left-right-shuffle-with-unigrams"]; + const method = options?.method || default_method; const n_top_members = 25; const max_suggestions = 10; - const seed = options?.seed || 0; + const random_seed = Number(Number(Math.random() * 10).toFixed(0)); + const seed = options?.seed || random_seed; const payload = { collection_id, @@ -276,11 +281,17 @@ export class NameGraph { public findCollectionsByMember( label: string, + options?: { + offset?: number; + max_results?: number; + limit_names?: number; + sort_order?: NameGraphSortOrderOptions; + }, ): Promise<NameGraphCollectionByMemberResponse> { - const limit_names = 10; - const offset = 0; - const sort_order = "AI"; - const max_results = 3; + const limit_names = options?.limit_names || 10; + const offset = options?.offset || 0; + const sort_order = options?.sort_order || NameGraphSortOrderOptions.AI; + const max_results = options?.max_results || 3; const payload = { limit_names, diff --git a/packages/namegraph-sdk/src/utils.ts b/packages/namegraph-sdk/src/utils.ts index 251232da7..bfb641204 100644 --- a/packages/namegraph-sdk/src/utils.ts +++ b/packages/namegraph-sdk/src/utils.ts @@ -109,7 +109,7 @@ export type NameGraphCollectionByMemberResponse = { elasticsearch_processing_time_ms: number; elasticsearch_communication_time_ms: number; }; - collections: NameGraphSuggestion[]; + collections: NameGraphCollection[]; }; export type NameGraphCollection = { @@ -245,3 +245,12 @@ export const sampleWritersBlockSuggestions = ( return result; }; + +export const ScrambleMethod = { + "left-right-shuffle": "left-right-shuffle", + "left-right-shuffle-with-unigrams": "left-right-shuffle-with-unigrams", + "full-shuffle": "full-shuffle", +} as const; + +export type ScrambleMethod = + (typeof ScrambleMethod)[keyof typeof ScrambleMethod]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee729f1ef..ac51609c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -148,6 +148,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.1 version: 1.1.1(@types/react@18.3.1)(react@18.3.1) + '@radix-ui/react-toggle': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -1986,6 +1989,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-toggle@1.1.1': + resolution: {integrity: sha512-i77tcgObYr743IonC1hrsnnPmszDRn8p+EGUsUt+5a/JFn28fxaM88Py6V2mc8J5kELMWishI0rLnuGLFD/nnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -6796,6 +6812,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.1 + '@radix-ui/react-toggle@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.1 + '@types/react-dom': 18.3.1 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.1)(react@18.3.1)': dependencies: react: 18.3.1