diff --git a/package.json b/package.json index 95b96e4..18551ef 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "i18next-resources-for-ts": "i18next-resources-for-ts toc -i ./src/i18n/locales/en -o ./src/types/resources.ts" }, "dependencies": { + "@faker-js/faker": "^9.2.0", "@hookform/resolvers": "^3.9.0", "@loadable/component": "^5.15.3", "@radix-ui/react-avatar": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e755f7b..05e2086 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: importers: .: dependencies: + '@faker-js/faker': + specifier: ^9.2.0 + version: 9.2.0 '@hookform/resolvers': specifier: ^3.9.0 version: 3.9.0(react-hook-form@7.52.1(react@18.3.1)) @@ -1524,6 +1527,11 @@ packages: { integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA== } engines: { node: '>=14' } + '@faker-js/faker@9.2.0': + resolution: + { integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg== } + engines: { node: '>=18.0.0', npm: '>=9.0.0' } + '@floating-ui/core@1.6.5': resolution: { integrity: sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA== } @@ -10331,6 +10339,8 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 + '@faker-js/faker@9.2.0': {} + '@floating-ui/core@1.6.5': dependencies: '@floating-ui/utils': 0.2.5 diff --git a/public/tasks.json b/public/tasks.json new file mode 100644 index 0000000..6335dfd --- /dev/null +++ b/public/tasks.json @@ -0,0 +1,703 @@ +[ + { + "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", + "priority23": "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/components/icons.tsx b/src/components/icons.tsx index 4cb5226..8904216 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -14,6 +14,7 @@ import { Settings, GitPullRequest, ClipboardList, + CalendarRange, } from 'lucide-react' export const Icons = { @@ -29,6 +30,7 @@ export const Icons = { tutorial: BookOpen, school: LucideSchool, settings: Settings, + period: CalendarRange, clipboardList: ClipboardList, gitPullRequest: GitPullRequest, github: ({ ...props }: LucideProps) => ( diff --git a/src/components/import-project.tsx b/src/components/import-project.tsx index 8e248e2..e05a409 100644 --- a/src/components/import-project.tsx +++ b/src/components/import-project.tsx @@ -121,6 +121,7 @@ export default function ImportProject() { queryFn: getMyInfo, enabled: !!open, }) + const { data: userOrOrgOptions, isLoading: isUserOrOrgOptionsLoading } = useQuery({ queryKey: ['userOrOrgOptions'], queryFn: async () => { diff --git a/src/components/shared/column/columns.tsx b/src/components/shared/column/columns.tsx new file mode 100644 index 0000000..b543430 --- /dev/null +++ b/src/components/shared/column/columns.tsx @@ -0,0 +1,99 @@ +import { ColumnDef } from '@tanstack/react-table' + +import { labels, priorities, statuses } from '../data/data' +import { Task } from '../data/schema' +import { DataTableColumnHeader } from './data-table-column-header' +import { DataTableRowActions } from './data-table-row-actions' +import { Checkbox } from '@/components/ui/checkbox' +import { Badge } from '@/components/ui/badge' + +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/shared/column/data-table-column-header.tsx b/src/components/shared/column/data-table-column-header.tsx new file mode 100644 index 0000000..c74168e --- /dev/null +++ b/src/components/shared/column/data-table-column-header.tsx @@ -0,0 +1,60 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { cn } from '@/lib/utils' +import { Column } from '@tanstack/react-table' +import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react' + +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/shared/column/data-table-faceted-filter.tsx b/src/components/shared/column/data-table-faceted-filter.tsx new file mode 100644 index 0000000..9fca62c --- /dev/null +++ b/src/components/shared/column/data-table-faceted-filter.tsx @@ -0,0 +1,127 @@ +import * as React from 'react' +import { Column } from '@tanstack/react-table' + +import { cn } from '@/lib/utils' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { Button } from '@/components/ui/button' +import { Separator } from '@/components/ui/separator' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from '@/components/ui/command' +import { Badge } from '@/components/ui/badge' +import { PlusCircle, Check } from 'lucide-react' + +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 ( + + + + + + + + + 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/shared/column/data-table-pagination.tsx b/src/components/shared/column/data-table-pagination.tsx new file mode 100644 index 0000000..9184b35 --- /dev/null +++ b/src/components/shared/column/data-table-pagination.tsx @@ -0,0 +1,81 @@ +import { Button } from '@/components/ui/button' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Table } from '@tanstack/react-table' +import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react' + +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()} +
+
+ + + + +
+
+
+ ) +} diff --git a/src/components/shared/column/data-table-row-actions.tsx b/src/components/shared/column/data-table-row-actions.tsx new file mode 100644 index 0000000..9dfd235 --- /dev/null +++ b/src/components/shared/column/data-table-row-actions.tsx @@ -0,0 +1,61 @@ +import { Row } from '@tanstack/react-table' +import { MoreHorizontal } from 'lucide-react' + +import { labels } from '../data/data' +import { taskSchema } from '../data/schema' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' + +interface DataTableRowActionsProps { + row: Row +} + +export function DataTableRowActions({ row }: DataTableRowActionsProps) { + const task = taskSchema.parse(row.original) + + return ( + + + + + + Edit + Make a copy + Favorite + + + Labels + + + {labels.map((label) => ( + + {label.label} + + ))} + + + + + + Delete + ⌘⌫ + + + + ) +} diff --git a/src/components/shared/column/data-table-toolbar.tsx b/src/components/shared/column/data-table-toolbar.tsx new file mode 100644 index 0000000..5c66952 --- /dev/null +++ b/src/components/shared/column/data-table-toolbar.tsx @@ -0,0 +1,42 @@ +import { Table } from '@tanstack/react-table' +import { X } from 'lucide-react' + +import { priorities, statuses } from '../data/data' +import { DataTableFacetedFilter } from './data-table-faceted-filter' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { DataTableViewOptions } from './data-table-view-options' + +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/shared/column/data-table-view-options.tsx b/src/components/shared/column/data-table-view-options.tsx new file mode 100644 index 0000000..21ea83e --- /dev/null +++ b/src/components/shared/column/data-table-view-options.tsx @@ -0,0 +1,47 @@ +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Table } from '@tanstack/react-table' +import { Settings2 } from 'lucide-react' + +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/shared/column/data-table.tsx b/src/components/shared/column/data-table.tsx new file mode 100644 index 0000000..fe8594e --- /dev/null +++ b/src/components/shared/column/data-table.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table' +import { DataTablePagination } from './data-table-pagination' +import { DataTableToolbar } from './data-table-toolbar' +import { TableHeader, TableRow, TableHead, TableBody, TableCell, Table } from '@/components/ui/table' + +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/shared/data/data.tsx b/src/components/shared/data/data.tsx new file mode 100644 index 0000000..cd69783 --- /dev/null +++ b/src/components/shared/data/data.tsx @@ -0,0 +1,62 @@ +import { HelpCircle, Circle, Timer, CheckCircle, CircleOff, ArrowDown, ArrowRight, ArrowUp } from 'lucide-react' + +export const labels = [ + { + value: 'bug', + label: 'Bug', + }, + { + value: 'feature', + label: 'Feature', + }, + { + value: 'documentation', + label: 'Documentation', + }, +] + +export const statuses = [ + { + value: 'backlog', + label: 'Backlog', + icon: HelpCircle, + }, + { + value: 'todo', + label: 'Todo', + icon: Circle, + }, + { + value: 'in progress', + label: 'In Progress', + icon: Timer, + }, + { + value: 'done', + label: 'Done', + icon: CheckCircle, + }, + { + value: 'canceled', + label: 'Canceled', + icon: CircleOff, + }, +] + +export const priorities = [ + { + label: 'Low', + value: 'low', + icon: ArrowDown, + }, + { + label: 'Medium', + value: 'medium', + icon: ArrowRight, + }, + { + label: 'High', + value: 'high', + icon: ArrowUp, + }, +] diff --git a/src/components/shared/data/schema.ts b/src/components/shared/data/schema.ts new file mode 100644 index 0000000..1c802a3 --- /dev/null +++ b/src/components/shared/data/schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +export const taskSchema = z.object({ + id: z.string(), + title: z.string(), + status: z.string(), + label: z.string(), + priority: z.string(), + priority23: z.string().optional(), +}) + +export type Task = z.infer diff --git a/src/components/shared/data/seed.ts b/src/components/shared/data/seed.ts new file mode 100644 index 0000000..115b754 --- /dev/null +++ b/src/components/shared/data/seed.ts @@ -0,0 +1,16 @@ +import path from 'path' +import fs from 'fs' +import { statuses, labels, priorities } from './data' +import { faker } from '@faker-js/faker' + +const tasks = Array.from({ length: 100 }, () => ({ + id: `TASK-${faker.number.int({ min: 1000, max: 9999 })}`, + title: faker.hacker.phrase().replace(/^./, (letter) => letter.toUpperCase()), + status: faker.helpers.arrayElement(statuses).value, + label: faker.helpers.arrayElement(labels).value, + priority: faker.helpers.arrayElement(priorities).value, +})) + +fs.writeFileSync(path.join(__dirname, 'tasks.json'), JSON.stringify(tasks, null, 2)) + +console.log('✅ Tasks data generated.') diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..043c4ff --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' +import * as SeparatorPrimitive from '@radix-ui/react-separator' + +import { cn } from '@/lib/utils' + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => ( + +)) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/constants/data.ts b/src/constants/data.ts index 4f7fe50..b6e4736 100644 --- a/src/constants/data.ts +++ b/src/constants/data.ts @@ -101,6 +101,14 @@ export const getNavItems = (userPermission?: UserPermission): NavItem[] => { layout: 'dashboard', hideInMenu: userPermission !== UserPermission.PullRequest && userPermission !== UserPermission.All, }, + { + title: 'Period', + href: '/admin/period', + icon: 'period', + component: 'periodAdmin', + layout: 'dashboard', + hideInMenu: userPermission !== UserPermission.PullRequest && userPermission !== UserPermission.All, + }, { title: 'Task Apply', href: '/admin/task-apply', diff --git a/src/pages/period/index.tsx b/src/pages/period/index.tsx new file mode 100644 index 0000000..713d8eb --- /dev/null +++ b/src/pages/period/index.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useState } from 'react' +import { fetchPeriod, getLoadMoreProjectList } from '@/service' +import { IResultPagination, IResultPaginationData, Project, Record } from '@/types' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { LoadingCards } from '@/components/loading-cards' +import { useAccount } from 'wagmi' +import { useInfiniteScroll } from 'ahooks' +import { DEFAULT_PAGINATION_LIMIT } from '@/constants/data' +import PaginationFast from '@/components/pagination-fast' +import { useQuery } from '@tanstack/react-query' +import { useSearchParams } from 'react-router-dom' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { PencilLine } from 'lucide-react' +import { RewardDialogForm } from './reward-form' +import { Button } from '@/components/ui/button' + +interface ProjectListProps { + loading: boolean + loadingMore: boolean + data: IResultPagination | undefined +} + +function ProjectList({ loading, loadingMore, data }: ProjectListProps) { + if (loading) return + if (!data) return null + + return ( +
+
+
{data.pagination.totalCount} Projects
+
+
+ {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {data.list.map((project) => ( + + {project.name} + + ))} + {loadingMore && } +
+
+ ) +} + +function PeriodTable(): React.ReactElement { + const [page, setPage] = useState(1) + const [urlParam, setUrlParam] = useSearchParams('') + const [projectId, setProjectId] = useState(undefined) + const [filterTags, setFilterTags] = useState([]) + const { address, chain } = useAccount() + const pageSize = 10 + + const { + data: projects, + loading: projectLoading, + loadingMore: projectLoadingMore, + reload, + } = useInfiniteScroll>( + async (d) => { + const res = await getLoadMoreProjectList({ + offset: d ? d.pagination.currentPage * DEFAULT_PAGINATION_LIMIT : 0, + limit: DEFAULT_PAGINATION_LIMIT, + filterTags, + search: decodeURIComponent(urlParam.get('search') || ''), + sort: decodeURIComponent(urlParam.get('sort') || ''), + onlyPeriodicReward: true, + }) + if (!projectId) setProjectId(res.list[0]._id.toString()) + return res + }, + { + manual: true, + target: document.querySelector('#scrollRef'), + isNoMore: (data) => { + return data ? !data.pagination.hasNextPage : false + }, + }, + ) + + const { data: aggregations, isLoading: isPullRequestsLoading } = useQuery | undefined>({ + queryKey: ['aggregations', projectId ?? ''], + queryFn: () => { + return fetchPeriod({ + offset: (page - 1) * pageSize, + limit: pageSize, + projectId: projectId ?? '', + }) + }, + }) + + const totalPages = Math.ceil((aggregations?.pagination.totalCount || 0) / pageSize) + + useEffect(() => { + reload() + }, [filterTags, reload, urlParam]) + + return ( +
+ + + {!isPullRequestsLoading && aggregations ? ( + + + + From + To + Users + Pr count + Reward + + + + {aggregations.data.map((periodPrs) => { + return ( + + {periodPrs.from} + {periodPrs.to} + +
+ {periodPrs.users.map((user) => { + return ( + {user._id} + ) + })} +
+
+ {periodPrs.pullRequests.length} + + {!periodPrs.rewardGranted ? ( + address && + chain && ( + + + Reward + + } + id={periodPrs._id} + users={periodPrs.users} + addressFrom={address} + chain={chain} + /> + ) + ) : ( +

Rewarded

+ )} +
+
+ ) + })} +
+
+ ) : ( + + )} + + +
+ ) +} + +export default function PeriodAdmin() { + return +} diff --git a/src/pages/period/reward-form.tsx b/src/pages/period/reward-form.tsx new file mode 100644 index 0000000..c114581 --- /dev/null +++ b/src/pages/period/reward-form.tsx @@ -0,0 +1,251 @@ +import { z } from 'zod' +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import { useConfig, useSendTransaction, useSwitchChain, useWriteContract } from 'wagmi' +import { Chain } from 'viem' +import { useToast } from '@/components/ui/use-toast' +import React, { useState } from 'react' +import { Button } from '@/components/ui/button' +import { DialogContent, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Dialog } from '@radix-ui/react-dialog' +import { postGrantAggregationRewards } from '@/service' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Label } from '@/components/ui/label' +import { PAYMENT_USDT_ADDRESS, USDT_ABI, USDT_DECIMAL, USDT_SYMBOL } from '@/constants/contracts/usdt' +import { paymentChain } from '@/constants/data' +import { User } from '@/types' + +import _ from 'lodash' + +function randomDistribute(amount: number, people: number): number[] { + const points = _.sortBy(_.times(people - 1, () => Math.random())) + points.push(1) + points.unshift(0) + + const amounts = points + .slice(1) + .map((point, index) => point - points[index]) + .map((fraction) => _.round(amount * fraction, 2)) + + const diff = _.round(amount - _.sum(amounts), 2) + amounts[amounts.length - 1] += diff + + return amounts +} + +const formSchema = z.object({ + amount: z.string().regex(/^(0|[1-9]\d*)(\.\d+)?$/, { + message: 'Amount must be a positive number', + }), + coin: z.string().min(1, { message: 'Coin is required' }), +}) + +interface IRewardForm { + trigger: React.ReactNode + id: string + addressFrom: `0x${string}` + users: User[] + chain: Chain +} + +export const RewardDialogForm = ({ trigger, id, users, addressFrom, chain }: IRewardForm) => { + const { toast } = useToast() + const { sendTransactionAsync } = useSendTransaction() + const [loading, setLoading] = useState(false) + const [isOpen, setOpen] = useState(false) + const { switchChain } = useSwitchChain() + const { writeContractAsync } = useWriteContract() + const { chains } = useConfig() + const [amounts, setAmounts] = useState(Array(users.length).fill(0.0)) + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + coin: USDT_SYMBOL, + }, + }) + + const onOpenChange = (isOpen: boolean) => { + if (!isOpen) { + switchChain({ chainId: paymentChain.id }) + } + + setOpen(isOpen) + } + + const onSubmit = async (values: z.infer) => { + setLoading(true) + try { + let transactions: `0x${string}`[] = [] + if (values.coin !== USDT_SYMBOL) { + transactions = await Promise.all( + users.map( + async (user, index) => + await sendTransactionAsync({ + to: user.wallet?.address as `0x${string}`, + value: BigInt(amounts[index]), + }), + ), + ) + } else { + transactions = await Promise.all( + users.map( + async (user, index) => + await writeContractAsync({ + abi: USDT_ABI, + address: PAYMENT_USDT_ADDRESS, + functionName: 'transfer', + args: [user.wallet?.address, amounts[index] * Math.pow(10, USDT_DECIMAL)], + }), + ), + ) + } + + await postGrantAggregationRewards({ id }) + + setOpen(false) + + toast({ + variant: 'default', + title: 'Success', + description: 'Success reward', + }) + } catch (error) { + if (error instanceof Error) { + toast({ + variant: 'destructive', + title: 'Error', + description: error.name, + }) + } else { + toast({ + variant: 'destructive', + title: 'Error', + description: 'An unknown error occurred', + }) + } + } finally { + setLoading(false) + } + } + + return ( + + {trigger} + + +
+ {!loading ? ( + <> + +

Reward

+
+ +
+
+ ( + +
+ Amount + + { + const newValue = e.target.value + field.onChange(newValue) + setAmounts(randomDistribute(Number(newValue), users.length)) + }} + value={field.value || ''} + /> + +
+ +
+ )} + /> + ( + + + + + )} + /> +
+
+ {users.map((user, index) => { + return ( +
+ {user._id} + {user?.wallet?.address} + {amounts[index]} +
+ ) + })} +
+ + + + + +
+ + + +
+ + ) : ( +
+
+
+ )} + + +
+ ) +} diff --git a/src/pages/pull-request/index.tsx b/src/pages/pull-request/index.tsx index 1ed1fe6..edf8ab0 100644 --- a/src/pages/pull-request/index.tsx +++ b/src/pages/pull-request/index.tsx @@ -14,7 +14,6 @@ import { useAccount } from 'wagmi' import { useSearchParams } from 'react-router-dom' import { SearchInput } from '@/components/search' -// TODO: reward feature function PullRequestsTable(): React.ReactElement { const [page, setPage] = useState(1) const [urlParam, setUrlParam] = useSearchParams('') diff --git a/src/pages/task/index.tsx b/src/pages/task/index.tsx index b001d4d..ee229b3 100644 --- a/src/pages/task/index.tsx +++ b/src/pages/task/index.tsx @@ -10,10 +10,29 @@ import { import { Input } from '@/components/ui/input' import { LucideSearch } from 'lucide-react' import { TaskCatalog } from '@/components/task' +import { DataTable } from '@/components/shared/column/data-table' +import { columns } from '@/components/shared/column/columns' +import { useEffect, useState } from 'react' +import { Task, taskSchema } from '@/components/shared/data/schema' +import { z } from 'zod' export default function TaskPage() { const { project } = useParams<{ project: string }>() + const [taskData, setTaskData] = useState([]) + + useEffect(() => { + fetch('/tasks.json') + .then(async (response) => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + return await response.json() + }) + .then((data) => setTaskData(z.array(taskSchema).parse(data))) + .catch((error) => console.error('Error loading JSON:', error)) + }, []) + return ( <> @@ -40,6 +59,7 @@ export default function TaskPage() { + ) diff --git a/src/router/pages.tsx b/src/router/pages.tsx index e791f0d..36eaec1 100644 --- a/src/router/pages.tsx +++ b/src/router/pages.tsx @@ -11,6 +11,7 @@ import TaskDetailPage from '@/pages/task-detail' import ErrorPage from '@/pages/error' import PullRequestAdmin from '@/pages/pull-request' import TaskApplyAdmin from '@/pages/task-apply' +import PeriodAdmin from '@/pages/period' export const Pages = { dashboard: Dashboard, @@ -26,4 +27,5 @@ export const Pages = { error: ErrorPage, pullRequestAdmin: PullRequestAdmin, taskApplyAdmin: TaskApplyAdmin, + periodAdmin: PeriodAdmin, } diff --git a/src/service/index.ts b/src/service/index.ts index 6afa0b5..433d6a8 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -14,23 +14,28 @@ import { PrRewardInfo, TaskState, UserInfo, + FetchPullRequestAggregationsParams, + Record, + FetchGrantAggregationRewardsParams, } from '@/types' import http from './instance' export async function getLoadMoreProjectList(params: { offset: number | undefined limit: number - filterTags: string[] - sort: string - search: string + filterTags?: string[] + sort?: string + search?: string + onlyPeriodicReward?: boolean }) { const res = await http.get>(`/projects`, { params: { - tags: params.filterTags, + tags: params.filterTags ?? '', offset: params.offset, limit: params.limit, - sort: params.sort, - search: params.search, + sort: params.sort ?? '', + search: params.search ?? '', + onlyPeriodicReward: params.onlyPeriodicReward, }, }) return { list: res.data.data, pagination: res.data.pagination } @@ -46,9 +51,9 @@ export async function fetchTask(githubId: string) { return response.data as Task } -export async function fetchProjects() { +export async function fetchProjects(): Promise { const response = await http.get('/projects?limit=1000') - return response.data.data as Project[] + return response.data.data } export async function fetchLeaderboard(): Promise<{ data: Profile[]; totalCount: number }> { @@ -124,6 +129,16 @@ export async function fetchPullRequests(params: FetchPullRequestParams) { return response.data } +export async function fetchPeriod(params: FetchPullRequestAggregationsParams) { + const response = await http.get>('/aggregations', { params }) + return response.data +} + +export async function postGrantAggregationRewards(params: FetchGrantAggregationRewardsParams) { + const response = await http.post(`/aggregations/${params.id}/grant-rewards`) + return response.data +} + export async function fetchTaskApplies(params: FetchTaskAppliesParams) { const response = await http.get>('/task-applies', { params }) return response.data diff --git a/src/types/index.ts b/src/types/index.ts index 4b7e972..dd877b6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -14,6 +14,24 @@ export interface IResultPaginationData { pagination: IPagination } +export interface Wallet { + address: string + chain: string +} + +export interface Record { + _id: string + projectId: string + from: string + to: string + pullRequests: string[] + users: User[] + rewardGranted: boolean + createdAt: string + updatedAt: string + __v: number +} + export interface IResultPagination { list: T[] pagination: IPagination @@ -95,6 +113,9 @@ export interface User { login: string htmlUrl: string avatarUrl: string + wallet?: Wallet + rewards?: number + _id?: string } export interface FetchIssuesParams { @@ -109,6 +130,14 @@ export interface FetchPullRequestParams extends PaginationParams { sort?: string } +export interface FetchPullRequestAggregationsParams extends PaginationParams { + projectId: string +} + +export interface FetchGrantAggregationRewardsParams { + id: string +} + export interface PullRequest { _id: string githubId: number