Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,160 changes: 4,160 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* - Please do NOT modify this file.
*/

const PACKAGE_VERSION = '2.13.6'
const PACKAGE_VERSION = '2.14.6'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down Expand Up @@ -105,7 +105,7 @@ addEventListener('fetch', function (event) {
return
}

// Bypass all requests when there are no active clients
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
Expand Down
5 changes: 0 additions & 5 deletions src/components/approval/ApprovalStatusList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ export interface ApprovalStatusListProps {
* Matched against `required` by `approverRole`.
*/
given: ApprovalDecision[];
/**
* Role names that are still awaiting a decision.
* Used as a hint, but state is always re-derived from `given` first.
*/
pending: string[];
}

type RowState = "approved" | "rejected" | "pending";
Expand Down
49 changes: 49 additions & 0 deletions src/components/approval/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";

export interface EmptyStateProps {
/** The main heading explaining the empty state */
title: string;
/** Additional details or instructions */
description?: React.ReactNode;
/** An optional icon element (e.g., from lucide-react) */
icon?: React.ReactNode;
/** Optional action button or link to help the user recover from the empty state */
action?: React.ReactNode;
/** Optional additional CSS classes for the container */
className?: string;
}

/**
* A reusable component for displaying an empty state when there is no data
* available to show to the user (e.g., empty lists, no search results).
*/
export function EmptyState({
title,
description,
icon,
action,
className = "",
}: EmptyStateProps) {
return (
<div
data-testid="empty-state"
className={`flex flex-col items-center justify-center p-8 text-center rounded-2xl border-2 border-dashed border-slate-200 bg-slate-50/50 ${className}`}
>
{icon && (
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-slate-100 text-slate-500">
{icon}
</div>
)}

<h3 className="text-lg font-semibold text-slate-900">{title}</h3>

{description && (
<div className="mt-2 max-w-sm text-sm text-slate-500" data-testid="empty-state-message">
{description}
</div>
)}

{action && <div className="mt-6">{action}</div>}
</div>
);
}
5 changes: 0 additions & 5 deletions src/components/approval/__tests__/ApprovalStatusList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ describe("ApprovalStatusList", () => {
<ApprovalStatusList
required={["admin"]}
given={[]}
pending={["admin"]}
/>,
);

Expand All @@ -48,7 +47,6 @@ describe("ApprovalStatusList", () => {
<ApprovalStatusList
required={["admin"]}
given={given}
pending={[]}
/>,
);

Expand All @@ -69,7 +67,6 @@ describe("ApprovalStatusList", () => {
<ApprovalStatusList
required={["manager"]}
given={given}
pending={[]}
/>,
);

Expand All @@ -89,7 +86,6 @@ describe("ApprovalStatusList", () => {
<ApprovalStatusList
required={["admin", "manager"]}
given={given}
pending={["manager"]}
/>,
);

Expand All @@ -113,7 +109,6 @@ describe("ApprovalStatusList", () => {
<ApprovalStatusList
required={REQUIRED_ROLES}
given={given}
pending={["reviewer"]}
/>,
);

Expand Down
31 changes: 31 additions & 0 deletions src/components/auth/RoleGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { ReactNode } from "react";

export interface RoleGuardProps {
/** The role(s) required to view this component */
allowedRoles: string | string[];
/** The current user's role (usually fetched from an auth context/hook) */
userRole?: string;
/** Content to display if the user has the required role */
children: ReactNode;
/** Content to display if the user does not have the required role */
fallback?: ReactNode;
}

/**
* A wrapper component to conditionally render UI elements based on user roles.
*/
export function RoleGuard({
allowedRoles,
userRole,
children,
fallback = null,
}: RoleGuardProps) {
if (!userRole) return <>{fallback}</>;

const roles = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
const hasAccess = roles.some(
(role) => role.toLowerCase() === userRole.toLowerCase()
);

return hasAccess ? <>{children}</> : <>{fallback}</>;
}
11 changes: 6 additions & 5 deletions src/hooks/useDisputeDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ interface DisputeDetailApiResponse extends Omit<DisputeDetail, "resolution"> {
}

export interface EnrichedDisputeDetail extends DisputeDetail {
escrowOnChainStatus: string;
stellarExplorerUrl: string;
escrowOnChainStatus?: string;
stellarExplorerUrl?: string;
resolutionTxHash?: string;
}

function buildStellarExplorerUrl(accountId: string): string {
function buildStellarExplorerUrl(accountId?: string | null): string {
if (!accountId) return "";
return `https://stellar.expert/explorer/public/account/${encodeURIComponent(accountId)}`;
}

Expand Down Expand Up @@ -60,8 +61,8 @@ export function useDisputeDetail(disputeId: string) {
return {
...raw,
resolution,
escrowOnChainStatus: raw.escrow.status,
stellarExplorerUrl: buildStellarExplorerUrl(raw.escrow.accountId),
escrowOnChainStatus: raw.escrow?.status,
stellarExplorerUrl: buildStellarExplorerUrl(raw.escrow?.accountId),
resolutionTxHash: raw.resolution?.txHash,
};
}, [query.data]);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useDisputes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function useDisputes({ status, overdue }: UseDisputesParams = {}) {

const query = useInfiniteQuery({
queryKey: ["disputes", queryParams],
queryFn: async ({ pageParam }: { pageParam: string | undefined | unknown }) => {
queryFn: ({ pageParam }) => {
const params = new URLSearchParams();

if (status && status !== "all") {
Expand Down
9 changes: 3 additions & 6 deletions src/mocks/handlers/adoption.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { http, HttpResponse, delay } from "msw";
import type {
AdoptionDetails,
AdoptionTimelineEntry,
} from "../../types/adoption";
import type { AdoptionDetails } from "../../types/adoption";

const MOCK_TIMELINE: AdoptionTimelineEntry[] = [
const MOCK_TIMELINE = [
{
id: "1",
adoptionId: "adoption-1",
sdkEvent: "event1",
message: "Initial adoption request",
fromStatus: undefined,
fromStatus: null,
toStatus: "ESCROW_CREATED",
actor: "System",
timestamp: "2026-03-25T10:00:00Z",
Expand Down
10 changes: 7 additions & 3 deletions src/pages/AdoptionTimelinePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ export default function AdoptionTimelinePage() {

entries.forEach((entry) => {
const entryDate = new Date(entry.timestamp);

// Coerce undefined fromStatus to null to satisfy strict TimelineEntry type expectations
const normalizedEntry = { ...entry, fromStatus: entry.fromStatus ?? null } as unknown as TimelineEntryType;

if (entryDate >= today) {
groups[0].items.push(entry);
groups[0].items.push(normalizedEntry);
} else if (entryDate >= yesterday) {
groups[1].items.push(entry);
groups[1].items.push(normalizedEntry);
} else {
groups[2].items.push(entry);
groups[2].items.push(normalizedEntry);
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/pages/EditAdoptionListing.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type ChangeEvent, } from "react";
import { useState, type ChangeEvent } from "react";
import { Upload } from "lucide-react";
export default function EditAdoptionListing() {
const [isLoading, setIsLoading] = useState(false);
Expand Down
33 changes: 33 additions & 0 deletions src/pages/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useNavigate } from "react-router-dom";
import { EmptyState } from "../components/approval/EmptyState";
import { AlertTriangle } from "lucide-react";

export default function ErrorPage() {
const navigate = useNavigate();

return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<EmptyState
title="500 - Application Error"
description="Something went wrong on our end. Please try again later."
icon={<AlertTriangle className="w-6 h-6 text-red-500" />}
action={
<div className="flex flex-col sm:flex-row gap-3">
<button
onClick={() => window.location.reload()}
className="px-6 py-2.5 bg-[#E84D2A] text-white font-semibold rounded-xl hover:bg-[#d4431f] transition-colors shadow-sm"
>
Reload Page
</button>
<button
onClick={() => navigate("/")}
className="px-6 py-2.5 bg-white border border-gray-300 text-gray-700 font-semibold rounded-xl hover:bg-gray-50 transition-colors shadow-sm"
>
Go Home
</button>
</div>
}
/>
</div>
);
}
25 changes: 25 additions & 0 deletions src/pages/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useNavigate } from "react-router-dom";
import { EmptyState } from "../components/approval/EmptyState";
import { SearchX } from "lucide-react";

export default function NotFoundPage() {
const navigate = useNavigate();

return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<EmptyState
title="404 - Page Not Found"
description="The page you are looking for doesn't exist or has been moved."
icon={<SearchX className="w-6 h-6" />}
action={
<button
onClick={() => navigate("/")}
className="px-6 py-2.5 bg-[#E84D2A] text-white font-semibold rounded-xl hover:bg-[#d4431f] transition-colors shadow-sm"
>
Go Back Home
</button>
}
/>
</div>
);
}
Loading
Loading