From fe44854bed2847175ab447ba8c64b7ef5a6dd26e Mon Sep 17 00:00:00 2001 From: shadrach Date: Fri, 4 Oct 2024 16:25:30 -0500 Subject: [PATCH] feat: communities view page and datatable --- src/app/(auth)/community/new/page.tsx | 12 +- src/app/(auth)/community/page.tsx | 17 +- src/app/(auth)/nodes/page.tsx | 55 +- src/app/ThemeProvider.tsx | 29 +- src/app/globals.scss | 13 + src/components/organisms/Communities.tsx | 26 + .../organisms/community-datatable/columns.tsx | 252 ++++++ .../data-table-column-header.tsx | 77 ++ .../data-table-faceted-filter.tsx | 156 ++++ .../data-table-pagination.tsx | 97 +++ .../data-table-row-actions.tsx | 48 ++ .../data-table-toolbar.tsx | 55 ++ .../data-table-view-options.tsx | 57 ++ .../community-datatable/data-table.tsx | 126 +++ .../community-datatable/data/community.ts | 17 + .../community-datatable/data/data.tsx | 19 + .../community-datatable/data/schema.ts | 27 + .../organisms/nodes-datatable/columns.tsx | 123 +++ .../data-table-column-header.tsx | 71 ++ .../data-table-faceted-filter.tsx | 147 ++++ .../nodes-datatable/data-table-pagination.tsx | 97 +++ .../data-table-row-actions.tsx | 67 ++ .../nodes-datatable/data-table-toolbar.tsx | 62 ++ .../data-table-view-options.tsx | 57 ++ .../organisms/nodes-datatable/data-table.tsx | 126 +++ .../organisms/nodes-datatable/data/data.tsx | 71 ++ .../organisms/nodes-datatable/data/schema.ts | 13 + .../organisms/nodes-datatable/data/tasks.ts | 782 ++++++++++++++++++ src/data/navlinks.tsx | 4 +- src/lib/api/index.ts | 48 ++ src/lib/get-query-client.ts | 40 + src/lib/tags.ts | 3 + tailwind.config.ts | 30 +- 33 files changed, 2725 insertions(+), 99 deletions(-) create mode 100644 src/components/organisms/Communities.tsx create mode 100644 src/components/organisms/community-datatable/columns.tsx create mode 100644 src/components/organisms/community-datatable/data-table-column-header.tsx create mode 100644 src/components/organisms/community-datatable/data-table-faceted-filter.tsx create mode 100644 src/components/organisms/community-datatable/data-table-pagination.tsx create mode 100644 src/components/organisms/community-datatable/data-table-row-actions.tsx create mode 100644 src/components/organisms/community-datatable/data-table-toolbar.tsx create mode 100644 src/components/organisms/community-datatable/data-table-view-options.tsx create mode 100644 src/components/organisms/community-datatable/data-table.tsx create mode 100644 src/components/organisms/community-datatable/data/community.ts create mode 100644 src/components/organisms/community-datatable/data/data.tsx create mode 100644 src/components/organisms/community-datatable/data/schema.ts create mode 100644 src/components/organisms/nodes-datatable/columns.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-column-header.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-faceted-filter.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-pagination.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-row-actions.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-toolbar.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table-view-options.tsx create mode 100644 src/components/organisms/nodes-datatable/data-table.tsx create mode 100644 src/components/organisms/nodes-datatable/data/data.tsx create mode 100644 src/components/organisms/nodes-datatable/data/schema.ts create mode 100644 src/components/organisms/nodes-datatable/data/tasks.ts create mode 100644 src/lib/api/index.ts create mode 100644 src/lib/get-query-client.ts create mode 100644 src/lib/tags.ts diff --git a/src/app/(auth)/community/new/page.tsx b/src/app/(auth)/community/new/page.tsx index 0671a14..479f0b1 100644 --- a/src/app/(auth)/community/new/page.tsx +++ b/src/app/(auth)/community/new/page.tsx @@ -21,6 +21,8 @@ import { Layout, LayoutBody } from "@/components/custom/Layout"; import { useFormState, useFormStatus } from "react-dom"; import { createCommunity } from "@/app/actions"; import { useEffect } from "react"; +import { getQueryClient } from "@/lib/get-query-client"; +import { tags } from "@/lib/tags"; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ACCEPTED_IMAGE_TYPES = [ @@ -139,14 +141,12 @@ export default function CommunityForm() { formAction(formData); } - console.log("form", state); - useEffect(() => { if (formState?.ok) { - form.reset(); - // todo: show success toast + form.reset(); + // todo: show success toast } - }, [form, formState]) + }, [form, formState]); return ( @@ -154,7 +154,7 @@ export default function CommunityForm() {

- Complete this form to add a new entry + Complete this form to add a new Community

diff --git a/src/app/(auth)/community/page.tsx b/src/app/(auth)/community/page.tsx index dd015ea..276f07f 100644 --- a/src/app/(auth)/community/page.tsx +++ b/src/app/(auth)/community/page.tsx @@ -1,14 +1,13 @@ -import { DataTable } from "@/components/organisms/users-datatable/data-table"; -import { tasks } from "@/components/organisms/users-datatable/data/tasks"; -import { columns } from "@/components/organisms/users-datatable/columns"; - +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import Communities from "@/components/organisms/Communities"; +import { getQueryClient } from "@/lib/get-query-client"; export default function CommunityPage() { + const queryClient = getQueryClient() + return ( - <> -
- -
- + + + ); } diff --git a/src/app/(auth)/nodes/page.tsx b/src/app/(auth)/nodes/page.tsx index 7830173..be0e927 100644 --- a/src/app/(auth)/nodes/page.tsx +++ b/src/app/(auth)/nodes/page.tsx @@ -1,51 +1,14 @@ -import { Button } from "@/components/ui/button"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { DataTable } from "@/components/organisms/nodes-datatable/data-table"; +import { tasks } from "@/components/organisms/nodes-datatable/data/tasks"; +import { columns } from "@/components/organisms/nodes-datatable/columns"; -interface Node { - id: string; - name: string; - status: "active" | "inactive"; -} - -const mockNodes: Node[] = [ - { id: "1", name: "Node 1", status: "active" }, - { id: "2", name: "Node 2", status: "inactive" }, -]; -export default function NodesTable() { +export default function NodesPage() { return ( - - - - Name - Status - Actions - - - - {mockNodes.map((node) => ( - - {node.name} - {node.status} - -
- - - -
-
-
- ))} -
-
+ <> +
+ +
+ ); } diff --git a/src/app/ThemeProvider.tsx b/src/app/ThemeProvider.tsx index c33f858..a1b0454 100644 --- a/src/app/ThemeProvider.tsx +++ b/src/app/ThemeProvider.tsx @@ -1,40 +1,15 @@ "use client"; import { ThemeProvider } from "@/components/theme-provider"; +import { getQueryClient } from "@/lib/get-query-client"; import { + defaultShouldDehydrateQuery, isServer, QueryClient, QueryClientProvider, } from "@tanstack/react-query"; import { PropsWithChildren } from "react"; -function makeQueryClient() { - return new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 60 * 1000, - }, - }, - }); -} - -let browserQueryClient: QueryClient | undefined = undefined; - -function getQueryClient() { - if (isServer) { - // Server: always make a new query client - return makeQueryClient(); - } else { - // Browser: make a new query client if we don't already have one - // This is very important, so we don't re-make a new client if React - // suspends during the initial render. This may not be needed if we - // have a suspense boundary BELOW the creation of the query client - if (!browserQueryClient) browserQueryClient = makeQueryClient(); - return browserQueryClient; - } -} export default function ThemeContext({ children }: PropsWithChildren<{}>) { const queryClient = getQueryClient(); diff --git a/src/app/globals.scss b/src/app/globals.scss index 6517b25..61251a9 100644 --- a/src/app/globals.scss +++ b/src/app/globals.scss @@ -53,6 +53,13 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --warning: 38 92% 50%; + --warning-foreground: 48 96% 89%; + --danger: 353, 100%, 71%; + --danger-foreground: 4, 100%, 51%; + --success: 123, 100%, 83%; + --success-foreground: 152, 100%, 50%; + } .dark { @@ -80,6 +87,12 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --warning: 48 96% 89%; + --warning-foreground: 38 92% 50%; + --danger: 2, 100%, 67%; + --danger-foreground: 1, 100%, 58%; + --success: 126, 100%, 84%; + --success-foreground: 156, 100%, 50%; } } diff --git a/src/components/organisms/Communities.tsx b/src/components/organisms/Communities.tsx new file mode 100644 index 0000000..5a33717 --- /dev/null +++ b/src/components/organisms/Communities.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { listCommunitiesQuery } from "@/lib/api"; +import { useSuspenseQuery } from "@tanstack/react-query"; +import { LoaderCircleIcon } from "lucide-react"; +import { DataTable } from "./community-datatable/data-table"; +import { columns } from "./community-datatable/columns"; + +export default function Communities() { + const { data , isLoading } = + useSuspenseQuery(listCommunitiesQuery); + + return ( + <> +
+ {isLoading ? ( +
+ +
+ ) : ( + + )} +
+ + ); +} diff --git a/src/components/organisms/community-datatable/columns.tsx b/src/components/organisms/community-datatable/columns.tsx new file mode 100644 index 0000000..98448f6 --- /dev/null +++ b/src/components/organisms/community-datatable/columns.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; +import { DataTableColumnHeader } from "./data-table-column-header"; +import { DataTableRowActions } from "./data-table-row-actions"; + +import { statuses } from "./data/data"; +import { Community } from "./data/schema"; +import { CheckCircledIcon, CrossCircledIcon } from "@radix-ui/react-icons"; +import { Avatar, AvatarImage } from "@/components/ui/avatar"; +import { cn } from "@/lib/utils"; + +export const columns: ColumnDef[] = [ + // { + // id: "select", + // header: ({ table }) => ( + // table.toggleAllPageRowsSelected(!!value)} + // aria-label="Select all" + // className="translate-y-[2px]" + // /> + // ), + // cell: ({ row }) => ( + // row.toggleSelected(!!value)} + // aria-label="Select row" + // className="translate-y-[2px]" + // /> + // ), + // enableSorting: false, + // enableHiding: false, + // }, + + { + accessorKey: "id", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("id")}
, + enableSorting: false, + }, + // { + // accessorKey: "image_url", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => ( + // + // + // + // ), + // enableSorting: false, + // }, + { + id: "logo", + header: ({ table }) => ( + + Icon + + ), + cell: ({ row }) => ( + + + + ), + enableSorting: false, + enableHiding: true, + }, + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("name")} + +
+ ); + }, + enableHiding: false, + }, + { + accessorKey: "subtitle", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("subtitle")} + +
+ ); + }, + }, + { + accessorKey: "slug", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("slug")} + +
+ ); + }, + }, + { + accessorKey: "hidden", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const status = statuses.find( + (status) => status.value === !row.getValue("hidden") + ); + + if (!status) return null; + + return ( +
+ {status.icon && ( + + )} + {status.label} +
+ ); + }, + filterFn: (row, id, value) => { + return value.includes((!row.getValue(id) as boolean).toString()); + }, + enableSorting: false, + }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const label = row.original.description; + + return ( +
+ + {label} + +
+ ); + }, + }, + { + accessorKey: "keywords", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const keywords = row.original.keywords; + + return ( +
+ {keywords?.slice(0, 2).map((label, idx) => ( + + {label} + + ))} + {keywords?.length > 2 && ( + + + {keywords.length - 2} + + )} +
+ ); + }, + enableHiding: true, + enableSorting: false, + // enableResizing: true, + }, + { + accessorKey: "image_url", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("image_url")} + +
+ ); + }, + enableSorting: false, + }, + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("createdAt")} + +
+ ); + }, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ + {row.getValue("updatedAt")} + +
+ ); + }, + }, + { + id: "actions", + cell: ({ row }) => , + }, +]; diff --git a/src/components/organisms/community-datatable/data-table-column-header.tsx b/src/components/organisms/community-datatable/data-table-column-header.tsx new file mode 100644 index 0000000..02b80ad --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-column-header.tsx @@ -0,0 +1,77 @@ +import { + ArrowDownIcon, + ArrowUpIcon, + CaretSortIcon, + EyeNoneIcon, +} from "@radix-ui/react-icons"; +import { Column } from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { cn } from "@/lib/utils"; + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column; + title: string; +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort() && !column.getCanHide()) { + return
{title}
; + } + + return ( +
+ + + + + + {column.getCanSort() && ( + <> + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + )} + + {column.getCanHide() && ( + column.toggleVisibility(false)}> + + Hide + + )} + + +
+ ); +} diff --git a/src/components/organisms/community-datatable/data-table-faceted-filter.tsx b/src/components/organisms/community-datatable/data-table-faceted-filter.tsx new file mode 100644 index 0000000..d8546e0 --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-faceted-filter.tsx @@ -0,0 +1,156 @@ +import * as React from "react"; +import { CheckIcon, PlusCircledIcon } from "@radix-ui/react-icons"; +import { Column } from "@tanstack/react-table"; + +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import { LoaderButton } from "@/components/custom/LoaderButton"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; + +interface DataTableFacetedFilterProps { + column?: Column; + title?: string; + options: { + label: string; + value: boolean; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function DataTableFacetedFilter({ + column, + title, + options, +}: DataTableFacetedFilterProps) { + const facets = column?.getFacetedUniqueValues(); + const selectedValues = new Set(column?.getFilterValue() as string[]); + + return ( + + + + + {title} + {selectedValues?.size > 0 && ( + <> + + + {selectedValues.size} + +
+ {selectedValues.size > 2 ? ( + + {selectedValues.size} selected + + ) : ( + options + .filter((option) => + selectedValues.has(option.value.toString()) + ) + .map((option) => ( + + {option.label} + + )) + )} +
+ + )} +
+
+ + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value.toString()); + return ( + { + if (isSelected) { + selectedValues.delete(option.value.toString()); + } else { + selectedValues.add(option.value.toString()); + } + const filterValues = Array.from(selectedValues); + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ); + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} + {facets?.get(!option.value) && ( + + {facets.get(!option.value)} + + )} +
+ ); + })} +
+ {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className="justify-center text-center" + > + Clear filters + + + + )} +
+
+
+
+ ); +} diff --git a/src/components/organisms/community-datatable/data-table-pagination.tsx b/src/components/organisms/community-datatable/data-table-pagination.tsx new file mode 100644 index 0000000..6c048d6 --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-pagination.tsx @@ -0,0 +1,97 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + +import { LoaderButton } from '@/components/custom/LoaderButton' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +interface DataTablePaginationProps { + table: Table +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} of{' '} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount()} +
+
+ table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + Go to first page + + + table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + Go to previous page + + + table.nextPage()} + disabled={!table.getCanNextPage()} + > + Go to next page + + + table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + Go to last page + + +
+
+
+ ) +} diff --git a/src/components/organisms/community-datatable/data-table-row-actions.tsx b/src/components/organisms/community-datatable/data-table-row-actions.tsx new file mode 100644 index 0000000..ace54fc --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-row-actions.tsx @@ -0,0 +1,48 @@ +import { DotsHorizontalIcon } from '@radix-ui/react-icons' +import { Row } from '@tanstack/react-table' + +import { LoaderButton } from '@/components/custom/LoaderButton' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +import { communitySchema } from './data/schema' + +interface DataTableRowActionsProps { + row: Row +} + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + const community = communitySchema.parse(row.original) + + return ( + + + + + Open menu + + + + Edit + {community.hidden ? 'Enable' : 'Disable'} + + + + Delete + ⌘⌫ + + + + ) +} diff --git a/src/components/organisms/community-datatable/data-table-toolbar.tsx b/src/components/organisms/community-datatable/data-table-toolbar.tsx new file mode 100644 index 0000000..1d3e853 --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-toolbar.tsx @@ -0,0 +1,55 @@ +import { Cross2Icon } from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + + +import { Input } from '@/components/ui/input' +import { DataTableViewOptions } from './data-table-view-options' + +import { statuses } from './data/data' +import { DataTableFacetedFilter } from './data-table-faceted-filter' +import { Button } from '@/components/ui/button' + +interface DataTableToolbarProps { + table: Table +} + +export function DataTableToolbar({ + table, +}: DataTableToolbarProps) { + const isFiltered = table.getState().columnFilters.length > 0 + + return ( +
+
+ + table.getColumn('name')?.setFilterValue(event.target.value) + } + className='h-8 w-[150px] lg:w-[250px]' + /> +
+ {table.getColumn('hidden') && ( + + )} +
+ {isFiltered && ( + + )} +
+ +
+ ) +} diff --git a/src/components/organisms/community-datatable/data-table-view-options.tsx b/src/components/organisms/community-datatable/data-table-view-options.tsx new file mode 100644 index 0000000..eb489d0 --- /dev/null +++ b/src/components/organisms/community-datatable/data-table-view-options.tsx @@ -0,0 +1,57 @@ +import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu' +import { MixerHorizontalIcon } from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, +} from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' + +interface DataTableViewOptionsProps { + table: Table +} + +export function DataTableViewOptions({ + table, +}: DataTableViewOptionsProps) { + return ( + + + + + + Toggle columns + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== 'undefined' && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} + + + ) +} diff --git a/src/components/organisms/community-datatable/data-table.tsx b/src/components/organisms/community-datatable/data-table.tsx new file mode 100644 index 0000000..6f5a875 --- /dev/null +++ b/src/components/organisms/community-datatable/data-table.tsx @@ -0,0 +1,126 @@ +"use client"; + +import * as React from 'react' +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +import { DataTablePagination } from './data-table-pagination' +import { DataTableToolbar } from './data-table-toolbar' + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [sorting, setSorting] = React.useState([]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + return ( +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ +
+ ) +} diff --git a/src/components/organisms/community-datatable/data/community.ts b/src/components/organisms/community-datatable/data/community.ts new file mode 100644 index 0000000..7a6bc4c --- /dev/null +++ b/src/components/organisms/community-datatable/data/community.ts @@ -0,0 +1,17 @@ +export const communities = Array.from({ length: 100 }, (_, index) => ({ + id: 9000 + index, + name: `Institution ${index}`, + subtitle: `Research and Development ${index}`, + description: `Exploring advanced technologies and innovations at Institution ${index}.`, + hidden: false, + keywords: ["research", "innovation", "technology", "institution"], + image_url: `https://pub.desci.com/ipfs/bafkreie7kxhzpzhsbywcrpgyv5yvy3qxcjsibuxsnsh5olaztl2uvnrzx4`, + slug: `institution-${index}`, + links: [ + `https://example.com/resource${index * 2 + 1}`, + `https://example.com/resource${index * 2 + 2}` + ], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), +})); + diff --git a/src/components/organisms/community-datatable/data/data.tsx b/src/components/organisms/community-datatable/data/data.tsx new file mode 100644 index 0000000..d3d75f3 --- /dev/null +++ b/src/components/organisms/community-datatable/data/data.tsx @@ -0,0 +1,19 @@ +import { + CheckCircledIcon, + CrossCircledIcon, +} from '@radix-ui/react-icons' + + +export const statuses = [ + { + value: true, + label: 'Enabled', + icon: CheckCircledIcon, + }, + { + value: false, + label: 'Disabled', + icon: CrossCircledIcon, + }, +] + diff --git a/src/components/organisms/community-datatable/data/schema.ts b/src/components/organisms/community-datatable/data/schema.ts new file mode 100644 index 0000000..59c8862 --- /dev/null +++ b/src/components/organisms/community-datatable/data/schema.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +// We're keeping a simple non-relational schema here. +// IRL, you will have a schema for your data models. +export const taskSchema = z.object({ + id: z.string(), + title: z.string(), + status: z.string(), + label: z.string(), + priority: z.string(), +}); + +export const communitySchema = z.object({ + id: z.number(), + name: z.string(), + subtitle: z.string(), + description: z.string(), + hidden: z.coerce.boolean(), + keywords: z.array(z.string()), + image_url: z.string().url(), //"https://pub.desci.com/ipfs/bafkreie7kxhzpzhsbywcrpgyv5yvy3qxcjsibuxsnsh5olaztl2uvnrzx4", + slug: z.string(), + links: z.array(z.string().url()), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +export type Community = z.infer; diff --git a/src/components/organisms/nodes-datatable/columns.tsx b/src/components/organisms/nodes-datatable/columns.tsx new file mode 100644 index 0000000..a5fc6bf --- /dev/null +++ b/src/components/organisms/nodes-datatable/columns.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { ColumnDef } from '@tanstack/react-table' + +import { Badge } from '@/components/ui/badge' +import { Checkbox } from '@/components/ui/checkbox' +import { DataTableColumnHeader } from './data-table-column-header' +import { DataTableRowActions } from './data-table-row-actions' + +import { labels, priorities, statuses } from './data/data' +import { Task } from './data/schema' + +export const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label='Select all' + className='translate-y-[2px]' + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label='Select row' + className='translate-y-[2px]' + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'id', + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue('id')}
, + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: 'title', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const label = labels.find((label) => label.value === row.original.label) + + return ( +
+ {label && {label.label}} + + {row.getValue('title')} + +
+ ) + }, + }, + { + accessorKey: 'status', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const status = statuses.find( + (status) => status.value === row.getValue('status') + ) + + if (!status) { + return null + } + + return ( +
+ {status.icon && ( + + )} + {status.label} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + accessorKey: 'priority', + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const priority = priorities.find( + (priority) => priority.value === row.getValue('priority') + ) + + if (!priority) { + return null + } + + return ( +
+ {priority.icon && ( + + )} + {priority.label} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + id: 'actions', + cell: ({ row }) => , + }, +] diff --git a/src/components/organisms/nodes-datatable/data-table-column-header.tsx b/src/components/organisms/nodes-datatable/data-table-column-header.tsx new file mode 100644 index 0000000..a33cb01 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-column-header.tsx @@ -0,0 +1,71 @@ +import { + ArrowDownIcon, + ArrowUpIcon, + CaretSortIcon, + EyeNoneIcon, +} from '@radix-ui/react-icons' +import { Column } from '@tanstack/react-table' + +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' + +interface DataTableColumnHeaderProps + extends React.HTMLAttributes { + column: Column + title: string +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
+ } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + column.toggleVisibility(false)}> + + Hide + + + +
+ ) +} diff --git a/src/components/organisms/nodes-datatable/data-table-faceted-filter.tsx b/src/components/organisms/nodes-datatable/data-table-faceted-filter.tsx new file mode 100644 index 0000000..e1f4a59 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-faceted-filter.tsx @@ -0,0 +1,147 @@ +import * as React from 'react' +import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons' +import { Column } from '@tanstack/react-table' + +import { cn } from '@/lib/utils' +import { Badge } from '@/components/ui/badge' +import { LoaderButton } from '@/components/custom/LoaderButton' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover' +import { Separator } from '@/components/ui/separator' + +interface DataTableFacetedFilterProps { + column?: Column + title?: string + options: { + label: string + value: string + icon?: React.ComponentType<{ className?: string }> + }[] +} + +export function DataTableFacetedFilter({ + column, + title, + options, +}: DataTableFacetedFilterProps) { + const facets = column?.getFacetedUniqueValues() + const selectedValues = new Set(column?.getFilterValue() as string[]) + + return ( + + + + + {title} + {selectedValues?.size > 0 && ( + <> + + + {selectedValues.size} + +
+ {selectedValues.size > 2 ? ( + + {selectedValues.size} selected + + ) : ( + options + .filter((option) => selectedValues.has(option.value)) + .map((option) => ( + + {option.label} + + )) + )} +
+ + )} +
+
+ + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value) + return ( + { + if (isSelected) { + selectedValues.delete(option.value) + } else { + selectedValues.add(option.value) + } + const filterValues = Array.from(selectedValues) + column?.setFilterValue( + filterValues.length ? filterValues : undefined + ) + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} + {facets?.get(option.value) && ( + + {facets.get(option.value)} + + )} +
+ ) + })} +
+ {selectedValues.size > 0 && ( + <> + + + column?.setFilterValue(undefined)} + className='justify-center text-center' + > + Clear filters + + + + )} +
+
+
+
+ ) +} diff --git a/src/components/organisms/nodes-datatable/data-table-pagination.tsx b/src/components/organisms/nodes-datatable/data-table-pagination.tsx new file mode 100644 index 0000000..6c048d6 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-pagination.tsx @@ -0,0 +1,97 @@ +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + +import { LoaderButton } from '@/components/custom/LoaderButton' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +interface DataTablePaginationProps { + table: Table +} + +export function DataTablePagination({ + table, +}: DataTablePaginationProps) { + return ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} of{' '} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of{' '} + {table.getPageCount()} +
+
+ table.setPageIndex(0)} + disabled={!table.getCanPreviousPage()} + > + Go to first page + + + table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + Go to previous page + + + table.nextPage()} + disabled={!table.getCanNextPage()} + > + Go to next page + + + table.setPageIndex(table.getPageCount() - 1)} + disabled={!table.getCanNextPage()} + > + Go to last page + + +
+
+
+ ) +} diff --git a/src/components/organisms/nodes-datatable/data-table-row-actions.tsx b/src/components/organisms/nodes-datatable/data-table-row-actions.tsx new file mode 100644 index 0000000..f10f08f --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-row-actions.tsx @@ -0,0 +1,67 @@ +import { DotsHorizontalIcon } from '@radix-ui/react-icons' +import { Row } from '@tanstack/react-table' + +import { LoaderButton } from '@/components/custom/LoaderButton' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' + +import { labels } from './data/data' +import { taskSchema } from './data/schema' + +interface DataTableRowActionsProps { + row: Row +} + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + const task = taskSchema.parse(row.original) + + return ( + + + + + Open menu + + + + Edit + Make a copy + Favorite + + + Labels + + + {labels.map((label) => ( + + {label.label} + + ))} + + + + + + Delete + ⌘⌫ + + + + ) +} diff --git a/src/components/organisms/nodes-datatable/data-table-toolbar.tsx b/src/components/organisms/nodes-datatable/data-table-toolbar.tsx new file mode 100644 index 0000000..cb8b22f --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-toolbar.tsx @@ -0,0 +1,62 @@ +import { Cross2Icon } from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + + +import { Input } from '@/components/ui/input' +import { DataTableViewOptions } from './data-table-view-options' + +import { priorities, statuses } from './data/data' +import { DataTableFacetedFilter } from './data-table-faceted-filter' +import { Button } from '@/components/ui/button' + +interface DataTableToolbarProps { + table: Table +} + +export function DataTableToolbar({ + table, +}: DataTableToolbarProps) { + const isFiltered = table.getState().columnFilters.length > 0 + + return ( +
+
+ + table.getColumn('title')?.setFilterValue(event.target.value) + } + className='h-8 w-[150px] lg:w-[250px]' + /> +
+ {table.getColumn('status') && ( + + )} + {table.getColumn('priority') && ( + + )} +
+ {isFiltered && ( + + )} +
+ +
+ ) +} diff --git a/src/components/organisms/nodes-datatable/data-table-view-options.tsx b/src/components/organisms/nodes-datatable/data-table-view-options.tsx new file mode 100644 index 0000000..eb489d0 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table-view-options.tsx @@ -0,0 +1,57 @@ +import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu' +import { MixerHorizontalIcon } from '@radix-ui/react-icons' +import { Table } from '@tanstack/react-table' + +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, +} from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' + +interface DataTableViewOptionsProps { + table: Table +} + +export function DataTableViewOptions({ + table, +}: DataTableViewOptionsProps) { + return ( + + + + + + Toggle columns + + {table + .getAllColumns() + .filter( + (column) => + typeof column.accessorFn !== 'undefined' && column.getCanHide() + ) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} + + + ) +} diff --git a/src/components/organisms/nodes-datatable/data-table.tsx b/src/components/organisms/nodes-datatable/data-table.tsx new file mode 100644 index 0000000..6f5a875 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data-table.tsx @@ -0,0 +1,126 @@ +"use client"; + +import * as React from 'react' +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' + +import { DataTablePagination } from './data-table-pagination' +import { DataTableToolbar } from './data-table-toolbar' + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = + React.useState({}) + const [columnFilters, setColumnFilters] = React.useState( + [] + ) + const [sorting, setSorting] = React.useState([]) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + columnVisibility, + rowSelection, + columnFilters, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + }) + + return ( +
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ +
+ ) +} diff --git a/src/components/organisms/nodes-datatable/data/data.tsx b/src/components/organisms/nodes-datatable/data/data.tsx new file mode 100644 index 0000000..8ba5130 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data/data.tsx @@ -0,0 +1,71 @@ +import { + ArrowDownIcon, + ArrowRightIcon, + ArrowUpIcon, + CheckCircledIcon, + CircleIcon, + CrossCircledIcon, + QuestionMarkCircledIcon, + StopwatchIcon, +} from '@radix-ui/react-icons' + +export const labels = [ + { + value: 'bug', + label: 'Bug', + }, + { + value: 'feature', + label: 'Feature', + }, + { + value: 'documentation', + label: 'Documentation', + }, +] + +export const statuses = [ + { + value: 'backlog', + label: 'Backlog', + icon: QuestionMarkCircledIcon, + }, + { + value: 'todo', + label: 'Todo', + icon: CircleIcon, + }, + { + value: 'in progress', + label: 'In Progress', + icon: StopwatchIcon, + }, + { + value: 'done', + label: 'Done', + icon: CheckCircledIcon, + }, + { + value: 'canceled', + label: 'Canceled', + icon: CrossCircledIcon, + }, +] + +export const priorities = [ + { + label: 'Low', + value: 'low', + icon: ArrowDownIcon, + }, + { + label: 'Medium', + value: 'medium', + icon: ArrowRightIcon, + }, + { + label: 'High', + value: 'high', + icon: ArrowUpIcon, + }, +] diff --git a/src/components/organisms/nodes-datatable/data/schema.ts b/src/components/organisms/nodes-datatable/data/schema.ts new file mode 100644 index 0000000..deacb53 --- /dev/null +++ b/src/components/organisms/nodes-datatable/data/schema.ts @@ -0,0 +1,13 @@ +import { z } from 'zod' + +// We're keeping a simple non-relational schema here. +// IRL, you will have a schema for your data models. +export const taskSchema = z.object({ + id: z.string(), + title: z.string(), + status: z.string(), + label: z.string(), + priority: z.string(), +}) + +export type Task = z.infer diff --git a/src/components/organisms/nodes-datatable/data/tasks.ts b/src/components/organisms/nodes-datatable/data/tasks.ts new file mode 100644 index 0000000..fc6f3ba --- /dev/null +++ b/src/components/organisms/nodes-datatable/data/tasks.ts @@ -0,0 +1,782 @@ +export const tasks = [ + { + id: 'TASK-8782', + title: + "You can't compress the program without quantifying the open-source SSD pixel!", + status: 'in progress', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-7878', + title: + 'Try to calculate the EXE feed, maybe it will index the multi-byte pixel!', + status: 'backlog', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-7839', + title: 'We need to bypass the neural TCP card!', + status: 'todo', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-5562', + title: + 'The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!', + status: 'backlog', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-8686', + title: + "I'll parse the wireless SSL protocol, that should driver the API panel!", + status: 'canceled', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-1280', + title: + 'Use the digital TLS panel, then you can transmit the haptic system!', + status: 'done', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-7262', + title: + 'The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!', + status: 'done', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-1138', + title: + "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!", + status: 'in progress', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-7184', + title: 'We need to program the back-end THX pixel!', + status: 'todo', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-5160', + title: + "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!", + status: 'in progress', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-5618', + title: + "Generating the driver won't do anything, we need to index the online SSL application!", + status: 'done', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-6699', + title: + "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!", + status: 'backlog', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-2858', + title: 'We need to override the online UDP bus!', + status: 'backlog', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-9864', + title: + "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!", + status: 'done', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-8404', + title: 'We need to generate the virtual HEX alarm!', + status: 'in progress', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-5365', + title: + "Backing up the pixel won't do anything, we need to transmit the primary IB array!", + status: 'in progress', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-1780', + title: + 'The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!', + status: 'todo', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-6938', + title: + 'Use the redundant SCSI application, then you can hack the optical alarm!', + status: 'todo', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-9885', + title: 'We need to compress the auxiliary VGA driver!', + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-3216', + title: + "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!", + status: 'backlog', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-9285', + title: + 'The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!', + status: 'todo', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-1024', + title: + "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!", + status: 'in progress', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-7068', + title: + "You can't generate the capacitor without indexing the wireless HEX pixel!", + status: 'canceled', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-6502', + title: + "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!", + status: 'todo', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-5326', + title: 'We need to hack the redundant UTF8 transmitter!', + status: 'todo', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-6274', + title: + 'Use the virtual PCI circuit, then you can parse the bluetooth alarm!', + status: 'canceled', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-1571', + title: + "I'll input the neural DRAM circuit, that should protocol the SMTP interface!", + status: 'in progress', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-9518', + title: + "Compressing the interface won't do anything, we need to compress the online SDD matrix!", + status: 'canceled', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-5581', + title: + "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!", + status: 'backlog', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-2197', + title: + "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!", + status: 'todo', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-8484', + title: 'We need to parse the solid state UDP firewall!', + status: 'in progress', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-9892', + title: + 'If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!', + status: 'done', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-9616', + title: 'We need to synthesize the cross-platform ASCII pixel!', + status: 'in progress', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-9744', + title: + 'Use the back-end IP card, then you can input the solid state hard drive!', + status: 'done', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-1376', + title: + "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!", + status: 'backlog', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-7382', + title: + 'If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!', + status: 'todo', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-2290', + title: + "I'll compress the virtual JSON panel, that should application the UTF8 bus!", + status: 'canceled', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-1533', + title: + "You can't input the firewall without overriding the wireless TCP firewall!", + status: 'done', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-4920', + title: + "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!", + status: 'in progress', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-5168', + title: + 'If we synthesize the bus, we can get to the IP panel through the virtual TLS array!', + status: 'in progress', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-7103', + title: 'We need to parse the multi-byte EXE bandwidth!', + status: 'canceled', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-4314', + title: + 'If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!', + status: 'in progress', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-3415', + title: + 'Use the cross-platform XML application, then you can quantify the solid state feed!', + status: 'todo', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-8339', + title: + 'Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!', + status: 'in progress', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-6995', + title: + 'Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!', + status: 'todo', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-8053', + title: + 'If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!', + status: 'todo', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-4336', + title: + 'If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!', + status: 'todo', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-8790', + title: + "I'll back up the optical COM alarm, that should alarm the RSS capacitor!", + status: 'done', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-8980', + title: + 'Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!', + status: 'canceled', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-7342', + title: 'Use the neural CLI card, then you can parse the online port!', + status: 'backlog', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-5608', + title: + "I'll hack the haptic SSL program, that should bus the UDP transmitter!", + status: 'canceled', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-1606', + title: + "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!", + status: 'done', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-7872', + title: + "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!", + status: 'canceled', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-4167', + title: + 'Use the cross-platform SMS circuit, then you can synthesize the optical feed!', + status: 'canceled', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-9581', + title: + "You can't index the port without hacking the cross-platform XSS monitor!", + status: 'backlog', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-8806', + title: 'We need to bypass the back-end SSL panel!', + status: 'done', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-6542', + title: + 'Try to quantify the RSS firewall, maybe it will quantify the open-source system!', + status: 'done', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-6806', + title: + 'The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!', + status: 'canceled', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-9549', + title: "You can't bypass the bus without connecting the neural JBOD bus!", + status: 'todo', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-1075', + title: + "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!", + status: 'done', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-1427', + title: + 'Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!', + status: 'done', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-1907', + title: + "Hacking the circuit won't do anything, we need to back up the online DRAM system!", + status: 'todo', + label: 'documentation', + priority: 'high', + }, + { + id: 'TASK-4309', + title: + 'If we generate the system, we can get to the TCP sensor through the optical GB pixel!', + status: 'backlog', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-3973', + title: + "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!", + status: 'todo', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-7962', + title: + 'Use the wireless RAM program, then you can hack the cross-platform feed!', + status: 'canceled', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-3360', + title: + "You can't quantify the program without synthesizing the neural OCR interface!", + status: 'done', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-9887', + title: + 'Use the auxiliary ASCII sensor, then you can connect the solid state port!', + status: 'backlog', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-3649', + title: + "I'll input the virtual USB system, that should circuit the DNS monitor!", + status: 'in progress', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-3586', + title: + 'If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!', + status: 'in progress', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-5150', + title: + "I'll hack the wireless XSS port, that should transmitter the IP interface!", + status: 'canceled', + label: 'feature', + priority: 'medium', + }, + { + id: 'TASK-3652', + title: + 'The SQL interface is down, override the optical bus so we can program the ASCII interface!', + status: 'backlog', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-6884', + title: + 'Use the digital PCI circuit, then you can synthesize the multi-byte microchip!', + status: 'canceled', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-1591', + title: 'We need to connect the mobile XSS driver!', + status: 'in progress', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-3802', + title: + 'Try to override the ASCII protocol, maybe it will parse the virtual matrix!', + status: 'in progress', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-7253', + title: + "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!", + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-9739', + title: 'We need to hack the multi-byte HDD bus!', + status: 'done', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-4424', + title: + 'Try to hack the HEX alarm, maybe it will connect the optical pixel!', + status: 'in progress', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-3922', + title: + "You can't back up the capacitor without generating the wireless PCI program!", + status: 'backlog', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-4921', + title: + "I'll index the open-source IP feed, that should system the GB application!", + status: 'canceled', + label: 'bug', + priority: 'low', + }, + { + id: 'TASK-5814', + title: 'We need to calculate the 1080p AGP feed!', + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-2645', + title: + "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!", + status: 'todo', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-4535', + title: + 'Try to copy the JSON circuit, maybe it will connect the wireless feed!', + status: 'in progress', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-4463', + title: 'We need to copy the solid state AGP monitor!', + status: 'done', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-9745', + title: + 'If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!', + status: 'canceled', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-2080', + title: + 'If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!', + status: 'todo', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-3838', + title: + "I'll bypass the online TCP application, that should panel the AGP system!", + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-1340', + title: 'We need to navigate the virtual PNG circuit!', + status: 'todo', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-6665', + title: + 'If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!', + status: 'canceled', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-7585', + title: + 'If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!', + status: 'backlog', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-6319', + title: 'We need to copy the multi-byte SCSI program!', + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-4369', + title: 'Try to input the SCSI bus, maybe it will generate the 1080p pixel!', + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-9035', + title: 'We need to override the solid state PNG array!', + status: 'canceled', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-3970', + title: + "You can't index the transmitter without quantifying the haptic ASCII card!", + status: 'todo', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-4473', + title: + "You can't bypass the protocol without overriding the neural RSS program!", + status: 'todo', + label: 'documentation', + priority: 'low', + }, + { + id: 'TASK-4136', + title: + "You can't hack the hard drive without hacking the primary JSON program!", + status: 'canceled', + label: 'bug', + priority: 'medium', + }, + { + id: 'TASK-3939', + title: + 'Use the back-end SQL firewall, then you can connect the neural hard drive!', + status: 'done', + label: 'feature', + priority: 'low', + }, + { + id: 'TASK-2007', + title: + "I'll input the back-end USB protocol, that should bandwidth the PCI system!", + status: 'backlog', + label: 'bug', + priority: 'high', + }, + { + id: 'TASK-7516', + title: + 'Use the primary SQL program, then you can generate the auxiliary transmitter!', + status: 'done', + label: 'documentation', + priority: 'medium', + }, + { + id: 'TASK-6906', + title: + 'Try to back up the DRAM system, maybe it will reboot the online transmitter!', + status: 'done', + label: 'feature', + priority: 'high', + }, + { + id: 'TASK-5207', + title: + 'The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!', + status: 'in progress', + label: 'bug', + priority: 'low', + }, +] diff --git a/src/data/navlinks.tsx b/src/data/navlinks.tsx index 55cbba3..6d1cdfa 100644 --- a/src/data/navlinks.tsx +++ b/src/data/navlinks.tsx @@ -52,13 +52,13 @@ export const sidelinks: SideLink[] = [ ], }, { - title: 'Community', + title: 'Desci Communities', label: '', href: '/community', icon: , sub: [ { - title: 'Communities', + title: 'Desci Communities', label: '', href: '/community', icon: , diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts new file mode 100644 index 0000000..0774fb5 --- /dev/null +++ b/src/lib/api/index.ts @@ -0,0 +1,48 @@ +import { queryOptions } from "@tanstack/react-query"; +import { tags } from "../tags"; +import { NODES_API_URL } from "../config"; + +type Community = { + id: number; + createdAt: string; + updatedAt: string; + name: string; + slug: string; + image_url: string; + subtitle: string; + description: string; + keywords: string[]; + memberString: string[]; + links: string[]; + hidden: boolean; + CommunityMember: { + id: number; + role: "ADMIN" | "MEMBER"; + userId: number; + user: { + name: string; + userOrganizations: { id: string; name: string }[]; + }; + }[]; + engagements: { + reactions: number; + annotations: number; + verifications: number; + }; + verifiedEngagements: { + reactions: number; + annotations: number; + verifications: number; + }; +}; + +export const listCommunitiesQuery = queryOptions({ + queryKey: [tags.communities], + queryFn: async () => { + const response = await fetch(`${NODES_API_URL}/v1/admin/communities`, { + credentials: "include", + }); + console.log('fetch list', response.ok); + return (await response.json()) as { data: Community[] }; + }, +}); diff --git a/src/lib/get-query-client.ts b/src/lib/get-query-client.ts new file mode 100644 index 0000000..b599f62 --- /dev/null +++ b/src/lib/get-query-client.ts @@ -0,0 +1,40 @@ +import { + defaultShouldDehydrateQuery, + isServer, + QueryClient, +} from "@tanstack/react-query"; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + dehydrate: { + // include pending queries in dehydration + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + }, + }); + } + + let browserQueryClient: QueryClient | undefined = undefined; + +export function getQueryClient() { + if (isServer) { + // Server: always make a new query client + return makeQueryClient(); + } else { + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; + } + } + \ No newline at end of file diff --git a/src/lib/tags.ts b/src/lib/tags.ts new file mode 100644 index 0000000..2c6a895 --- /dev/null +++ b/src/lib/tags.ts @@ -0,0 +1,3 @@ +export enum tags { + communities = 'communities' +} \ No newline at end of file diff --git a/tailwind.config.ts b/tailwind.config.ts index 9f0afc3..165b10d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -61,6 +61,18 @@ const config: Config = { "4": "hsl(var(--chart-4))", "5": "hsl(var(--chart-5))", }, + warning: { + DEFAULT: "hsl(var(--warning))", + foreground: "hsl(var(--warning-foreground))", + }, + danger: { + DEFAULT: "hsl(var(--danger))", + foreground: "hsl(var(--danger-foreground))", + }, + success: { + DEFAULT: "hsl(var(--success))", + foreground: "hsl(var(--success-foreground))", + }, slate: generateScale("slate"), cyan: generateScale("cyan"), @@ -109,7 +121,7 @@ const config: Config = { "btn-surface-secondary-focus": "var(--btn-surface-secondary-focus)", error: "#AF372E", - success: "#17251B", + // success: "#17251B", "space-green": "rgba(36, 131, 123, 0.15)", "space-cadet": "#1D3149", }, @@ -136,13 +148,13 @@ const config: Config = { export default config; function generateScale(name: string) { - let scale = Array.from({ length: 12 }, (_, i) => { - let id = i + 1; - return [ - [id, `var(--${name}-${id})`], - [`a${id}`, `var(--${name}-a${id})`], - ]; - }).flat(); + let scale = Array.from({ length: 12 }, (_, i) => { + let id = i + 1; + return [ + [id, `var(--${name}-${id})`], + [`a${id}`, `var(--${name}-a${id})`], + ]; + }).flat(); - return Object.fromEntries(scale); + return Object.fromEntries(scale); }