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
7 changes: 6 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { DeliveryList } from '@/components/DeliveryList';
import { WorkflowCards } from '@/components/deliveries/WorkflowCards';

export default function Home() {
return (
<main className="flex flex-col min-h-screen">
<section className="bg-white px-6 py-8">
<div className="mx-auto max-w-6xl">
<WorkflowCards />
</div>
</section>
<section className="relative bg-gradient-to-b from-black via-gray-900 to-black text-white py-32 px-6 text-center">
<div className="max-w-4xl mx-auto space-y-8">
<p className="uppercase tracking-widest text-blue-400 text-sm">
Expand Down
98 changes: 98 additions & 0 deletions components/deliveries/WorkflowCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
'use client';

import { useMemo } from 'react';
import { useWorkflowCards } from '@/hooks/useWorkflowCards';
import type { Delivery } from '@/types/delivery';

function getStatusColor(status: string) {
switch (status) {
case 'DELIVERED':
return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
case 'IN_TRANSIT':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
case 'PENDING':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200';
case 'ACCEPTED':
return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200';
case 'CANCELLED':
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
}
}

function formatDate(dateString: string) {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}

export function WorkflowCards() {
const { data, isLoading, error } = useWorkflowCards();

const cards = useMemo(() => data ?? [], [data]);

if (isLoading) {
return <div className="p-4 text-center text-sm text-gray-500">Loading workflow cards...</div>;
}

if (error) {
return <div className="p-4 text-center text-sm text-red-500">Error loading workflow cards: {error.message}</div>;
}

if (!cards.length) {
return <div className="p-4 text-center text-sm text-gray-500">No workflow cards available.</div>;
}

return (
<div className="w-full max-w-5xl mx-auto space-y-4 p-4">
{cards.map((delivery: Delivery) => (
<article
key={delivery.id}
className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<p className="text-lg font-semibold text-gray-900 dark:text-gray-100">{delivery.trackingNumber}</p>
<span className={`inline-flex rounded-full px-3 py-1 text-sm font-medium ${getStatusColor(delivery.status)}`}>
{delivery.status}
</span>
</div>

<div className="space-y-1 text-sm text-gray-600 dark:text-gray-300">
<p className="break-words">
<span className="font-medium text-gray-700 dark:text-gray-200">From:</span> {delivery.origin}
</p>
<p className="break-words">
<span className="font-medium text-gray-700 dark:text-gray-200">To:</span> {delivery.destination}
</p>
</div>

{delivery.landmark ? (
<div className="flex flex-wrap items-start gap-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm dark:border-amber-800 dark:bg-amber-950/60">
<span className="shrink-0 rounded-full bg-amber-200 px-2 py-0.5 text-xs font-semibold uppercase tracking-wide text-amber-800 dark:bg-amber-800 dark:text-amber-100">
Landmark
</span>
<p className="min-w-0 flex-1 break-words italic text-amber-800 dark:text-amber-200">
{delivery.landmark}
</p>
</div>
) : null}
</div>

<div className="flex flex-col gap-2 text-sm text-gray-500 dark:text-gray-400 md:items-end">
<p className="font-medium text-gray-600 dark:text-gray-300">Escrow: {delivery.escrowStatus}</p>
{delivery.amount ? <p>Amount: {delivery.amount}</p> : null}
<p className="text-xs">Created: {formatDate(delivery.createdAt)}</p>
</div>
</div>
</article>
))}
</div>
);
}
92 changes: 92 additions & 0 deletions components/deliveries/__tests__/WorkflowCards.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WorkflowCards } from '@/components/deliveries/WorkflowCards';
import { deliveryWorkflowService } from '@/services/deliveryWorkflowService';
import type { Delivery } from '@/types/delivery';

jest.mock('@/services/deliveryWorkflowService', () => ({
deliveryWorkflowService: {
getWorkflowCards: jest.fn(),
},
}));

const mockedService = deliveryWorkflowService as jest.Mocked<typeof deliveryWorkflowService>;

const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});

return ({ children }: { children: React.ReactNode }) => (

Check failure on line 23 in components/deliveries/__tests__/WorkflowCards.test.tsx

View workflow job for this annotation

GitHub Actions / build (24.x)

Component definition is missing display name

Check failure on line 23 in components/deliveries/__tests__/WorkflowCards.test.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

Component definition is missing display name
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};

describe('WorkflowCards', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders a landmark badge and address details when landmark data exists', async () => {
const deliveries: Delivery[] = [
{
id: 'delivery-1',
trackingNumber: 'TRACK-100',
senderId: 'sender-1',
status: 'IN_TRANSIT',
origin: '123 Main St, Nairobi',
destination: '456 River Rd, Mombasa',
landmark: 'Near the central market',
escrowStatus: 'LOCKED',
amount: 120,
currency: 'USD',
createdAt: '2026-05-28T08:00:00Z',
updatedAt: '2026-05-28T08:00:00Z',
},
];

mockedService.getWorkflowCards.mockResolvedValue(deliveries);

render(<WorkflowCards />, { wrapper: createWrapper() });

await waitFor(() => {
expect(screen.getByText(/Near the central market/i)).toBeInTheDocument();
});

expect(screen.getByText('Landmark')).toBeInTheDocument();
expect(screen.getByText('123 Main St, Nairobi')).toBeInTheDocument();
});

it('renders cards without crashing when landmark is null', async () => {
const deliveries: Delivery[] = [
{
id: 'delivery-2',
trackingNumber: 'TRACK-200',
senderId: 'sender-2',
status: 'PENDING',
origin: '789 West St, Kisumu',
destination: '101 East Ave, Nakuru',
landmark: null,
escrowStatus: 'NOT_LOCKED',
amount: 80,
currency: 'USD',
createdAt: '2026-05-28T08:00:00Z',
updatedAt: '2026-05-28T08:00:00Z',
},
];

mockedService.getWorkflowCards.mockResolvedValue(deliveries);

render(<WorkflowCards />, { wrapper: createWrapper() });

await waitFor(() => {
expect(screen.getByText('TRACK-200')).toBeInTheDocument();
});

expect(screen.getByText('789 West St, Kisumu')).toBeInTheDocument();
expect(screen.getByText('101 East Ave, Nakuru')).toBeInTheDocument();
});
});
10 changes: 10 additions & 0 deletions hooks/useWorkflowCards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import { deliveryWorkflowService } from '@/services/deliveryWorkflowService';
import type { Delivery } from '@/types/delivery';

export function useWorkflowCards() {
return useQuery<Delivery[], Error>({
queryKey: ['workflow-cards'],
queryFn: () => deliveryWorkflowService.getWorkflowCards(),
});
}
9 changes: 9 additions & 0 deletions services/deliveryWorkflowService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { apiClient } from './api';
import type { Delivery } from '../types/delivery';

export const deliveryWorkflowService = {
getWorkflowCards: async (): Promise<Delivery[]> => {
const { data } = await apiClient.get<Delivery[]>('/deliveries');
return data;
},
};
Loading