From 0d967e56e4e5c18092bc12658a5259d4c3185c2d Mon Sep 17 00:00:00 2001 From: JCNoguera <88061365+VmMad@users.noreply.github.com> Date: Thu, 7 Nov 2024 11:43:22 +0100 Subject: [PATCH] feat(wallet-dashboard): add grid for visual assets (#3758) * feat(wallet-dashboard): add protected layout * feat: add missing TopNav * fix: tests * fix: imports * feat(wallet-dashboard): add assets page * fix: add missing themes * feat: improve connect button size * revert: "feat: improve connect button size" * feat(wallet-dashboard): add grid for visual assets * refactor: remove unnecessary useMemo * fix: remove commented lines * fix: improve type * fix: update IotaLogo --------- Co-authored-by: evavirseda --- apps/core/src/hooks/index.ts | 1 + apps/core/src/hooks/useCursorPagination.ts | 56 +++++++++++++++ .../activity/EpochsActivityTable.tsx | 3 +- .../activity/TransactionsActivityTable.tsx | 3 +- .../checkpoints/CheckpointsTable.tsx | 3 +- .../components/owned-objects/OwnedObjects.tsx | 9 ++- .../explorer/src/components/ui/Pagination.tsx | 40 +---------- .../CheckpointTransactionBlocks.tsx | 3 +- .../atoms/visual-asset-card/index.ts | 2 +- .../app/(protected)/assets/page.tsx | 68 ++++++++++++------- apps/wallet-dashboard/app/globals.css | 4 ++ .../components/Cards/AssetCard.tsx | 47 +++++++------ .../components/Cards/index.ts | 2 +- .../components/PageSizeSelector.tsx | 38 +++++++++++ .../components/PaginationOptions.tsx | 62 +++++++++++++++++ apps/wallet-dashboard/components/index.ts | 2 + .../src/components/icons/IotaIcon.tsx | 16 +++-- 17 files changed, 256 insertions(+), 103 deletions(-) create mode 100644 apps/core/src/hooks/useCursorPagination.ts create mode 100644 apps/wallet-dashboard/components/PageSizeSelector.tsx create mode 100644 apps/wallet-dashboard/components/PaginationOptions.tsx diff --git a/apps/core/src/hooks/index.ts b/apps/core/src/hooks/index.ts index 0219881a4d2..fe71839183a 100644 --- a/apps/core/src/hooks/index.ts +++ b/apps/core/src/hooks/index.ts @@ -37,5 +37,6 @@ export * from './useUnlockTimelockedObjectsTransaction'; export * from './useGetAllOwnedObjects'; export * from './useGetTimelockedStakedObjects'; export * from './useGetActiveValidatorsInfo'; +export * from './useCursorPagination'; export * from './stake'; diff --git a/apps/core/src/hooks/useCursorPagination.ts b/apps/core/src/hooks/useCursorPagination.ts new file mode 100644 index 00000000000..c1c8c6cfc98 --- /dev/null +++ b/apps/core/src/hooks/useCursorPagination.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { useState } from 'react'; +import { type InfiniteData, type UseInfiniteQueryResult } from '@tanstack/react-query'; + +interface PaginationProps { + hasFirst: boolean; + hasPrev: boolean; + hasNext: boolean; + onFirst(): void; + onPrev(): void; + onNext(): void; +} + +interface CursorPaginationProps extends PaginationProps { + currentPage: number; +} + +export interface PaginationResponse { + nextCursor: Cursor | null; + hasNextPage: boolean; +} + +export function useCursorPagination(query: UseInfiniteQueryResult>) { + const [currentPage, setCurrentPage] = useState(0); + + return { + ...query, + data: query.data?.pages[currentPage], + pagination: { + onFirst: () => setCurrentPage(0), + onNext: () => { + if (!query.data || query.isFetchingNextPage) { + return; + } + + // Make sure we are at the end before fetching another page + if (currentPage >= query.data.pages.length - 1) { + query.fetchNextPage(); + } + + setCurrentPage(currentPage + 1); + }, + onPrev: () => { + setCurrentPage(Math.max(currentPage - 1, 0)); + }, + hasFirst: currentPage !== 0, + hasNext: + !query.isFetchingNextPage && + (currentPage < (query.data?.pages.length ?? 0) - 1 || !!query.hasNextPage), + hasPrev: currentPage !== 0, + currentPage, + } satisfies CursorPaginationProps, + }; +} diff --git a/apps/explorer/src/components/activity/EpochsActivityTable.tsx b/apps/explorer/src/components/activity/EpochsActivityTable.tsx index d3f50c0ad5b..f0db0b602ce 100644 --- a/apps/explorer/src/components/activity/EpochsActivityTable.tsx +++ b/apps/explorer/src/components/activity/EpochsActivityTable.tsx @@ -4,10 +4,11 @@ import { InfoBox, InfoBoxStyle, InfoBoxType, Select, SelectSize } from '@iota/apps-ui-kit'; import { useIotaClientQuery, useIotaClient, useIotaClientInfiniteQuery } from '@iota/dapp-kit'; +import { useCursorPagination } from '@iota/core'; import { Warning } from '@iota/ui-icons'; import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; -import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; +import { PlaceholderTable, TableCard } from '~/components/ui'; import { PAGE_SIZES_RANGE_20_60 } from '~/lib/constants'; import { generateEpochsTableColumns } from '~/lib/ui'; import { numberSuffix } from '~/lib/utils'; diff --git a/apps/explorer/src/components/activity/TransactionsActivityTable.tsx b/apps/explorer/src/components/activity/TransactionsActivityTable.tsx index ac46b7e9dc2..e5f06e71af0 100644 --- a/apps/explorer/src/components/activity/TransactionsActivityTable.tsx +++ b/apps/explorer/src/components/activity/TransactionsActivityTable.tsx @@ -5,7 +5,8 @@ import { useIotaClient } from '@iota/dapp-kit'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; -import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; +import { PlaceholderTable, TableCard } from '~/components/ui'; +import { useCursorPagination } from '@iota/core'; import { DEFAULT_TRANSACTIONS_LIMIT, useGetTransactionBlocks, diff --git a/apps/explorer/src/components/checkpoints/CheckpointsTable.tsx b/apps/explorer/src/components/checkpoints/CheckpointsTable.tsx index 46baed3dd09..6d90547d25e 100644 --- a/apps/explorer/src/components/checkpoints/CheckpointsTable.tsx +++ b/apps/explorer/src/components/checkpoints/CheckpointsTable.tsx @@ -6,11 +6,12 @@ import { InfoBox, InfoBoxStyle, InfoBoxType, Select, SelectSize } from '@iota/ap import { useIotaClientQuery } from '@iota/dapp-kit'; import { Warning } from '@iota/ui-icons'; import { useMemo, useState } from 'react'; -import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; +import { PlaceholderTable, TableCard } from '~/components/ui'; import { DEFAULT_CHECKPOINTS_LIMIT, useGetCheckpoints } from '~/hooks/useGetCheckpoints'; import { PAGE_SIZES_RANGE_20_60 } from '~/lib/constants'; import { generateCheckpointsTableColumns } from '~/lib/ui'; import { numberSuffix } from '~/lib/utils'; +import { useCursorPagination } from '@iota/core'; interface CheckpointsTableProps { disablePagination?: boolean; diff --git a/apps/explorer/src/components/owned-objects/OwnedObjects.tsx b/apps/explorer/src/components/owned-objects/OwnedObjects.tsx index a55f4ab536e..f7f7aa07733 100644 --- a/apps/explorer/src/components/owned-objects/OwnedObjects.tsx +++ b/apps/explorer/src/components/owned-objects/OwnedObjects.tsx @@ -2,7 +2,12 @@ // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useGetKioskContents, useGetOwnedObjects, useLocalStorage } from '@iota/core'; +import { + useGetKioskContents, + useGetOwnedObjects, + useLocalStorage, + useCursorPagination, +} from '@iota/core'; import { Button, ButtonSize, @@ -27,7 +32,7 @@ import clsx from 'clsx'; import { useEffect, useMemo, useState } from 'react'; import { ListView, SmallThumbnailsView, ThumbnailsView } from '~/components'; import { ObjectViewMode } from '~/lib/enums'; -import { Pagination, useCursorPagination } from '~/components/ui'; +import { Pagination } from '~/components/ui'; import { PAGE_SIZES_RANGE_10_50 } from '~/lib/constants'; const SHOW_PAGINATION_MAX_ITEMS = 9; diff --git a/apps/explorer/src/components/ui/Pagination.tsx b/apps/explorer/src/components/ui/Pagination.tsx index af200b605f0..3dceec84667 100644 --- a/apps/explorer/src/components/ui/Pagination.tsx +++ b/apps/explorer/src/components/ui/Pagination.tsx @@ -4,7 +4,6 @@ import { Button, ButtonSize, ButtonType } from '@iota/apps-ui-kit'; import { ArrowLeft, ArrowRight, DoubleArrowLeft } from '@iota/ui-icons'; -import { type InfiniteData, type UseInfiniteQueryResult } from '@tanstack/react-query'; import { useState } from 'react'; interface PaginationProps { @@ -16,49 +15,12 @@ interface PaginationProps { onNext(): void; } -interface CursorPaginationProps extends PaginationProps { - currentPage: number; -} - export interface PaginationResponse { nextCursor: Cursor | null; hasNextPage: boolean; } -export function useCursorPagination(query: UseInfiniteQueryResult>) { - const [currentPage, setCurrentPage] = useState(0); - - return { - ...query, - data: query.data?.pages[currentPage], - pagination: { - onFirst: () => setCurrentPage(0), - onNext: () => { - if (!query.data || query.isFetchingNextPage) { - return; - } - - // Make sure we are at the end before fetching another page - if (currentPage >= query.data.pages.length - 1) { - query.fetchNextPage(); - } - - setCurrentPage(currentPage + 1); - }, - onPrev: () => { - setCurrentPage(Math.max(currentPage - 1, 0)); - }, - hasFirst: currentPage !== 0, - hasNext: - !query.isFetchingNextPage && - (currentPage < (query.data?.pages.length ?? 0) - 1 || !!query.hasNextPage), - hasPrev: currentPage !== 0, - currentPage, - } satisfies CursorPaginationProps, - }; -} - -/** @deprecated Prefer `useCursorPagination` + `useInfiniteQuery` for pagination. */ +/** @deprecated Prefer `useCursorPagination` from core + `useInfiniteQuery` for pagination. */ export function usePaginationStack() { const [stack, setStack] = useState([]); diff --git a/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx b/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx index f5370c56255..0cc6b34f8fd 100644 --- a/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx +++ b/apps/explorer/src/pages/checkpoints/CheckpointTransactionBlocks.tsx @@ -4,7 +4,8 @@ import { DropdownPosition, Select, SelectSize } from '@iota/apps-ui-kit'; import { useState } from 'react'; -import { PlaceholderTable, TableCard, useCursorPagination } from '~/components/ui'; +import { PlaceholderTable, TableCard } from '~/components/ui'; +import { useCursorPagination } from '@iota/core'; import { DEFAULT_TRANSACTIONS_LIMIT, useGetTransactionBlocks, diff --git a/apps/ui-kit/src/lib/components/atoms/visual-asset-card/index.ts b/apps/ui-kit/src/lib/components/atoms/visual-asset-card/index.ts index ce1d450e13d..05a2d49f88d 100644 --- a/apps/ui-kit/src/lib/components/atoms/visual-asset-card/index.ts +++ b/apps/ui-kit/src/lib/components/atoms/visual-asset-card/index.ts @@ -1,5 +1,5 @@ // Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -export { VisualAssetCard } from './VisualAssetCard'; +export * from './VisualAssetCard'; export * from './visual-asset-card.enums'; diff --git a/apps/wallet-dashboard/app/(protected)/assets/page.tsx b/apps/wallet-dashboard/app/(protected)/assets/page.tsx index 1f1f4d085f3..db4929addcf 100644 --- a/apps/wallet-dashboard/app/(protected)/assets/page.tsx +++ b/apps/wallet-dashboard/app/(protected)/assets/page.tsx @@ -3,15 +3,18 @@ 'use client'; -import { AssetCard, VirtualList } from '@/components'; +import { AssetCard, PageSizeSelector, PaginationOptions } from '@/components'; import { ASSETS_ROUTE } from '@/lib/constants/routes.constants'; -import { Panel, Title, Chip } from '@iota/apps-ui-kit'; -import { hasDisplayData, useGetOwnedObjects } from '@iota/core'; +import { Panel, Title, Chip, TitleSize, DropdownPosition } from '@iota/apps-ui-kit'; +import { hasDisplayData, useCursorPagination, useGetOwnedObjects } from '@iota/core'; import { useCurrentAccount } from '@iota/dapp-kit'; import { IotaObjectData } from '@iota/iota-sdk/client'; import { useState } from 'react'; import { AssetCategory } from '@/lib/enums'; -import Link from 'next/link'; +import { VisibilityOff } from '@iota/ui-icons'; +import { useRouter } from 'next/navigation'; + +const PAGINATION_RANGE = [20, 40, 60]; const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [ { @@ -26,22 +29,22 @@ const ASSET_CATEGORIES: { label: string; value: AssetCategory }[] = [ export default function AssetsDashboardPage(): React.JSX.Element { const [selectedCategory, setSelectedCategory] = useState(AssetCategory.Visual); + const [limit, setLimit] = useState(PAGINATION_RANGE[1]); + const router = useRouter(); const account = useCurrentAccount(); - const { - data: ownedObjects, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - } = useGetOwnedObjects(account?.address); + const ownedObjectsQuery = useGetOwnedObjects(account?.address, undefined, limit); + + const { data, pagination } = useCursorPagination(ownedObjectsQuery); + + const { data: ownedObjects } = data || {}; const [visual, nonVisual] = (() => { const visual: IotaObjectData[] = []; const nonVisual: IotaObjectData[] = []; - ownedObjects?.pages - .flatMap((page) => page.data) - .filter((asset) => asset.data && asset.data.objectId) + ownedObjects + ?.filter((asset) => asset.data && asset.data.objectId) .forEach((asset) => { if (asset.data) { if (hasDisplayData(asset)) { @@ -64,7 +67,7 @@ export default function AssetsDashboardPage(): React.JSX.Element { return ( - + <Title title="Assets" size={TitleSize.Medium} /> <div className="px-lg"> <div className="flex flex-row items-center justify-start gap-xs py-xs"> {ASSET_CATEGORIES.map((tab) => ( @@ -77,18 +80,31 @@ export default function AssetsDashboardPage(): React.JSX.Element { ))} </div> - <div className="max-h-[600px] overflow-auto py-sm"> - <VirtualList - items={assetList} - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} - fetchNextPage={fetchNextPage} - estimateSize={() => 180} - render={(asset) => ( - <Link href={ASSETS_ROUTE.path + `/${asset.objectId}`}> - <AssetCard asset={asset} /> - </Link> - )} + <div className="grid-template-visual-assets grid max-h-[600px] gap-md overflow-auto py-sm"> + {assetList.map((asset) => ( + <div key={asset.digest}> + <AssetCard + asset={asset} + icon={<VisibilityOff />} + onClick={() => + router.push(ASSETS_ROUTE.path + `/${asset.objectId}`) + } + /> + </div> + ))} + </div> + <div className="flex flex-row items-center justify-end py-xs"> + <PaginationOptions + pagination={pagination} + action={ + <PageSizeSelector + pagination={pagination} + range={PAGINATION_RANGE} + dropdownPosition={DropdownPosition.Top} + setLimit={(e) => setLimit(e)} + limit={limit.toString()} + /> + } /> </div> </div> diff --git a/apps/wallet-dashboard/app/globals.css b/apps/wallet-dashboard/app/globals.css index 0f6fa810858..37de49ae479 100644 --- a/apps/wallet-dashboard/app/globals.css +++ b/apps/wallet-dashboard/app/globals.css @@ -81,4 +81,8 @@ body { 'coins activity activity'; } } + + .grid-template-visual-assets { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } } diff --git a/apps/wallet-dashboard/components/Cards/AssetCard.tsx b/apps/wallet-dashboard/components/Cards/AssetCard.tsx index 1fda04703e9..4aa47e8801d 100644 --- a/apps/wallet-dashboard/components/Cards/AssetCard.tsx +++ b/apps/wallet-dashboard/components/Cards/AssetCard.tsx @@ -5,38 +5,37 @@ import { IotaObjectData } from '@iota/iota-sdk/client'; import React from 'react'; -import { Box, ExternalImage } from '@/components/index'; import { useGetNFTMeta } from '@iota/core'; import { FlexDirection } from '@/lib/ui/enums'; +import { VisualAssetCard, VisualAssetType, type VisualAssetCardProps } from '@iota/apps-ui-kit'; -interface AssetCardProps { +interface AssetCardProps extends Pick<VisualAssetCardProps, 'onClick' | 'onIconClick' | 'icon'> { asset: IotaObjectData; flexDirection?: FlexDirection; } -function AssetCard({ asset, flexDirection }: AssetCardProps): React.JSX.Element { +export function AssetCard({ + asset, + onClick, + onIconClick, + icon, +}: AssetCardProps): React.JSX.Element | null { const { data: nftMeta } = useGetNFTMeta(asset.objectId); + + if (!asset.display || !nftMeta || !nftMeta.imageUrl) { + return null; + } + return ( - <Box> - <div className={`flex ${flexDirection} w-full gap-2`}> - {asset.display && nftMeta && nftMeta.imageUrl && ( - <ExternalImage - src={nftMeta.imageUrl} - alt={nftMeta.name ?? asset.display.data?.name} - width={80} - height={80} - className="object-cover" - /> - )} - <div> - <p>Digest: {asset.digest}</p> - <p>Object ID: {asset.objectId}</p> - {asset.type ? <p>Type: {asset.type}</p> : null} - <p>Version: {asset.version}</p> - </div> - </div> - </Box> + <VisualAssetCard + assetSrc={nftMeta?.imageUrl ?? asset?.display?.data?.imageUrl ?? ''} + assetTitle={nftMeta?.name ?? asset?.display?.data?.name} + assetType={VisualAssetType.Image} + altText={nftMeta?.name ?? (asset?.display?.data?.name || 'NFT')} + isHoverable + icon={icon} + onClick={onClick} + onIconClick={onIconClick} + /> ); } - -export default AssetCard; diff --git a/apps/wallet-dashboard/components/Cards/index.ts b/apps/wallet-dashboard/components/Cards/index.ts index 59d8c5dd155..4f7b2563c0c 100644 --- a/apps/wallet-dashboard/components/Cards/index.ts +++ b/apps/wallet-dashboard/components/Cards/index.ts @@ -2,4 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 export { default as StakeCard } from './StakeCard'; -export { default as AssetCard } from './AssetCard'; +export * from './AssetCard'; diff --git a/apps/wallet-dashboard/components/PageSizeSelector.tsx b/apps/wallet-dashboard/components/PageSizeSelector.tsx new file mode 100644 index 00000000000..cc2e0fe1017 --- /dev/null +++ b/apps/wallet-dashboard/components/PageSizeSelector.tsx @@ -0,0 +1,38 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +'use client'; +import { Select, SelectSize } from '@iota/apps-ui-kit'; + +interface PageSizeSelectorProps { + limit: string; + setLimit: (limit: number) => void; + pagination: { + onFirst: () => void; + }; + range: number[]; + dropdownPosition?: React.ComponentProps<typeof Select>['dropdownPosition']; +} +export function PageSizeSelector({ + limit, + setLimit, + pagination, + range, + dropdownPosition, +}: PageSizeSelectorProps): React.JSX.Element { + return ( + <Select + value={limit.toString()} + options={range.map((size) => ({ + label: `${size} / page`, + id: size.toString(), + }))} + size={SelectSize.Small} + dropdownPosition={dropdownPosition} + onValueChange={(e) => { + setLimit(Number(e)); + pagination.onFirst(); + }} + /> + ); +} diff --git a/apps/wallet-dashboard/components/PaginationOptions.tsx b/apps/wallet-dashboard/components/PaginationOptions.tsx new file mode 100644 index 00000000000..eba852565cb --- /dev/null +++ b/apps/wallet-dashboard/components/PaginationOptions.tsx @@ -0,0 +1,62 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { Button, ButtonType, ButtonSize } from '@iota/apps-ui-kit'; +import { ArrowLeft, ArrowRight, DoubleArrowLeft, DoubleArrowRight } from '@iota/ui-icons'; + +interface PaginationOptionsProps { + pagination: { + onFirst?: () => void; + hasFirst?: boolean; + onPrev?: () => void; + hasPrev?: boolean; + onNext?: () => void; + hasNext?: boolean; + onLast?: () => void; + hasLast?: boolean; + }; + action?: React.ReactNode; +} + +export function PaginationOptions({ + pagination: { onFirst, hasFirst, onPrev, hasPrev, onNext, hasNext, onLast, hasLast }, + action, +}: PaginationOptionsProps): React.JSX.Element { + const PAGINATION_BUTTONS: React.ComponentProps<typeof Button>[] = [ + { + icon: <DoubleArrowLeft />, + disabled: !hasFirst, + onClick: onFirst, + }, + { + icon: <ArrowLeft />, + disabled: !hasPrev, + onClick: onPrev, + }, + { + icon: <ArrowRight />, + disabled: !hasNext, + onClick: onNext, + }, + { + icon: <DoubleArrowRight />, + disabled: !hasLast, + onClick: onLast, + }, + ]; + return ( + <div className="flex gap-2"> + <div className="flex flex-row items-center gap-2"> + {PAGINATION_BUTTONS.map((button, index) => ( + <Button + key={index} + type={ButtonType.Secondary} + size={ButtonSize.Small} + {...button} + /> + ))} + </div> + {action} + </div> + ); +} diff --git a/apps/wallet-dashboard/components/index.ts b/apps/wallet-dashboard/components/index.ts index 9e14f80e881..744534d3592 100644 --- a/apps/wallet-dashboard/components/index.ts +++ b/apps/wallet-dashboard/components/index.ts @@ -9,6 +9,8 @@ export { default as Input } from './Input'; export { default as VirtualList } from './VirtualList'; export { default as ExternalImage } from './ExternalImage'; export { default as Dropdown } from './Dropdown'; +export * from './PageSizeSelector'; +export * from './PaginationOptions'; export * from './account-balance/AccountBalance'; export * from './coins'; diff --git a/sdk/dapp-kit/src/components/icons/IotaIcon.tsx b/sdk/dapp-kit/src/components/icons/IotaIcon.tsx index 205cd3fc9f8..312d7620c31 100644 --- a/sdk/dapp-kit/src/components/icons/IotaIcon.tsx +++ b/sdk/dapp-kit/src/components/icons/IotaIcon.tsx @@ -6,13 +6,17 @@ import type { ComponentProps } from 'react'; export function IotaIcon(props: ComponentProps<'svg'>) { return ( - <svg width={28} height={28} fill="none" xmlns="http://www.w3.org/2000/svg" {...props}> - <rect width={28} height={28} rx={6} fill="#6FBCF0" /> + <svg + xmlns="http://www.w3.org/2000/svg" + width="28px" + height="28px" + fill="none" + viewBox="0 0 24 24" + {...props} + > <path - fillRule="evenodd" - clipRule="evenodd" - d="M7.942 20.527A6.875 6.875 0 0 0 13.957 24c2.51 0 4.759-1.298 6.015-3.473a6.875 6.875 0 0 0 0-6.945l-5.29-9.164a.837.837 0 0 0-1.45 0l-5.29 9.164a6.875 6.875 0 0 0 0 6.945Zm4.524-11.75 1.128-1.953a.418.418 0 0 1 .725 0l4.34 7.516a5.365 5.365 0 0 1 .449 4.442 4.675 4.675 0 0 0-.223-.73c-.599-1.512-1.954-2.68-4.029-3.47-1.426-.54-2.336-1.336-2.706-2.364-.476-1.326.021-2.77.316-3.44Zm-1.923 3.332L9.255 14.34a5.373 5.373 0 0 0 0 5.43 5.373 5.373 0 0 0 4.702 2.714 5.38 5.38 0 0 0 3.472-1.247c.125-.314.51-1.462.034-2.646-.44-1.093-1.5-1.965-3.15-2.594-1.864-.707-3.076-1.811-3.6-3.28a4.601 4.601 0 0 1-.17-.608Z" - fill="#fff" + fill="#171d26" + d="M18.27 2.259c0 .695-.568 1.259-1.268 1.259s-1.267-.564-1.267-1.259S16.302 1 17.002 1s1.268.564 1.268 1.259m1.425 17.785c0 .695-.568 1.259-1.268 1.259s-1.267-.564-1.267-1.26c0-.695.567-1.258 1.267-1.258s1.268.563 1.268 1.259m-2.33-13.379a1.06 1.06 0 0 0 1.063-1.056 1.06 1.06 0 0 0-1.063-1.057A1.06 1.06 0 0 0 16.3 5.61a1.06 1.06 0 0 0 1.064 1.056Zm3.891-.651a1.06 1.06 0 0 1-1.063 1.056 1.06 1.06 0 0 1-1.064-1.056 1.06 1.06 0 0 1 1.064-1.057 1.06 1.06 0 0 1 1.063 1.057m-4.253 3.329c.5 0 .905-.403.905-.9 0-.496-.405-.9-.905-.9s-.906.404-.906.9c0 .497.406.9.906.9m3.734-.52c0 .497-.405.9-.905.9a.903.903 0 0 1-.906-.9c0-.496.405-.9.906-.9.5 0 .905.404.905.9m1.357 1.777c.5 0 .906-.402.906-.899s-.405-.9-.906-.9c-.5 0-.905.403-.905.9s.405.9.905.9Zm-2.309.495a.79.79 0 0 1-.792.787.79.79 0 0 1-.791-.787c0-.434.354-.786.791-.786s.792.352.792.786m-3.62.404a.79.79 0 0 0 .792-.787.79.79 0 0 0-.792-.786.79.79 0 0 0-.791.786c0 .435.354.787.791.787m5.907.471a.79.79 0 0 1-.791.787.79.79 0 0 1-.792-.786.79.79 0 0 1 .792-.787.79.79 0 0 1 .791.787Zm-4.21 1.554c.375 0 .68-.302.68-.675a.677.677 0 0 0-.68-.675.68.68 0 0 0-.68.675c0 .373.304.675.68.675m2.968.178a.68.68 0 0 1-.68.676.677.677 0 0 1-.68-.676c0-.373.304-.675.68-.675.375 0 .68.302.68.675m-5.774-.582c.375 0 .68-.302.68-.675a.68.68 0 0 0-.68-.675.68.68 0 0 0-.68.675c0 .373.304.675.68.675m2.127.943c0 .31-.254.561-.566.561a.564.564 0 0 1-.566-.562c0-.31.253-.561.566-.561s.566.251.566.562m.407 2.114a.496.496 0 0 0 .498-.494.496.496 0 0 0-.498-.495.496.496 0 0 0-.498.495c0 .273.223.494.498.494m-.791-.09a.407.407 0 0 1-.408.405.407.407 0 0 1-.408-.405c0-.223.182-.405.408-.405s.408.181.408.405m-1.472-.765a.496.496 0 0 0 .498-.494.496.496 0 0 0-.498-.495.496.496 0 0 0-.498.495c0 .273.223.494.498.494m-.973-1.64c0 .31-.254.562-.566.562a.564.564 0 0 1-.566-.562c0-.31.253-.562.566-.562s.566.251.566.562m-.588-3.666a.564.564 0 0 0 .566-.562.564.564 0 0 0-.566-.562.564.564 0 0 0-.565.562c0 .31.253.562.565.562m.745-2.473a.496.496 0 0 1-.497.494.496.496 0 0 1-.498-.494c0-.273.223-.494.498-.494s.498.22.498.494Zm.047-1.123a.406.406 0 0 0 .407-.406.406.406 0 0 0-.407-.405.407.407 0 0 0-.408.405c0 .224.182.406.408.406m-.452-1.26a.496.496 0 0 1-.498.494.496.496 0 0 1-.498-.494c0-.273.223-.494.498-.494s.498.22.498.494m4.776 10.32a.564.564 0 0 0 .566-.561.564.564 0 0 0-.566-.562.564.564 0 0 0-.566.562c0 .31.253.562.566.562ZM12.883 4.439c0 .31-.254.562-.566.562a.564.564 0 0 1-.566-.562c0-.31.254-.562.566-.562s.566.251.566.562m-.18 2.945a.564.564 0 0 0 .566-.562.564.564 0 0 0-.566-.561.564.564 0 0 0-.566.561c0 .31.253.562.566.562m.045 1.597a.677.677 0 0 1-.68.676.68.68 0 0 1-.679-.676c0-.373.304-.675.68-.675.375 0 .68.302.68.675Zm-2.74.676a.79.79 0 0 0 .791-.786.79.79 0 0 0-.791-.787.79.79 0 0 0-.792.787c0 .434.354.786.792.786m-1.494-.36c0 .497-.405.9-.905.9a.903.903 0 0 1-.906-.9c0-.497.406-.9.906-.9s.905.403.905.9m-3.529 2.18a1.06 1.06 0 0 0 1.063-1.056 1.06 1.06 0 0 0-1.063-1.056A1.06 1.06 0 0 0 3.92 10.42a1.06 1.06 0 0 0 1.064 1.056Zm-1.45.9c0 .696-.568 1.26-1.268 1.26S1 13.073 1 12.377c0-.695.567-1.259 1.267-1.259s1.268.564 1.268 1.26Zm.386-3.531A1.06 1.06 0 0 0 4.985 7.79 1.06 1.06 0 0 0 3.92 6.733 1.06 1.06 0 0 0 2.858 7.79 1.06 1.06 0 0 0 3.92 8.846Zm3.53-2.181c0 .497-.405.9-.906.9a.903.903 0 0 1-.905-.9c0-.496.405-.9.905-.9s.906.404.906.9M6.162 5.182c.5 0 .905-.403.905-.9 0-.496-.405-.9-.905-.9s-.906.404-.906.9c0 .497.406.9.906.9m3.19-1.326a.79.79 0 0 1-.791.787.79.79 0 0 1-.792-.787c0-.434.354-.786.792-.786.437 0 .791.352.791.786m1.268.788c.375 0 .68-.303.68-.675a.677.677 0 0 0-.68-.676.68.68 0 0 0-.68.675c0 .373.304.676.68.676m1.063 1.708a.68.68 0 0 1-.68.675.677.677 0 0 1-.68-.675c0-.373.305-.675.68-.675s.68.302.68.675m-2.739.695a.79.79 0 0 0 .792-.786.79.79 0 0 0-.792-.786.79.79 0 0 0-.791.786c0 .434.354.786.791.786m-1.832 5.578a.407.407 0 0 1-.408.405.407.407 0 0 1-.408-.405c0-.223.182-.405.408-.405s.408.181.408.405m1.221.201a.496.496 0 0 0 .497-.494.496.496 0 0 0-.497-.495.496.496 0 0 0-.498.495c0 .273.223.494.498.494m2.354-1.236c0 .31-.253.562-.565.562a.564.564 0 0 1-.566-.562c0-.31.253-.562.566-.562s.565.252.565.562m-.135 2.361c.375 0 .68-.302.68-.675a.677.677 0 0 0-.68-.675.677.677 0 0 0-.68.675c0 .373.304.675.68.675m-1.608-.135c0 .31-.253.562-.566.562a.564.564 0 0 1-.565-.562c0-.31.253-.562.565-.562.313 0 .566.252.566.562m-2.488.538a.496.496 0 0 0 .498-.494.496.496 0 0 0-.498-.495.496.496 0 0 0-.498.495c0 .273.223.494.498.494m.588.991c0 .31-.254.562-.566.562a.564.564 0 0 1-.566-.562c0-.31.253-.562.566-.562s.566.252.566.562m1.742.855c.376 0 .68-.303.68-.675a.677.677 0 0 0-.68-.676.677.677 0 0 0-.68.675c0 .373.305.676.68.676m3.484-1.08a.79.79 0 0 1-.79.786.79.79 0 0 1-.792-.786c0-.435.354-.787.791-.787s.792.352.792.787Zm.794 2.743c.5 0 .906-.402.906-.9 0-.496-.406-.899-.906-.899s-.905.403-.905.9.405.9.905.9Zm-2.536-.518a.79.79 0 0 1-.792.786.79.79 0 0 1-.792-.787.79.79 0 0 1 .792-.786.79.79 0 0 1 .792.787m-2.669 2.316a.79.79 0 0 0 .791-.786.79.79 0 0 0-.792-.786.79.79 0 0 0-.791.786c0 .434.354.786.792.786m2.466 1.079c0 .497-.405.9-.905.9a.903.903 0 0 1-.906-.9c0-.496.405-.9.906-.9.5 0 .905.404.905.9M13.584 22a1.06 1.06 0 0 0 1.064-1.056 1.06 1.06 0 0 0-1.064-1.057 1.06 1.06 0 0 0-1.063 1.057A1.06 1.06 0 0 0 13.584 22m-1.379-2.788c0 .497-.405.9-.905.9a.903.903 0 0 1-.906-.9c0-.497.405-.9.905-.9s.906.403.906.9m3.123.54a1.06 1.06 0 0 0 1.063-1.057 1.06 1.06 0 0 0-1.063-1.056 1.06 1.06 0 0 0-1.064 1.056 1.06 1.06 0 0 0 1.064 1.057m-7.741-2.699a.677.677 0 0 1-.68.676.677.677 0 0 1-.68-.676c0-.373.305-.675.68-.675s.68.302.68.675" /> </svg> );