From bcf68e0a624ab77fb3476018cd0ec61c3c193bb7 Mon Sep 17 00:00:00 2001 From: Elliot Braem <16282460+elliotBraem@users.noreply.github.com> Date: Sat, 5 Oct 2024 20:31:48 -0400 Subject: [PATCH] add proposals --- src/components/ui/icons.tsx | 6 +- src/constants/data.ts | 12 + src/lib/dao/config.ts | 22 ++ src/lib/dao/members.ts | 3 +- src/lib/dao/proposals.ts | 57 +++++ src/pages/proposals/ProposalDetailPage.tsx | 142 ++++++++++++ .../member-forms/member-create-form.tsx | 215 ++++++++++++++++++ .../proposal-feed-table/cell-action.tsx | 56 +++++ .../proposal-feed-table/columns.tsx | 38 ++++ .../components/proposal-feed-table/index.tsx | 24 ++ .../member-table-action.tsx | 25 ++ .../proposals-table/cell-action.tsx | 56 +++++ .../components/proposals-table/columns.tsx | 80 +++++++ .../components/proposals-table/index.tsx | 25 ++ .../proposals-table/member-table-action.tsx | 18 ++ src/pages/proposals/index.tsx | 104 +++++++++ src/pages/proposals/queries/queries.ts | 13 ++ src/pages/settings/index.tsx | 45 ++++ src/pages/settings/queries/queries.ts | 13 ++ src/routes/index.tsx | 17 ++ 20 files changed, 969 insertions(+), 2 deletions(-) create mode 100644 src/lib/dao/config.ts create mode 100644 src/lib/dao/proposals.ts create mode 100644 src/pages/proposals/ProposalDetailPage.tsx create mode 100644 src/pages/proposals/components/member-forms/member-create-form.tsx create mode 100644 src/pages/proposals/components/proposal-feed-table/cell-action.tsx create mode 100644 src/pages/proposals/components/proposal-feed-table/columns.tsx create mode 100644 src/pages/proposals/components/proposal-feed-table/index.tsx create mode 100644 src/pages/proposals/components/proposal-feed-table/member-table-action.tsx create mode 100644 src/pages/proposals/components/proposals-table/cell-action.tsx create mode 100644 src/pages/proposals/components/proposals-table/columns.tsx create mode 100644 src/pages/proposals/components/proposals-table/index.tsx create mode 100644 src/pages/proposals/components/proposals-table/member-table-action.tsx create mode 100644 src/pages/proposals/index.tsx create mode 100644 src/pages/proposals/queries/queries.ts create mode 100644 src/pages/settings/index.tsx create mode 100644 src/pages/settings/queries/queries.ts diff --git a/src/components/ui/icons.tsx b/src/components/ui/icons.tsx index 96f0357..617b266 100644 --- a/src/components/ui/icons.tsx +++ b/src/components/ui/icons.tsx @@ -28,7 +28,9 @@ import { User, User2Icon, UserX2Icon, - X + X, + HandHelping, + Cog } from 'lucide-react'; export type Icon = LucideIcon; @@ -38,6 +40,7 @@ export const Icons = { logo: Command, login: LogIn, close: X, + cog: Cog, profile: User2Icon, spinner: Loader2, kanban: CircuitBoardIcon, @@ -60,6 +63,7 @@ export const Icons = { sun: SunMedium, moon: Moon, laptop: Laptop, + handHelping: HandHelping, gitHub: ({ ...props }: LucideProps) => (

Loading!!!

; + } + return ( +
+
+ +
+ + +
+
+
+
+ + +

Profile

+ Active +
+ + + +
+ + + About Me + + + Hello! I'm Srikkath, your dedicated admin at Kutubi, ensuring a + seamless and enriching experience for teachers, Proposals, and + parents. Feel free to reach out for any assistance or feedback + + + + + Last Login + + + 12 Aug 2022 9:30 AM + + +
+ {/* contact information */} + + + Contact Information + + + +
+
+

First Name

+

John

+
+
+

Last Name

+

Doe

+
+
+

User Name

+

John

+
+
+

Sex

+

Male

+
+
+

Position

+

Super Admin

+
+
+

Department

+

Kutubi

+
+
+

Contact Email

+

ElonMusk@x.com

+
+
+

Contact Number

+

Nil

+
+
+

City

+

Dubai

+
+
+

Language

+

English

+
+
+

Date of Birth

+

26/4/1989

+
+
+

Social Media

+

x

+
+
+
+
+
+
+ +
+ ); +} diff --git a/src/pages/proposals/components/member-forms/member-create-form.tsx b/src/pages/proposals/components/member-forms/member-create-form.tsx new file mode 100644 index 0000000..9a3559c --- /dev/null +++ b/src/pages/proposals/components/member-forms/member-create-form.tsx @@ -0,0 +1,215 @@ +import Heading from '@/components/shared/heading'; +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +const memberFormSchema = z + .object({ + firstname: z + .string({ required_error: 'First name is required' }) + .min(1, { message: 'firstname is should be at least 1 character' }), + lastname: z.string().min(1, { message: 'lastname is required' }), + username: z.string().min(1, { message: 'username is required' }), + school: z.string().min(1, { message: 'school is required' }), + email: z.string().email({ message: 'Enter a valid email address' }), + phone: z.string().min(1, { message: 'Enter a valid phone number' }), + password: z.string().min(1, { message: 'Password is required' }), + confirmPassword: z + .string() + .min(1, { message: 'Confirm Password is required' }) + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords must match', + path: ['confirmPassword'] + }); + +type MemberFormSchemaType = z.infer; + +const MemberCreateForm = ({ modalClose }: { modalClose: () => void }) => { + const form = useForm({ + resolver: zodResolver(memberFormSchema), + defaultValues: {} + }); + + const onSubmit = (values: MemberFormSchemaType) => { + // Do something with the form values. + // ✅ This will be type-safe and validated. + console.log(values); + }; + + return ( +
+ +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> +
+ +
+ + +
+
+ +
+ ); +}; + +export default MemberCreateForm; diff --git a/src/pages/proposals/components/proposal-feed-table/cell-action.tsx b/src/pages/proposals/components/proposal-feed-table/cell-action.tsx new file mode 100644 index 0000000..a1eea3a --- /dev/null +++ b/src/pages/proposals/components/proposal-feed-table/cell-action.tsx @@ -0,0 +1,56 @@ +import { AlertModal } from '@/components/shared/alert-modal'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Edit, MoreHorizontal, Trash } from 'lucide-react'; +import { useRouter } from '@/routes/hooks'; +import { useState } from 'react'; +import { Proposal } from '@/lib/dao/proposals'; + +interface CellActionProps { + data: Proposal; +} + +export const CellAction: React.FC = ({ data }) => { + const [loading] = useState(false); + const [open, setOpen] = useState(false); + const router = useRouter(); + + const onConfirm = async () => {}; + + return ( + <> + setOpen(false)} + onConfirm={onConfirm} + loading={loading} + /> + + + + + + Actions + + router.push(`/dashboard/user/${data.id}`)} + > + Update + + setOpen(true)}> + Delete + + + + + ); +}; diff --git a/src/pages/proposals/components/proposal-feed-table/columns.tsx b/src/pages/proposals/components/proposal-feed-table/columns.tsx new file mode 100644 index 0000000..e7ceb46 --- /dev/null +++ b/src/pages/proposals/components/proposal-feed-table/columns.tsx @@ -0,0 +1,38 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { ColumnDef } from '@tanstack/react-table'; +import { CellAction } from './cell-action'; +import { Member } from '@/lib/dao/members'; + +export const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false + }, + { + accessorKey: 'id', + header: 'ID' + }, + { + accessorKey: 'roles', + header: 'ROLES' + }, + { + id: 'actions', + cell: ({ row }) => + } +]; diff --git a/src/pages/proposals/components/proposal-feed-table/index.tsx b/src/pages/proposals/components/proposal-feed-table/index.tsx new file mode 100644 index 0000000..a88a1bf --- /dev/null +++ b/src/pages/proposals/components/proposal-feed-table/index.tsx @@ -0,0 +1,24 @@ +import DataTable from '@/components/shared/data-table'; +import { columns } from './columns'; +import MemberTableActions from './member-table-action'; + +type TMembersTableProps = { + users: any; + page: number; + totalUsers: number; + pageCount: number; +}; + +export default function MemberFeedTable({ + users, + pageCount +}: TMembersTableProps) { + return ( + <> + + {users && ( + + )} + + ); +} diff --git a/src/pages/proposals/components/proposal-feed-table/member-table-action.tsx b/src/pages/proposals/components/proposal-feed-table/member-table-action.tsx new file mode 100644 index 0000000..dab7f9d --- /dev/null +++ b/src/pages/proposals/components/proposal-feed-table/member-table-action.tsx @@ -0,0 +1,25 @@ +import PopupModal from '@/components/shared/popup-modal'; +import TableSearchInput from '@/components/shared/table-search-input'; +import { Button } from '@/components/ui/button'; +import MemberCreateForm from '../member-forms/member-create-form'; +import { DownloadIcon } from 'lucide-react'; + +export default function MemberTableActions() { + return ( +
+
+ +
+
+ + + } + /> +
+
+ ); +} diff --git a/src/pages/proposals/components/proposals-table/cell-action.tsx b/src/pages/proposals/components/proposals-table/cell-action.tsx new file mode 100644 index 0000000..a553f44 --- /dev/null +++ b/src/pages/proposals/components/proposals-table/cell-action.tsx @@ -0,0 +1,56 @@ +import { AlertModal } from '@/components/shared/alert-modal'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu'; +import { Proposal } from '@/lib/dao/proposals'; +import { useRouter } from '@/routes/hooks'; +import { Edit, MoreHorizontal, Trash } from 'lucide-react'; +import { useState } from 'react'; + +interface CellActionProps { + data: Proposal; +} + +export const CellAction: React.FC = ({ data }) => { + const [loading] = useState(false); + const [open, setOpen] = useState(false); + const router = useRouter(); + + const onConfirm = async () => {}; + + return ( + <> + setOpen(false)} + onConfirm={onConfirm} + loading={loading} + /> + + + + + + Actions + + router.push(`/dashboard/proposal/${data.id}`)} + > + Update + + setOpen(true)}> + Delete + + + + + ); +}; diff --git a/src/pages/proposals/components/proposals-table/columns.tsx b/src/pages/proposals/components/proposals-table/columns.tsx new file mode 100644 index 0000000..0675ade --- /dev/null +++ b/src/pages/proposals/components/proposals-table/columns.tsx @@ -0,0 +1,80 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { Proposal } from '@/lib/dao/proposals'; +import { ColumnDef } from '@tanstack/react-table'; +import { CellAction } from './cell-action'; + +export const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false + }, + { + accessorKey: 'id', + header: 'ID' + }, + { + accessorKey: 'proposer', + header: 'Proposer' + }, + { + accessorKey: 'description', + header: 'Description' + }, + { + accessorKey: 'kind', + header: 'Kind', + cell: ({ row }) => { + const kind = row.original.kind; + if ('AddMemberToRole' in kind) { + return `Add Member: ${kind.AddMemberToRole.member_id}`; + } else if ('FunctionCall' in kind) { + return `Function Call: ${kind.FunctionCall.receiver_id}`; + } + return 'Unknown'; + } + }, + { + accessorKey: 'status', + header: 'Status' + }, + { + accessorKey: 'votes', + header: 'Votes', + cell: ({ row }) => { + const votes = row.original.votes; + return Object.entries(votes) + .map(([key, value]) => `${key}: ${value}`) + .join(', '); + } + }, + { + accessorKey: 'vote_counts', + header: 'Vote Counts', + cell: ({ row }) => { + const voteCounts = row.original.vote_counts.council; + if (Array.isArray(voteCounts)) { + return `Yes: ${voteCounts[0]}, No: ${voteCounts[1]}, Abstain: ${voteCounts[2]}`; + } + return 'N/A'; + } + }, + { + id: 'actions', + cell: ({ row }) => + } +]; diff --git a/src/pages/proposals/components/proposals-table/index.tsx b/src/pages/proposals/components/proposals-table/index.tsx new file mode 100644 index 0000000..a8ab7c1 --- /dev/null +++ b/src/pages/proposals/components/proposals-table/index.tsx @@ -0,0 +1,25 @@ +import DataTable from '@/components/shared/data-table'; +import { columns } from './columns'; +import MemberTableActions from './member-table-action'; +import { Proposal } from '@/lib/dao/proposals'; + +type TProposalsTableProps = { + proposals: Proposal[]; + page: number; + totalProposals: number; + pageCount: number; +}; + +export default function ProposalsTable({ + proposals, + pageCount +}: TProposalsTableProps) { + return ( + <> + + {proposals && ( + + )} + + ); +} diff --git a/src/pages/proposals/components/proposals-table/member-table-action.tsx b/src/pages/proposals/components/proposals-table/member-table-action.tsx new file mode 100644 index 0000000..dc2cafe --- /dev/null +++ b/src/pages/proposals/components/proposals-table/member-table-action.tsx @@ -0,0 +1,18 @@ +import PopupModal from '@/components/shared/popup-modal'; +import TableSearchInput from '@/components/shared/table-search-input'; +import MemberCreateForm from '../member-forms/member-create-form'; + +export default function MemberTableActions() { + return ( +
+
+ +
+
+ } + /> +
+
+ ); +} diff --git a/src/pages/proposals/index.tsx b/src/pages/proposals/index.tsx new file mode 100644 index 0000000..01bd130 --- /dev/null +++ b/src/pages/proposals/index.tsx @@ -0,0 +1,104 @@ +import { Breadcrumbs } from '@/components/shared/breadcrumbs'; +import { DataTableSkeleton } from '@/components/shared/data-table-skeleton'; +import PageHead from '@/components/shared/page-head'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Proposal } from '@/lib/dao/proposals'; +import Fuse from 'fuse.js'; +import debounce from 'lodash/debounce'; +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import ProposalsTable from './components/proposals-table'; +import { useGetProposals } from './queries/queries'; + +export default function ProposalsPage() { + const [searchParams] = useSearchParams(); + const page = Number(searchParams.get('page') || 1); + const pageLimit = Number(searchParams.get('limit') || 20); + const searchQuery = searchParams.get('search') || ''; + const offset = (page - 1) * pageLimit; + + const { + data: proposals, + isLoading, + isError, + error + } = useGetProposals(offset, pageLimit); + + const [filteredProposals, setFilteredProposals] = useState([]); + const [paginatedProposals, setPaginatedProposals] = useState([]); + + useEffect(() => { + if (proposals) { + const debouncedSearch = debounce((query) => { + if (!query) { + setFilteredProposals(proposals); + return; + } + + const fuse = new Fuse(proposals, { + keys: ['id', 'roles'], // adjust these fields based on your user object structure + threshold: 0.3, + includeScore: true + }); + + const results = fuse.search(query); + setFilteredProposals(results.map((result) => result.item)); + }, 300); + + debouncedSearch(searchQuery); + + return () => debouncedSearch.cancel(); + } + }, [proposals, searchQuery]); + + useEffect(() => { + const offset = (page - 1) * pageLimit; + setPaginatedProposals(filteredProposals.slice(offset, offset + pageLimit)); + }, [filteredProposals, page, pageLimit]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ + Error + + {error?.message || 'An error occurred while fetching proposals.'} + + +
+ ); + } + + const totalProposals = filteredProposals.length; + const pageCount = Math.ceil(totalProposals / pageLimit); + + return ( +
+ + + +
+ ); +} diff --git a/src/pages/proposals/queries/queries.ts b/src/pages/proposals/queries/queries.ts new file mode 100644 index 0000000..53ece04 --- /dev/null +++ b/src/pages/proposals/queries/queries.ts @@ -0,0 +1,13 @@ +import { useWallet } from '@/hooks/use-wallet'; +import { getProposals } from '@/lib/dao/proposals'; +import { useQuery } from '@tanstack/react-query'; + +export const useGetProposals = (offset: number, limit: number) => { + const { wallet } = useWallet(); + + return useQuery({ + queryKey: ['proposals'], + queryFn: async () => getProposals(offset, limit, wallet!), + retry: 3 + }); +}; diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx new file mode 100644 index 0000000..38031dc --- /dev/null +++ b/src/pages/settings/index.tsx @@ -0,0 +1,45 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useGetConfig } from './queries/queries'; + +export default function SettingsPage() { + const { data, isLoading, isError, error } = useGetConfig(); + + if (isLoading) { + return ( + + + + + + + + ); + } + + if (isError) { + return ( + + + Error + + +

{error ? error.message : 'An error occurred'}

+
+
+ ); + } + + return ( + + + Configuration Data + + +
+          {JSON.stringify(data, null, 2)}
+        
+
+
+ ); +} diff --git a/src/pages/settings/queries/queries.ts b/src/pages/settings/queries/queries.ts new file mode 100644 index 0000000..9a64ce0 --- /dev/null +++ b/src/pages/settings/queries/queries.ts @@ -0,0 +1,13 @@ +import { useWallet } from '@/hooks/use-wallet'; +import { getConfig } from '@/lib/dao/config'; +import { useQuery } from '@tanstack/react-query'; + +export const useGetConfig = () => { + const { wallet } = useWallet(); + + return useQuery({ + queryKey: ['config'], + queryFn: async () => getConfig(wallet!), + retry: 3 + }); +}; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 41b5982..dacc81c 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -10,6 +10,11 @@ const SignInPage = lazy(() => import('@/pages/auth/signin')); const DashboardPage = lazy(() => import('@/pages/dashboard')); const MemberPage = lazy(() => import('@/pages/members')); const MemberDetailPage = lazy(() => import('@/pages/members/MemberDetailPage')); +const ProposalsPage = lazy(() => import('@/pages/proposals')); +const ProposalDetailPage = lazy( + () => import('@/pages/proposals/ProposalDetailPage') +); +const SettingsPage = lazy(() => import('@/pages/settings')); // ---------------------------------------------------------------------- @@ -37,6 +42,18 @@ export default function AppRouter() { path: 'member/details', element: }, + { + path: 'proposal', + element: + }, + { + path: 'proposal/details', + element: + }, + { + path: 'settings', + element: + }, { path: 'form', element: