From 1fc6c7223d2ec3a6daf032eaba1a6365ab312ec8 Mon Sep 17 00:00:00 2001 From: kamaldeen Aliyu Date: Fri, 30 Jan 2026 12:43:26 +0100 Subject: [PATCH] Created the Escrow dashboard according to issues description --- apps/frontend/app/dashboard/page.tsx | 85 ++++++++ apps/frontend/app/layout.tsx | 15 +- apps/frontend/app/page.tsx | 141 ++++++------ apps/frontend/component/Providers.tsx | 21 ++ .../component/dashboard/EscrowCard.tsx | 206 ++++++++++++++++++ .../component/dashboard/EscrowFilters.tsx | 79 +++++++ .../component/dashboard/EscrowList.tsx | 126 +++++++++++ .../component/dashboard/StatusTabs.tsx | 38 ++++ apps/frontend/component/layout/Navbar.tsx | 51 ++--- apps/frontend/hooks/useEscrows.ts | 30 +++ apps/frontend/package-lock.json | 37 +++- apps/frontend/package.json | 1 + apps/frontend/services/escrow.ts | 205 +++++++++++++++++ apps/frontend/types/escrow.ts | 35 +++ package-lock.json | 6 + 15 files changed, 953 insertions(+), 123 deletions(-) create mode 100644 apps/frontend/app/dashboard/page.tsx create mode 100644 apps/frontend/component/Providers.tsx create mode 100644 apps/frontend/component/dashboard/EscrowCard.tsx create mode 100644 apps/frontend/component/dashboard/EscrowFilters.tsx create mode 100644 apps/frontend/component/dashboard/EscrowList.tsx create mode 100644 apps/frontend/component/dashboard/StatusTabs.tsx create mode 100644 apps/frontend/hooks/useEscrows.ts create mode 100644 apps/frontend/services/escrow.ts create mode 100644 apps/frontend/types/escrow.ts create mode 100644 package-lock.json diff --git a/apps/frontend/app/dashboard/page.tsx b/apps/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..ce58ebc --- /dev/null +++ b/apps/frontend/app/dashboard/page.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import StatusTabs from '@/component/dashboard/StatusTabs'; +import EscrowList from '@/component/dashboard/EscrowList'; +import EscrowFilters from '@/component/dashboard/EscrowFilters'; +import { useEscrows } from '../../hooks/useEscrows'; +import { IEscrow } from '@/types/escrow'; + +export default function DashboardPage() { + const [activeTab, setActiveTab] = useState<'all' | 'active' | 'pending' | 'completed' | 'disputed'>('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [sortBy, setSortBy] = useState<'date' | 'amount' | 'deadline'>('date'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + + const { + data: escrowsData, + isLoading, + isError, + hasNextPage, + fetchNextPage, + isFetchingNextPage + } = useEscrows({ + status: activeTab, + search: searchQuery, + sortBy, + sortOrder + }); + + // Flatten the paginated data + const flatEscrows = escrowsData?.pages.flatMap((page: any) => page.escrows) || []; + + // Handle tab changes + const handleTabChange = (tab: 'all' | 'active' | 'pending' | 'completed' | 'disputed') => { + setActiveTab(tab); + }; + + // Handle search + const handleSearchChange = (query: string) => { + setSearchQuery(query); + }; + + // Handle sort change + const handleSortChange = (field: 'date' | 'amount' | 'deadline', order: 'asc' | 'desc') => { + setSortBy(field); + setSortOrder(order); + }; + + return ( +
+
+
+

+ Escrow Dashboard +

+

+ Manage all your escrow agreements in one place +

+
+ +
+ + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/frontend/app/layout.tsx b/apps/frontend/app/layout.tsx index f7fa87e..7667b2d 100644 --- a/apps/frontend/app/layout.tsx +++ b/apps/frontend/app/layout.tsx @@ -1,6 +1,8 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import Providers from "@/component/Providers"; +import Navbar from "@/component/layout/Navbar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,8 +15,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Vaultix - Secure Escrow Platform", + description: "Decentralized escrow platform built on Stellar blockchain", }; export default function RootLayout({ @@ -27,8 +29,13 @@ export default function RootLayout({ - {children} + + +
+ {children} +
+
); -} +} \ No newline at end of file diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/page.tsx index 9064d98..432c880 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/page.tsx @@ -1,105 +1,90 @@ - import Image from "next/image"; export default function Home() { return (
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+
+
+
+
+ + V + +
+
+

+ Vaultix +

+
+ +
+

+ Secure Decentralized Escrow Platform +

+

+ Protect your transactions with smart escrow agreements powered by Stellar blockchain technology. +

+ +
+ + Access Dashboard + + + Create Escrow + +
+
-
- - Vercel logomark - Deploy now - - - Read our docs - +
+
+
🔒
+

Secure Transactions

+

Smart contracts ensure funds are only released when conditions are met

+
+ +
+
+

Fast Settlement

+

Blockchain-powered transactions settle in seconds, not days

+
+
+
🌐
+

Global Access

+

Access your escrow agreements from anywhere in the world

+
-
); -} +} \ No newline at end of file diff --git a/apps/frontend/component/Providers.tsx b/apps/frontend/component/Providers.tsx new file mode 100644 index 0000000..44f2fd7 --- /dev/null +++ b/apps/frontend/component/Providers.tsx @@ -0,0 +1,21 @@ +'use client'; + +import React, { useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +export default function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState(() => new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + retry: 1, + }, + }, + })); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/apps/frontend/component/dashboard/EscrowCard.tsx b/apps/frontend/component/dashboard/EscrowCard.tsx new file mode 100644 index 0000000..879fb00 --- /dev/null +++ b/apps/frontend/component/dashboard/EscrowCard.tsx @@ -0,0 +1,206 @@ +import React from 'react'; +// Temporarily define the interface here until types are properly configured +interface IEscrow { + id: string; + title: string; + description: string; + amount: string; + asset: string; + creatorAddress: string; + counterpartyAddress: string; + deadline: string; + status: 'created' | 'funded' | 'confirmed' | 'released' | 'completed' | 'cancelled' | 'disputed'; + createdAt: string; + updatedAt: string; + milestones?: Array<{ + id: string; + title: string; + amount: string; + status: 'pending' | 'released'; + }>; +} + +interface EscrowCardProps { + escrow: IEscrow; +} + +const getStatusColor = (status: string) => { + switch (status) { + case 'created': + case 'funded': + return 'bg-blue-100 text-blue-800'; + case 'confirmed': + return 'bg-yellow-100 text-yellow-800'; + case 'released': + 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 getStatusText = (status: string) => { + switch (status) { + case 'created': + return 'Created'; + case 'funded': + return 'Funded'; + case 'confirmed': + return 'Confirmed'; + case 'released': + return 'Released'; + case 'completed': + return 'Completed'; + case 'cancelled': + return 'Cancelled'; + case 'disputed': + return 'Disputed'; + default: + return status.charAt(0).toUpperCase() + status.slice(1); + } +}; + +const EscrowCard: React.FC = ({ escrow }) => { + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + }; + + const getQuickActions = () => { + const actions = []; + + // Based on status, show appropriate actions + switch (escrow.status) { + case 'created': + case 'funded': + actions.push( + + View Details + + ); + break; + case 'confirmed': + actions.push( + + Confirm Delivery + , + + Dispute + + ); + break; + case 'released': + actions.push( + + View Details + + ); + break; + case 'completed': + actions.push( + + View Details + + ); + break; + case 'disputed': + actions.push( + + View Details + + ); + break; + default: + actions.push( + + View Details + + ); + } + + return actions; + }; + + return ( +
+
+
+
+
+

{escrow.title}

+ + {getStatusText(escrow.status)} + +
+

{escrow.description}

+ +
+
+

Amount

+

{escrow.amount} {escrow.asset}

+
+ +
+

Counterparty

+

{escrow.counterpartyAddress.substring(0, 10)}...

+
+ +
+

Created

+

{formatDate(escrow.createdAt)}

+
+ +
+

Deadline

+

{formatDate(escrow.deadline)}

+
+
+
+ +
+
+ {getQuickActions()} +
+
+
+
+
+ ); +}; + +export default EscrowCard; \ No newline at end of file diff --git a/apps/frontend/component/dashboard/EscrowFilters.tsx b/apps/frontend/component/dashboard/EscrowFilters.tsx new file mode 100644 index 0000000..8e870e3 --- /dev/null +++ b/apps/frontend/component/dashboard/EscrowFilters.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect } from 'react'; +import Input from '@/component/ui/Input'; + +interface EscrowFiltersProps { + searchQuery: string; + onSearchChange: (query: string) => void; + sortBy: 'date' | 'amount' | 'deadline'; + sortOrder: 'asc' | 'desc'; + onSortChange: (field: 'date' | 'amount' | 'deadline', order: 'asc' | 'desc') => void; +} + +const EscrowFilters: React.FC = ({ + searchQuery, + onSearchChange, + sortBy, + sortOrder, + onSortChange, +}) => { + const [localSearch, setLocalSearch] = useState(searchQuery); + + // Debounce search input + useEffect(() => { + const handler = setTimeout(() => { + onSearchChange(localSearch); + }, 300); + + return () => { + clearTimeout(handler); + }; + }, [localSearch, onSearchChange]); + + const handleSortFieldChange = (e: React.ChangeEvent) => { + onSortChange(e.target.value as 'date' | 'amount' | 'deadline', sortOrder); + }; + + const handleSortOrderChange = (e: React.ChangeEvent) => { + onSortChange(sortBy, e.target.value as 'asc' | 'desc'); + }; + + return ( +
+
+ setLocalSearch(e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+
+ ); +}; + +export default EscrowFilters; \ No newline at end of file diff --git a/apps/frontend/component/dashboard/EscrowList.tsx b/apps/frontend/component/dashboard/EscrowList.tsx new file mode 100644 index 0000000..cb1f65f --- /dev/null +++ b/apps/frontend/component/dashboard/EscrowList.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import EscrowCard from './EscrowCard'; + +// Define the interface here since we can't import from types yet +interface IEscrow { + id: string; + title: string; + description: string; + amount: string; + asset: string; + creatorAddress: string; + counterpartyAddress: string; + deadline: string; + status: 'created' | 'funded' | 'confirmed' | 'released' | 'completed' | 'cancelled' | 'disputed'; + createdAt: string; + updatedAt: string; + milestones?: Array<{ + id: string; + title: string; + amount: string; + status: 'pending' | 'released'; + }>; +} + +interface EscrowListProps { + escrows: IEscrow[]; + isLoading: boolean; + isError: boolean; + activeTab: 'all' | 'active' | 'pending' | 'completed' | 'disputed'; + hasNextPage?: boolean; + fetchNextPage?: () => void; + isFetchingNextPage?: boolean; +} + +const EscrowList: React.FC = ({ + escrows, + isLoading, + isError, + activeTab, + hasNextPage, + fetchNextPage, + isFetchingNextPage, +}) => { + // Show loading skeletons when data is loading + if (isLoading && escrows.length === 0) { + return ( +
+ {[...Array(3)].map((_, index) => ( +
+ ))} +
+ ); + } + + // Show error state + if (isError) { + return ( +
+

Error Loading Escrows

+

There was an issue retrieving your escrow agreements. Please try again later.

+
+ ); + } + + // Show empty state based on active tab + if (escrows.length === 0) { + let emptyMessage = ''; + switch (activeTab) { + case 'all': + emptyMessage = 'You have no escrow agreements yet.'; + break; + case 'active': + emptyMessage = 'You have no active escrow agreements.'; + break; + case 'pending': + emptyMessage = 'You have no escrows pending confirmation.'; + break; + case 'completed': + emptyMessage = 'You have no completed escrow agreements.'; + break; + case 'disputed': + emptyMessage = 'You have no disputed escrow agreements.'; + break; + default: + emptyMessage = 'No escrow agreements found.'; + } + + return ( +
+

No Escrows Found

+

{emptyMessage}

+ +
+ ); + } + + return ( +
+ {escrows.map((escrow) => ( + + ))} + + {/* Load more button for pagination */} + {hasNextPage && ( +
+ +
+ )} +
+ ); +}; + +export default EscrowList; \ No newline at end of file diff --git a/apps/frontend/component/dashboard/StatusTabs.tsx b/apps/frontend/component/dashboard/StatusTabs.tsx new file mode 100644 index 0000000..aa28918 --- /dev/null +++ b/apps/frontend/component/dashboard/StatusTabs.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface StatusTabsProps { + activeTab: 'all' | 'active' | 'pending' | 'completed' | 'disputed'; + onTabChange: (tab: 'all' | 'active' | 'pending' | 'completed' | 'disputed') => void; +} + +const StatusTabs: React.FC = ({ activeTab, onTabChange }) => { + const tabs = [ + { id: 'all', label: 'All' }, + { id: 'active', label: 'Active' }, + { id: 'pending', label: 'Pending Confirmation' }, + { id: 'completed', label: 'Completed' }, + { id: 'disputed', label: 'Disputed' }, + ]; + + return ( +
+ +
+ ); +}; + +export default StatusTabs; \ No newline at end of file diff --git a/apps/frontend/component/layout/Navbar.tsx b/apps/frontend/component/layout/Navbar.tsx index 24ff1db..f86e09d 100644 --- a/apps/frontend/component/layout/Navbar.tsx +++ b/apps/frontend/component/layout/Navbar.tsx @@ -28,7 +28,7 @@ export default function Navbar(): JSX.Element {
-
+
V @@ -43,34 +43,29 @@ export default function Navbar(): JSX.Element { {/* Desktop Navigation */}
- Features + Home - How It Works + Dashboard - DAO + Create Escrow - Docs - - - Launch App + GitHub
@@ -92,43 +87,37 @@ export default function Navbar(): JSX.Element {
setIsMenuOpen(false)} className="block text-gray-300 hover:text-white transition-colors" > - Features + Home setIsMenuOpen(false)} className="block text-gray-300 hover:text-white transition-colors" > - How It Works + Dashboard setIsMenuOpen(false)} className="block text-gray-300 hover:text-white transition-colors" > - DAO + Create Escrow setIsMenuOpen(false)} className="block text-gray-300 hover:text-white transition-colors" > - Docs - - setIsMenuOpen(false)} - className="block w-full text-center bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white px-5 py-2 rounded-full font-medium" - > - Launch App + GitHub
)} ); -} +} \ No newline at end of file diff --git a/apps/frontend/hooks/useEscrows.ts b/apps/frontend/hooks/useEscrows.ts new file mode 100644 index 0000000..6635d90 --- /dev/null +++ b/apps/frontend/hooks/useEscrows.ts @@ -0,0 +1,30 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { IEscrowResponse } from '@/types/escrow'; +import { EscrowService } from '@/services/escrow'; + +interface UseEscrowsParams { + status?: 'all' | 'active' | 'pending' | 'completed' | 'disputed'; + search?: string; + sortBy?: 'date' | 'amount' | 'deadline'; + sortOrder?: 'asc' | 'desc'; + enabled?: boolean; +} + +export const useEscrows = (params: UseEscrowsParams = {}) => { + return useInfiniteQuery({ + queryKey: ['escrows', params], + queryFn: async ({ pageParam = 1 }) => { + const response = await EscrowService.getEscrows({ + ...params, + page: pageParam as number, + limit: 10, + }); + return response; + }, + getNextPageParam: (lastPage, pages) => { + return lastPage.hasNextPage ? pages.length + 1 : undefined; + }, + initialPageParam: 1, + enabled: params.enabled !== false, + }); +}; \ No newline at end of file diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 02fc5af..b27d988 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@stellar/freighter-api": "^6.0.1", + "@tanstack/react-query": "^5.90.20", "clsx": "^2.1.1", "framer-motion": "^12.29.2", "lucide-react": "^0.562.0", @@ -1260,6 +1261,32 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.20.tgz", + "integrity": "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1294,7 +1321,6 @@ "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1354,7 +1380,6 @@ "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -1861,7 +1886,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2856,7 +2880,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3026,7 +3049,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5388,7 +5410,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5398,7 +5419,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5411,7 +5431,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6223,7 +6242,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6424,7 +6442,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 4b96b1a..21fc3cc 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@hookform/resolvers": "^5.2.2", "@stellar/freighter-api": "^6.0.1", + "@tanstack/react-query": "^5.90.20", "clsx": "^2.1.1", "framer-motion": "^12.29.2", "lucide-react": "^0.562.0", diff --git a/apps/frontend/services/escrow.ts b/apps/frontend/services/escrow.ts new file mode 100644 index 0000000..d5c932b --- /dev/null +++ b/apps/frontend/services/escrow.ts @@ -0,0 +1,205 @@ +import { IEscrow, IEscrowResponse, IEscrowFilters } from '@/types/escrow'; + +// Mock data for demonstration purposes +const MOCK_ESCROWS: IEscrow[] = [ + { + id: '1', + title: 'Website Development Project', + description: 'Development of a responsive website with React and Node.js backend', + amount: '1000', + asset: 'XLM', + creatorAddress: 'GABC...', + counterpartyAddress: 'GDEF...', + deadline: '2026-02-15T00:00:00Z', + status: 'confirmed', + createdAt: '2026-01-20T10:00:00Z', + updatedAt: '2026-01-25T14:30:00Z', + }, + { + id: '2', + title: 'Mobile App Design', + description: 'UI/UX design for iOS and Android mobile application', + amount: '500', + asset: 'XLM', + creatorAddress: 'GHIJ...', + counterpartyAddress: 'GKLM...', + deadline: '2026-03-01T00:00:00Z', + status: 'created', + createdAt: '2026-01-28T09:15:00Z', + updatedAt: '2026-01-28T09:15:00Z', + }, + { + id: '3', + title: 'Smart Contract Audit', + description: 'Security audit of Ethereum smart contracts', + amount: '2000', + asset: 'XLM', + creatorAddress: 'GNOP...', + counterpartyAddress: 'GQRS...', + deadline: '2026-02-28T00:00:00Z', + status: 'completed', + createdAt: '2026-01-15T11:45:00Z', + updatedAt: '2026-01-30T16:20:00Z', + }, + { + id: '4', + title: 'Content Writing Services', + description: 'Technical blog posts and documentation writing', + amount: '300', + asset: 'XLM', + creatorAddress: 'GTUV...', + counterpartyAddress: 'GWXY...', + deadline: '2026-02-10T00:00:00Z', + status: 'disputed', + createdAt: '2026-01-22T13:30:00Z', + updatedAt: '2026-01-29T08:45:00Z', + }, + { + id: '5', + title: 'Logo Design Package', + description: 'Professional logo design with multiple revisions and variations', + amount: '150', + asset: 'XLM', + creatorAddress: 'GAZY...', + counterpartyAddress: 'GBXW...', + deadline: '2026-02-20T00:00:00Z', + status: 'funded', + createdAt: '2026-01-25T16:20:00Z', + updatedAt: '2026-01-27T09:10:00Z', + }, + { + id: '6', + title: 'Video Editing Project', + description: 'Professional video editing for corporate presentation', + amount: '750', + asset: 'XLM', + creatorAddress: 'GCDE...', + counterpartyAddress: 'GFHI...', + deadline: '2026-03-15T00:00:00Z', + status: 'released', + createdAt: '2026-01-18T14:45:00Z', + updatedAt: '2026-01-26T11:30:00Z', + }, + { + id: '7', + title: 'Translation Services', + description: 'Document translation from English to Spanish', + amount: '120', + asset: 'XLM', + creatorAddress: 'GJKL...', + counterpartyAddress: 'GMNO...', + deadline: '2026-02-05T00:00:00Z', + status: 'cancelled', + createdAt: '2026-01-20T08:30:00Z', + updatedAt: '2026-01-24T15:20:00Z', + }, +]; + +// Simulate API delay +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +export class EscrowService { + static async getEscrows(filters: IEscrowFilters = {}): Promise { + await delay(800); // Simulate network delay + + const { status, search, sortBy, sortOrder, page = 1, limit = 10 } = filters; + + let filteredEscrows = [...MOCK_ESCROWS]; + + // Apply status filter + if (status && status !== 'all') { + switch (status) { + case 'active': + filteredEscrows = filteredEscrows.filter(e => + ['created', 'funded', 'confirmed'].includes(e.status) + ); + break; + case 'pending': + filteredEscrows = filteredEscrows.filter(e => e.status === 'confirmed'); + break; + case 'completed': + filteredEscrows = filteredEscrows.filter(e => e.status === 'completed'); + break; + case 'disputed': + filteredEscrows = filteredEscrows.filter(e => e.status === 'disputed'); + break; + } + } + + // Apply search filter + if (search) { + const searchTerm = search.toLowerCase(); + filteredEscrows = filteredEscrows.filter(e => + e.title.toLowerCase().includes(searchTerm) || + e.counterpartyAddress.toLowerCase().includes(searchTerm) + ); + } + + // Apply sorting + if (sortBy) { + filteredEscrows.sort((a, b) => { + let comparison = 0; + + switch (sortBy) { + case 'date': + comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + case 'amount': + comparison = parseFloat(a.amount) - parseFloat(b.amount); + break; + case 'deadline': + comparison = new Date(a.deadline).getTime() - new Date(b.deadline).getTime(); + break; + } + + return sortOrder === 'asc' ? comparison : -comparison; + }); + } + + // Apply pagination + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedEscrows = filteredEscrows.slice(startIndex, endIndex); + + return { + escrows: paginatedEscrows, + hasNextPage: endIndex < filteredEscrows.length, + totalPages: Math.ceil(filteredEscrows.length / limit), + totalCount: filteredEscrows.length, + }; + } + + static async getEscrowById(id: string): Promise { + await delay(500); // Simulate network delay + return MOCK_ESCROWS.find(escrow => escrow.id === id) || null; + } + + static async createEscrow(data: Partial): Promise { + await delay(1000); // Simulate network delay + const newEscrow: IEscrow = { + id: Math.random().toString(36).substr(2, 9), + title: data.title || '', + description: data.description || '', + amount: data.amount || '', + asset: data.asset || 'XLM', + creatorAddress: data.creatorAddress || '', + counterpartyAddress: data.counterpartyAddress || '', + deadline: data.deadline || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + status: 'created', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + return newEscrow; + } + + static async updateEscrowStatus(id: string, status: IEscrow['status']): Promise { + await delay(500); // Simulate network delay + const escrow = MOCK_ESCROWS.find(e => e.id === id); + if (escrow) { + escrow.status = status; + escrow.updatedAt = new Date().toISOString(); + return escrow; + } + return null; + } +} \ No newline at end of file diff --git a/apps/frontend/types/escrow.ts b/apps/frontend/types/escrow.ts new file mode 100644 index 0000000..b42ab21 --- /dev/null +++ b/apps/frontend/types/escrow.ts @@ -0,0 +1,35 @@ +export interface IEscrow { + id: string; + title: string; + description: string; + amount: string; + asset: string; + creatorAddress: string; + counterpartyAddress: string; + deadline: string; // ISO date string + status: 'created' | 'funded' | 'confirmed' | 'released' | 'completed' | 'cancelled' | 'disputed'; + createdAt: string; // ISO date string + updatedAt: string; // ISO date string + milestones?: Array<{ + id: string; + title: string; + amount: string; + status: 'pending' | 'released'; + }>; +} + +export interface IEscrowResponse { + escrows: IEscrow[]; + hasNextPage: boolean; + totalPages?: number; + totalCount?: number; +} + +export interface IEscrowFilters { + status?: 'all' | 'active' | 'pending' | 'completed' | 'disputed'; + search?: string; + sortBy?: 'date' | 'amount' | 'deadline'; + sortOrder?: 'asc' | 'desc'; + page?: number; + limit?: number; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..55d30bd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Vaultix", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}