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
110 changes: 110 additions & 0 deletions apps/frontend/app/escrow/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client';

import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { useEscrow } from '@/hooks/useEscrow';
import { useWallet } from '@/hooks/useWallet';
import EscrowHeader from '@/components/escrow/detail/EscrowHeader';
import PartiesSection from '@/components/escrow/detail/PartiesSection';
import TermsSection from '@/components/escrow/detail/TermsSection';
import TimelineSection from '@/components/escrow/detail/TimelineSection';
import TransactionHistory from '@/components/escrow/detail/TransactionHistory';
import { IEscrowExtended } from '@/types/escrow';

const EscrowDetailPage = () => {
const { id } = useParams();
const { escrow, loading, error } = useEscrow(id as string);
const { connected, publicKey, connect } = useWallet(); // Assuming wallet hook exists
const [userRole, setUserRole] = useState<'creator' | 'counterparty' | null>(null);

useEffect(() => {
if (escrow && publicKey) {
if (escrow.creatorId === publicKey) {
setUserRole('creator');
} else if (escrow.parties?.some((party: any) => party.userId === publicKey)) {
setUserRole('counterparty');
}
}
}, [escrow, publicKey]);

if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<p className="mt-4 text-lg text-gray-600">Loading escrow details...</p>
</div>
</div>
);
}

if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full text-center">
<h2 className="text-xl font-bold text-red-600 mb-4">Error Loading Escrow</h2>
<p className="text-gray-600">{error}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Retry
</button>
</div>
</div>
);
}

if (!escrow) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="bg-white p-8 rounded-lg shadow-md max-w-md w-full text-center">
<h2 className="text-xl font-bold text-red-600 mb-4">Escrow Not Found</h2>
<p className="text-gray-600">The requested escrow agreement could not be found.</p>
<Link
href="/escrow"
className="mt-4 inline-block px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Back to Escrows
</Link>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header Section */}
<EscrowHeader
escrow={escrow}
userRole={userRole}
connected={connected}
connect={connect}
publicKey={publicKey}
/>

<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
<div className="lg:col-span-2 space-y-8">
{/* Parties Section */}
<PartiesSection escrow={escrow} userRole={userRole} />

{/* Timeline Section */}
<TimelineSection escrow={escrow} />

{/* Transaction History */}
<TransactionHistory escrow={escrow} />
</div>

<div className="lg:col-span-1">
{/* Terms Section */}
<TermsSection escrow={escrow} userRole={userRole} />
</div>
</div>
</div>
</div>
);
};

export default EscrowDetailPage;
23 changes: 12 additions & 11 deletions apps/frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Image from "next/image";
import Image from "next/image";
import Link from "next/link";

export default function Home() {
return (
Expand Down Expand Up @@ -27,18 +28,18 @@ export default function Home() {
</p>

<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
<Link
href="/dashboard"
className="rounded-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white font-medium text-lg px-8 py-3 transition-all duration-200 ease-in-out transform hover:scale-105 shadow-lg"
>
Access Dashboard
</a>
<a
</Link>
<Link
href="/escrow/create"
className="rounded-full border-2 border-gray-300 hover:border-gray-400 text-gray-700 hover:text-gray-900 font-medium text-lg px-8 py-3 transition-all duration-200 ease-in-out transform hover:scale-105"
>
Create Escrow
</a>
</Link>
</div>
</div>

Expand All @@ -64,26 +65,26 @@ export default function Home() {
</main>

<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/dashboard"
>
Dashboard
</a>
<a
</Link>
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/escrow/create"
>
Create Escrow
</a>
<a
</Link>
<Link
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://github.com/Vaultix"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
</Link>
</footer>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions apps/frontend/components/escrow/detail/EscrowHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import { Share, Copy, Wallet, Clock, CheckCircle, AlertTriangle, XCircle, ShareIcon } from 'lucide-react';
import { IEscrowExtended } from '@/types/escrow';

interface EscrowHeaderProps {
escrow: IEscrowExtended;
userRole: 'creator' | 'counterparty' | null;
connected: boolean;
connect: () => void;
publicKey: string | null;
}

const getStatusColor = (status: string) => {
switch (status.toLowerCase()) {
case 'pending':
return 'bg-yellow-100 text-yellow-800';
case 'active':
return 'bg-blue-100 text-blue-800';
case 'completed':
return 'bg-green-100 text-green-800';
case 'cancelled':
return 'bg-gray-100 text-gray-800';
case 'disputed':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};

const getStatusIcon = (status: string) => {
switch (status.toLowerCase()) {
case 'pending':
return <Clock className="h-4 w-4" />;
case 'active':
return <CheckCircle className="h-4 w-4" />;
case 'completed':
return <CheckCircle className="h-4 w-4" />;
case 'cancelled':
return <XCircle className="h-4 w-4" />;
case 'disputed':
return <AlertTriangle className="h-4 w-4" />;
default:
return <Clock className="h-4 w-4" />;
}
};

const EscrowHeader: React.FC<EscrowHeaderProps> = ({
escrow,
userRole,
connected,
connect,
publicKey
}: EscrowHeaderProps) => {
const handleCopyLink = () => {
navigator.clipboard.writeText(window.location.href);
alert('Link copied to clipboard!');
};

return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex flex-col md:flex-row md:items-start md:justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h1 className="text-2xl font-bold text-gray-900">{escrow.title}</h1>
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${getStatusColor(escrow.status)}`}>
{getStatusIcon(escrow.status)}
<span className="ml-1 capitalize">{escrow.status}</span>
</span>
</div>
<p className="text-gray-600 mb-4">{escrow.description}</p>

<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500">
<div className="flex items-center">
<span className="font-medium">ID:</span>
<span className="ml-1 font-mono">{escrow.id.substring(0, 8)}...</span>
</div>
<div className="flex items-center">
<span className="font-medium">Amount:</span>
<span className="ml-1">{Number(escrow.amount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 7 })} {escrow.asset}</span>
</div>
<div className="flex items-center">
<span className="font-medium">Created:</span>
<span className="ml-1">{new Date(escrow.createdAt).toLocaleDateString()}</span>
</div>
</div>
</div>

<div className="flex gap-2 mt-4 md:mt-0">
<button
onClick={handleCopyLink}
className="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<ShareIcon className="h-4 w-4 mr-2" />
Share
</button>
{!connected && (
<button
onClick={connect}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
);
};

export default EscrowHeader;
94 changes: 94 additions & 0 deletions apps/frontend/components/escrow/detail/PartiesSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { IEscrowExtended } from '@/types/escrow';

interface PartiesSectionProps {
escrow: IEscrowExtended;
userRole: 'creator' | 'counterparty' | null;
}

const getRoleColor = (role: string) => {
switch (role.toUpperCase()) {
case 'BUYER':
return 'bg-blue-100 text-blue-800';
case 'SELLER':
return 'bg-green-100 text-green-800';
case 'ARBITRATOR':
return 'bg-purple-100 text-purple-800';
default:
return 'bg-gray-100 text-gray-800';
}
};

const getStatusColor = (status: string) => {
switch (status.toUpperCase()) {
case 'ACCEPTED':
return 'bg-green-100 text-green-800';
case 'REJECTED':
return 'bg-red-100 text-red-800';
case 'PENDING':
return 'bg-yellow-100 text-yellow-800';
default:
return 'bg-gray-100 text-gray-800';
}
};

const PartiesSection: React.FC<PartiesSectionProps> = ({ escrow, userRole }: PartiesSectionProps) => {
return (
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Parties</h2>

<div className="space-y-4">
{escrow.parties.map((party: any) => (
<div key={party.id} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getRoleColor(party.role)}`}>
{party.role}
</span>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(party.status)}`}>
{party.status}
</span>
</div>
<p className="mt-2 text-sm text-gray-600">
<span className="font-medium">User ID:</span> {party.userId}
</p>
</div>

{userRole === 'creator' && party.status === 'PENDING' && (
<div className="flex space-x-2">
<button className="px-3 py-1 bg-green-500 text-white text-sm rounded hover:bg-green-600">
Accept
</button>
<button className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600">
Reject
</button>
</div>
)}
</div>
</div>
))}
</div>

{escrow.conditions && escrow.conditions.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-medium text-gray-900 mb-3">Conditions</h3>
<ul className="space-y-2">
{escrow.conditions.map((condition: any) => (
<li key={condition.id} className="flex items-start">
<div className="flex-shrink-0 h-5 w-5 text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg>
</div>
<p className="ml-3 text-sm text-gray-700">{condition.description}</p>
</li>
))}
</ul>
</div>
)}
</div>
);
};

export default PartiesSection;
Loading
Loading