diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/GuideSection.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/GuideSection.tsx new file mode 100644 index 00000000000..8260d51d551 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/GuideSection.tsx @@ -0,0 +1,62 @@ +import { TrackedLinkTW } from "@/components/ui/tracked-link"; + +const TRACKING_CATEGORY = "storage"; + +const links = [ + { + title: "Documentation", + url: "https://docs.thirdweb.com/storage", + }, + { + title: "How To Upload And Pin Files to IPFS", + url: "https://blog.thirdweb.com/guides/how-to-upload-and-pin-files-to-ipfs-using-storage/", + }, +]; + +const videos = [ + { + title: "How To Easily Add IPFS Into Your Web3 App", + url: "https://www.youtube.com/watch?v=4Nnu9Cy7SKc", + }, + { + title: "How to Upload Files to IPFS (Step by Step Guide)", + url: "https://www.youtube.com/watch?v=wyYkpMgEVxE", + }, +]; + +export function GuidesSection() { + return ( +
+ + +
+ ); +} + +function LinkSectionCard(props: { + title: string; + links: { title: string; url: string }[]; +}) { + return ( +
+

+ {props.title} +

+ +
+ ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/SDKSection.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/SDKSection.tsx new file mode 100644 index 00000000000..68a86b0e93b --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/components/SDKSection.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { + type CodeEnvironment, + CodeSegment, +} from "@/components/blocks/code-segment.client"; +import { useState } from "react"; + +export function SDKSection() { + const [codeEnvironment, setCodeEnvironment] = + useState("javascript"); + + return ( +
+

+ Integrate into your app +

+ + +
+ ); +} + +const storageSnippets = { + react: `// Check out the latest docs here: https://portal.thirdweb.com/typescript/v5/storage + +import { ThirdwebProvider } from "thirdweb/react"; +import { upload } from "thirdweb/storage"; +import { MediaRenderer } from "thirdweb/react"; + +// Wrap your app in ThirdwebProvider +function Providers() { + return ( + + + + ); +} + +function UploadFiles() { + const uploadData = async () => { + const uri = await upload({ + client, // thirdweb client + files: [ + new File(["hello world"], "hello.txt"), + ], + }); + } + + return
...
+} + + // Supported types: image, video, audio, 3d model, html +function ShowFiles() { + return ( + + ); +}`, + javascript: `// Check out the latest docs here: https://portal.thirdweb.com/typescript/v5/storage + +import { upload } from "thirdweb/storage"; + +// Here we get the IPFS URI of where our metadata has been uploaded +const uri = await upload({ + client, + files: [ + new File(["hello world"], "hello.txt"), + ], +}); + +// This will log a URL like ipfs://QmWgbcjKWCXhaLzMz4gNBxQpAHktQK6MkLvBkKXbsoWEEy/0 +console.info(uri); + +// Here we a URL with a gateway that we can look at in the browser +const url = await download({ + client, + uri, +}).url; + +// This will log a URL like https://ipfs.thirdwebstorage.com/ipfs/QmWgbcjKWCXhaLzMz4gNBxQpAHktQK6MkLvBkKXbsoWEEy/0 +console.info(url);`, + + unity: `using Thirdweb; + +// Reference the SDK +var sdk = ThirdwebManager.Instance.SDK; + +// Create data +NFTMetadata meta = new NFTMetadata() +{ + name = "Unity NFT", + description = "Minted From Unity", + image = "ipfs://QmbpciV7R5SSPb6aT9kEBAxoYoXBUsStJkMpxzymV4ZcVc", +}; +string metaJson = Newtonsoft.Json.JsonConvert.SerializeObject(meta); + +// Upload raw text or from a file path +var response = await ThirdwebManager.Instance.SDK.storage.UploadText(metaJson);`, +}; diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/page.tsx index 63ef2086fe5..ea7da43ad1d 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/page.tsx @@ -1,257 +1,77 @@ -"use client"; - -import { ChakraProviderSetup } from "@/components/ChakraProviderSetup"; -import { - type CodeEnvironment, - CodeSegment, -} from "@/components/blocks/code-segment.client"; -import { Divider, Flex, GridItem, SimpleGrid, Tooltip } from "@chakra-ui/react"; -import { RelevantDataSection } from "components/dashboard/RelevantDataSection"; -import { useState } from "react"; -import { Card, Heading, Link, Text, TrackedCopyButton } from "tw-components"; +import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code"; +import Link from "next/link"; +import { GuidesSection } from "./components/GuideSection"; +import { SDKSection } from "./components/SDKSection"; import { YourFilesSection } from "./your-files"; -const TRACKING_CATEGORY = "storage"; - -const links = [ - { - title: "Documentation", - url: "https://docs.thirdweb.com/storage", - }, - { - title: "How To Upload And Pin Files to IPFS", - url: "https://blog.thirdweb.com/guides/how-to-upload-and-pin-files-to-ipfs-using-storage/", - }, -]; - -const videos = [ - { - title: "How To Easily Add IPFS Into Your Web3 App", - url: "https://www.youtube.com/watch?v=4Nnu9Cy7SKc", - }, - { - title: "How to Upload Files to IPFS (Step by Step Guide)", - url: "https://www.youtube.com/watch?v=wyYkpMgEVxE", - }, -]; - export default function Page() { - const [codeEnvironment, setCodeEnvironment] = - useState("javascript"); - return ( - - - - - - - - Gateway - - This is the structure of your unique gateway URL: - - - https://<your-client-id>.ipfscdn.io/ipfs/ - -
- - Copy code - - } - bgColor="backgroundCardHighlight" - borderRadius="xl" - placement="top" - shouldWrapChildren - > - - -
-
- - Gateway requests need to be authenticated using a client ID. You - can get it from the Project Settings page. - -
- - - CLI - - - - Using thirdweb CLI, you can easily upload files and folders to - IPFS from your terminal: - - - - npx thirdweb upload ./path/to/file-or-folder - -
- - Copy code - - } - bgColor="backgroundCardHighlight" - borderRadius="xl" - placement="top" - shouldWrapChildren - > - - -
-
- - If this is the first time that you are running this command, - you may have to first login using your secret key.{" "} - - Learn more here - - . - -
-
- - - Integrate into your app - - - -
-
- - - - - -
-
+
+

Storage

+
+
+ + + + + +
+
); } -const storageSnippets = { - react: `// Check out the latest docs here: https://portal.thirdweb.com/typescript/v5/storage - -import { ThirdwebProvider } from "thirdweb/react"; -import { upload } from "thirdweb/storage"; -import { MediaRenderer } from "thirdweb/react"; - -// Wrap your app in ThirdwebProvider -function Providers() { +function GatewaySection() { return ( - - - +
+

+ IPFS Gateway +

+ +

+ This is the structure of your unique gateway URL +

+ + + +

+ Gateway requests need to be authenticated using a client ID. You can get + it from the Project Settings page. +

+
); } -function UploadFiles() { - const uploadData = async () => { - const uri = await upload({ - client, // thirdweb client - files: [ - new File(["hello world"], "hello.txt"), - ], - }); - } - - return
...
-} - - // Supported types: image, video, audio, 3d model, html -function ShowFiles() { +function CLISection() { return ( - +
+

+ Upload with thirdweb CLI +

+ +

+ You can easily upload files and folders to IPFS from your terminal using + thirdweb CLI +

+ + + +

+ If this is the first time that you are running this command, you may + have to first login using your secret key.{" "} + + Learn more about thirdweb CLI + +

+
); -}`, - javascript: `// Check out the latest docs here: https://portal.thirdweb.com/typescript/v5/storage - -import { upload } from "thirdweb/storage"; - -// Here we get the IPFS URI of where our metadata has been uploaded -const uri = await upload({ - client, - files: [ - new File(["hello world"], "hello.txt"), - ], -}); - -// This will log a URL like ipfs://QmWgbcjKWCXhaLzMz4gNBxQpAHktQK6MkLvBkKXbsoWEEy/0 -console.info(uri); - -// Here we a URL with a gateway that we can look at in the browser -const url = await download({ - client, - uri, -}).url; - -// This will log a URL like https://ipfs.thirdwebstorage.com/ipfs/QmWgbcjKWCXhaLzMz4gNBxQpAHktQK6MkLvBkKXbsoWEEy/0 -console.info(url);`, - - unity: `using Thirdweb; - -// Reference the SDK -var sdk = ThirdwebManager.Instance.SDK; - -// Create data -NFTMetadata meta = new NFTMetadata() -{ - name = "Unity NFT", - description = "Minted From Unity", - image = "ipfs://QmbpciV7R5SSPb6aT9kEBAxoYoXBUsStJkMpxzymV4ZcVc", -}; -string metaJson = Newtonsoft.Json.JsonConvert.SerializeObject(meta); - -// Upload raw text or from a file path -var response = await ThirdwebManager.Instance.SDK.storage.UploadText(metaJson);`, -}; +} diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/your-files.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/your-files.tsx index d7e920abdb3..92c4c541bc7 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/your-files.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/storage/your-files.tsx @@ -1,19 +1,26 @@ "use client"; +import { PaginationButtons } from "@/components/pagination-buttons"; +import { CopyTextButton } from "@/components/ui/CopyTextButton"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ToolTipLabel } from "@/components/ui/tooltip"; import { DASHBOARD_STORAGE_URL } from "@/constants/env"; import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser"; -import { Flex, Tooltip } from "@chakra-ui/react"; -import { - keepPreviousData, - useMutation, - useQuery, - useQueryClient, -} from "@tanstack/react-query"; -import { createColumnHelper } from "@tanstack/react-table"; -import { TWQueryTable } from "components/shared/TWQueryTable"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { formatDistance } from "date-fns/formatDistance"; -import { useCallback, useState } from "react"; -import { Button, Card, Heading, Text, TrackedCopyButton } from "tw-components"; +import { PinOffIcon } from "lucide-react"; +import { useState } from "react"; import { toSize } from "utils/number"; interface PinnedFilesResponse { @@ -34,10 +41,11 @@ interface PinnedFile { // TODO: move to hooks file export const PINNED_FILES_QUERY_KEY_ROOT = "pinned-files"; -const DEFAULT_PAGE_SIZE = 50; +const pageSize = 10; + function usePinnedFilesQuery({ - page = 0, - pageSize = DEFAULT_PAGE_SIZE, + page, + pageSize, }: { page: number; pageSize: number; @@ -75,8 +83,6 @@ function usePinnedFilesQuery({ return (await res.json()) as PinnedFilesResponse; }, enabled: user.isLoggedIn && !!user.user?.address && !!user.user.jwt, - // keep the previous data while fetching new data - placeholderData: keepPreviousData, }); } @@ -105,121 +111,120 @@ function useUnpinFileMutation() { }); } -// END TOOD - -const TRACKING_CATEGORY = "storage-files"; - -const columnHelper = createColumnHelper(); - -const columns = [ - columnHelper.accessor((row) => row.ipfsHash, { - header: "IPFS Hash (CID)", - cell: ({ cell }) => { - const value = cell.getValue(); - - return ( - - {value} - - - ); - }, - }), - columnHelper.accessor((row) => toSize(BigInt(row.fileSizeBytes || 0), "MB"), { - header: "File Size", - cell: ({ cell }) => {cell.getValue()}, - }), - columnHelper.accessor((row) => row.pinnedAt, { - header: "Pinned", - cell: ({ cell }) => { - const date = new Date(cell.getValue()); - return ( - - {date.toLocaleString()} - - } - > - {formatDistance(date, new Date())} ago - - ); - }, - }), - columnHelper.accessor((row) => row.ipfsHash, { - id: "action", - header: "", - cell: ({ cell }) => , - }), -]; - const UnpinButton: React.FC<{ cid: string }> = ({ cid }) => { - const { mutateAsync, isPending } = useUnpinFileMutation(); + const unpinMutation = useUnpinFileMutation(); return ( - + + + ); }; export const YourFilesSection: React.FC = () => { - const user = useLoggedInUser(); - const [page, setPage] = useState(0); - - const query = usePinnedFilesQuery({ - page, - pageSize: DEFAULT_PAGE_SIZE, + const [page, setPage] = useState(1); + const pinnedFilesQuery = usePinnedFilesQuery({ + page: page - 1, + pageSize: pageSize, }); - const selectData = useCallback( - (data?: PinnedFilesResponse) => data?.result?.pinnedFiles || [], - [], - ); - const selectTotalCount = useCallback( - (data?: PinnedFilesResponse) => data?.result?.count || 0, - [], - ); + + const showPagination = pinnedFilesQuery.data + ? pinnedFilesQuery.data.result.count > pageSize + : false; + + const totalPages = pinnedFilesQuery.data + ? Math.ceil(pinnedFilesQuery.data.result.count / pageSize) + : 0; + + const pinnedFilesToShow = pinnedFilesQuery.data + ? pinnedFilesQuery.data.result.pinnedFiles + : undefined; + return ( - - +
+

Your Pinned Files - - - {user.isLoggedIn ? ( - - ) : ( - -
- - Please connect your wallet to see the files you have pinned. - +

+ +
+ + + + + + IPFS Hash (CID) + File Size + Pinned On + Unpin File + + + + {pinnedFilesToShow?.map((pinnedFile) => ( + + + + + + {toSize(BigInt(pinnedFile.fileSizeBytes || 0), "MB")} + + + + + {formatDistance( + new Date(pinnedFile.pinnedAt), + new Date(), + )} + + + + + + + + ))} + +
+ + {pinnedFilesQuery.data && pinnedFilesQuery.data.result.count === 0 && ( +
+ No Pinned Files +
+ )} + + {pinnedFilesQuery.isPending && ( +
+ +
+ )} + + {showPagination && ( +
+
- - )} - + )} +
+
); }; diff --git a/apps/dashboard/src/components/dashboard/RelevantDataSection.tsx b/apps/dashboard/src/components/dashboard/RelevantDataSection.tsx deleted file mode 100644 index ad7c976fa14..00000000000 --- a/apps/dashboard/src/components/dashboard/RelevantDataSection.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Flex } from "@chakra-ui/react"; -import { MoveRightIcon } from "lucide-react"; -import { useState } from "react"; -import { Heading, Text, TrackedLink } from "tw-components"; - -interface RelevantDataSectionProps { - data: { - title: string; - url: string; - }[]; - title: string; - TRACKING_CATEGORY: string; -} - -export const RelevantDataSection: React.FC = ({ - data, - title, - TRACKING_CATEGORY, -}) => { - const [showAllData, setShowAllData] = useState(false); - return data.length > 0 ? ( - - Relevant {title}s - - {data.slice(0, showAllData ? undefined : 3).map((item) => ( - - {item.title} - - ))} - {data.length > 3 && !showAllData ? ( - setShowAllData(true)} - cursor="pointer" - opacity={0.6} - color="heading" - _hover={{ opacity: 1, textDecoration: "none", color: "blue.500" }} - display="flex" - alignItems="center" - gap="0.5em" - > - View more - - ) : null} - - - ) : null; -}; diff --git a/apps/dashboard/src/components/shared/TWQueryTable.tsx b/apps/dashboard/src/components/shared/TWQueryTable.tsx deleted file mode 100644 index 0b761a0d32b..00000000000 --- a/apps/dashboard/src/components/shared/TWQueryTable.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { TableContainer } from "@/components/ui/table"; -import { - Box, - ButtonGroup, - Flex, - GridItem, - Select, - SimpleGrid, - Skeleton, - Spinner, - Table, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@chakra-ui/react"; -import type { UseQueryResult } from "@tanstack/react-query"; -import { - type ColumnDef, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table"; -import { MoveRightIcon } from "lucide-react"; -import pluralize from "pluralize"; -import { type SetStateAction, useMemo } from "react"; -import { Button, Text } from "tw-components"; - -type TWQueryTableProps = { - // biome-ignore lint/suspicious/noExplicitAny: FIXME - columns: ColumnDef[]; - query: UseQueryResult; - selectData: (data?: TInputData) => TRowData[]; - onRowClick?: (row: TRowData) => void; - pagination?: Omit, "data" | "isPending">; - title: string; -}; - -export function TWQueryTable( - tableProps: TWQueryTableProps, -) { - const data = tableProps.selectData(tableProps.query.data); - const isPending = tableProps.query.isPending; - const isFetching = tableProps.query.isFetching; - const isFetched = tableProps.query.isFetched; - - const table = useReactTable({ - data, - columns: tableProps.columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - {/* if the row is clickable we want an arrow to show */} - {tableProps.onRowClick && - ))} - - - - {table.getRowModel().rows.map((row) => { - return ( - tableProps.onRowClick?.(row.original), - _hover: { bg: "blackAlpha.50" }, - _dark: { - _hover: { - bg: "whiteAlpha.50", - }, - }, - } - : {})} - > - {row.getVisibleCells().map((cell) => { - return ( - - ); - })} - {/* if the row is clickable we want an arrow to show */} - {tableProps.onRowClick && ( - - )} - - ); - })} - -
- {header.isPlaceholder ? null : ( - - - {flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - - )} - } -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - -
- {isPending && ( -
- - - Loading {pluralize(tableProps.title, 0, false)} - -
- )} - - {!isPending && !isFetching && data.length === 0 && isFetched && ( -
- - No {pluralize(tableProps.title, 0, false)} found. - -
- )} -
- {/* render pagination if pagination is enabled */} - {tableProps.pagination && ( - - )} -
- ); -} - -type PaginationProps = { - data?: TInputData; - isPending: boolean; - pageSize: number; - setPageSize?: (value: SetStateAction) => void; - pageSizeOptions?: number[]; - page: number; - setPage: (value: SetStateAction) => void; - selectTotalCount: (data?: TInputData) => number; -}; - -const DEFAULT_PAGE_SIZE_OPTIONS = [25, 50, 100, 250]; - -const MAX_PAGE_BUTTONS = 7; - -type PageButton = - | { type: "page"; page: number } - | { type: "ellipsis"; key: string }; - -function Pagination(paginationProps: PaginationProps) { - const totalCount = paginationProps.selectTotalCount(paginationProps.data); - const totalPages = Math.ceil(totalCount / paginationProps.pageSize); - - const pageSizeOptions = - paginationProps.pageSizeOptions || DEFAULT_PAGE_SIZE_OPTIONS; - - const pagesToRender: PageButton[] = useMemo(() => { - // if there is only one page we don't need to render anything - if (totalPages <= 1) { - return []; - } - // if we have less than the max number of pages then we can just render all of them - if (totalPages <= MAX_PAGE_BUTTONS) { - return new Array(totalPages).fill(0).map((_, i) => ({ - type: "page", - page: i, - })); - } - - // otherwise compute the buttons to render - const pages = Array(MAX_PAGE_BUTTONS); - - const currentPage = paginationProps.page; - let startPage = Math.max(0, currentPage - 3); - let endPage = startPage + MAX_PAGE_BUTTONS - 1; - if (endPage > totalPages - 1) { - endPage = totalPages - 1; - startPage = Math.max(0, endPage - MAX_PAGE_BUTTONS + 1); - } - - for (let i = startPage; i <= endPage; i++) { - pages[i - startPage] = { type: "page", page: i }; - } - // if the second index is bigger than 2 then we need to show the first page and an ellipsis - if (pages[1].page > 2) { - pages[0] = { type: "page", page: 0 }; - pages[1] = { type: "ellipsis", key: "ellipsis_pre" }; - } - // if the second to last index is less than the total pages - 2 then we need to show an ellipsis and the last page - if (pages[pages.length - 2].page < totalPages - 2) { - pages[pages.length - 1] = { type: "page", page: totalPages - 1 }; - pages[pages.length - 2] = { type: "ellipsis", key: "ellipsis_post" }; - } - return pages; - }, [totalPages, paginationProps.page]); - - const buttonStringTemplate = useMemo(() => { - const maxPage = `${totalPages}`.length; - return new Array(maxPage).fill("0").join(""); - }, [totalPages]); - - return ( - - {/* filler box on left side for spacing */} - - -
- - {paginationProps.isPending - ? new Array(MAX_PAGE_BUTTONS).fill("0").map((val, i) => { - return ( - - - - ); - }) - : pagesToRender.map((page) => - page.type === "page" ? ( - - ) : ( - - ), - )} - -
-
- - {/* if we let the users set the page size then show a select to do that */} - - {paginationProps.setPageSize ? ( - - ) : ( - - )} - -
- ); -}