From 7a7fd71340c08fd52b51456e3ed4cfd929367c69 Mon Sep 17 00:00:00 2001 From: ogstevyn Date: Sat, 28 Mar 2026 09:52:23 +0100 Subject: [PATCH 1/2] created approval status widget --- src/components/ApprovalStatusWidget.tsx | 110 ++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/components/ApprovalStatusWidget.tsx diff --git a/src/components/ApprovalStatusWidget.tsx b/src/components/ApprovalStatusWidget.tsx new file mode 100644 index 0000000..45d7ba5 --- /dev/null +++ b/src/components/ApprovalStatusWidget.tsx @@ -0,0 +1,110 @@ +import { ClockIcon, CheckCircleIcon, XCircleIcon } from "./icons/StatusIcons"; + +interface ApprovalRole { + id: string; + name: string; + avatar?: string; +} + +interface ApprovalStatusWidgetProps { + required: ApprovalRole[]; + given: ApprovalRole[]; + pending: ApprovalRole[]; +} + +export function ApprovalStatusWidget({ + required, + given, + pending, +}: ApprovalStatusWidgetProps) { + const getInitial = (name: string): string => { + return name.charAt(0).toUpperCase(); + }; + + const renderRole = ( + role: ApprovalRole, + status: "pending" | "approved" | "rejected" + ) => { + const statusConfig = { + pending: { icon: ClockIcon, label: "Pending" }, + approved: { icon: CheckCircleIcon, label: "Approved" }, + rejected: { icon: XCircleIcon, label: "Rejected" }, + }; + + const { icon: IconComponent, label } = statusConfig[status]; + + return ( +
+
+ {role.avatar ? ( + {role.name} + ) : ( +
+ {getInitial(role.name)} +
+ )} +
+ +
+

{role.name}

+
+ +
+ + {label} +
+
+ ); + }; + + return ( +
+ {required.length > 0 && ( +
+

+ Required Approvals +

+
+ {required.map((role) => renderRole(role, "approved"))} +
+
+ )} + + {given.length > 0 && ( +
+

+ Given Approvals +

+
+ {given.map((role) => renderRole(role, "approved"))} +
+
+ )} + + {pending.length > 0 && ( +
+

+ Pending Approvals +

+
+ {pending.map((role) => renderRole(role, "pending"))} +
+
+ )} + + {required.length === 0 && given.length === 0 && pending.length === 0 && ( +
+

No approvals to display

+
+ )} +
+ ); +} From 8068a04a7a1b92f65fd182315f17403dde90999d Mon Sep 17 00:00:00 2001 From: ogstevyn Date: Sat, 28 Mar 2026 10:58:24 +0100 Subject: [PATCH 2/2] dispute page component --- src/components/icons/StatusIcons.tsx | 6 + src/pages/MyDisputesPage.tsx | 219 +++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/pages/MyDisputesPage.tsx diff --git a/src/components/icons/StatusIcons.tsx b/src/components/icons/StatusIcons.tsx index 0bedc1c..855091f 100644 --- a/src/components/icons/StatusIcons.tsx +++ b/src/components/icons/StatusIcons.tsx @@ -21,3 +21,9 @@ export const TrashIcon = () => ( ); + +export const XCircleIcon = () => ( + + + +); diff --git a/src/pages/MyDisputesPage.tsx b/src/pages/MyDisputesPage.tsx new file mode 100644 index 0000000..ca61d7e --- /dev/null +++ b/src/pages/MyDisputesPage.tsx @@ -0,0 +1,219 @@ +import { useState, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { useApiQuery } from "../hooks/useApiQuery"; +import { EscrowStatusBadge } from "../components/ui/EscrowStatusBadge"; + +interface Dispute { + id: string; + petName: string; + opponentName: string; + status: string; + createdAt: string; +} + +interface DisputesResponse { + disputes: Dispute[]; + total: number; + page: number; + pageSize: number; +} + +const ITEMS_PER_PAGE = 10; + +export default function MyDisputesPage() { + const navigate = useNavigate(); + const [currentPage, setCurrentPage] = useState(1); + + const { data, isLoading, isError } = useApiQuery( + ["disputes", currentPage], + () => + fetch(`/api/disputes?page=${currentPage}&pageSize=${ITEMS_PER_PAGE}`).then( + (res) => res.json() + ) + ); + + const disputes = data?.disputes || []; + const total = data?.total || 0; + const totalPages = Math.ceil(total / ITEMS_PER_PAGE); + + const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + }; + + if (isLoading) { + return ( +
+
+

+ My Disputes +

+
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+
+
+ ); + } + + if (isError || !data) { + return ( +
+
+

+ My Disputes +

+
+

+ Failed to load disputes. Please try again. +

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

+ My Disputes +

+

+ {total} dispute{total !== 1 ? "s" : ""} +

+ + {disputes.length === 0 ? ( +
+ + + +

+ No disputes yet +

+

+ Active disputes will appear here +

+
+ ) : ( + <> +
+ {disputes.map((dispute) => ( + + ))} +
+ + {totalPages > 1 && ( +
+ + +
+ {Array.from({ length: totalPages }).map((_, i) => { + const page = i + 1; + const isCurrentPage = page === currentPage; + if ( + page === 1 || + page === totalPages || + (page >= currentPage - 1 && page <= currentPage + 1) + ) { + return ( + + ); + } else if ( + (page === 2 && currentPage > 3) || + (page === totalPages - 1 && currentPage < totalPages - 2) + ) { + return ( + + ... + + ); + } + })} +
+ + +
+ )} + + )} +
+
+ ); +}