From 3c93b72fb15abe2ba174888375c41bdafec45345 Mon Sep 17 00:00:00 2001 From: Sebastian Anioke Date: Tue, 28 Jul 2026 15:36:36 +0100 Subject: [PATCH 01/10] ci: add PR target check workflow to redirect main PRs to dev --- .github/workflows/pr-target-check.yml | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/pr-target-check.yml diff --git a/.github/workflows/pr-target-check.yml b/.github/workflows/pr-target-check.yml new file mode 100644 index 0000000..87fa547 --- /dev/null +++ b/.github/workflows/pr-target-check.yml @@ -0,0 +1,37 @@ +name: PR Target Check +on: + pull_request_target: + branches: + - main +jobs: + warn-wrong-target: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - name: Comment and close PR targeting main + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: `👋 Hey @${context.payload.pull_request.user.login}! Thanks for your contribution. + +It looks like this PR targets the \`main\` branch. We only accept PRs targeting the \`dev\` branch — please reopen your PR against \`dev\` instead. + +Steps to fix: +1. Close this PR +2. Change the base branch to \`dev\` +3. Reopen the PR + +Thanks! 🚀` + }); + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + state: 'closed' + }); + From a184abe61d3066975a94fdb4da987ac19a86eb43 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:08 +0100 Subject: [PATCH 02/10] Add invoice types and API client Define Invoice, Investment, and InvestmentRequest TypeScript interfaces. Implement fetchInvoices, fetchInvoice, and createInvestment API functions. --- lib/api/index.ts | 52 ++++++++++++------------------------------------ lib/types.ts | 32 +++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 lib/types.ts diff --git a/lib/api/index.ts b/lib/api/index.ts index d9f12b1..6128286 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -1,53 +1,27 @@ -export interface Invoice { - id: string; - title: string; - seller: string; - amount: number; - raised: number; - investor_count: number; - status: "open" | "funded" | "settled"; - due_date: string; - has_more: boolean; - next_cursor: string | null; -} - -export interface InvoiceDetail extends Invoice { - description: string; - investors: { address: string; amount: number; timestamp: string }[]; - document_url: string; -} - -export interface InvoicesResponse { - invoices: Invoice[]; - has_more: boolean; - next_cursor: string | null; -} +import type { Invoice, Investment, InvestmentRequest } from "@/lib/types"; -const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "/api"; +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000/api"; -export async function fetchInvoices(cursor?: string): Promise { - const params = new URLSearchParams(); - if (cursor) params.set("cursor", cursor); - const res = await fetch(`${API_BASE}/invoices?${params}`); +export async function fetchInvoices(): Promise { + const res = await fetch(`${API_URL}/invoices`); if (!res.ok) throw new Error("Failed to fetch invoices"); return res.json(); } -export async function fetchInvoiceDetail(id: string): Promise { - const res = await fetch(`${API_BASE}/invoices/${id}`); - if (!res.ok) throw new Error("Failed to fetch invoice detail"); +export async function fetchInvoice(id: string): Promise { + const res = await fetch(`${API_URL}/invoices/${id}`); + if (!res.ok) throw new Error("Failed to fetch invoice"); return res.json(); } -export async function investInInvoice( - invoiceId: string, - amount: number -): Promise<{ success: boolean; invested_amount: number }> { - const res = await fetch(`${API_BASE}/invoices/${invoiceId}/invest`, { +export async function createInvestment( + data: InvestmentRequest +): Promise { + const res = await fetch(`${API_URL}/investments`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ amount }), + body: JSON.stringify(data), }); - if (!res.ok) throw new Error("Investment failed"); + if (!res.ok) throw new Error("Failed to create investment"); return res.json(); } diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..a58e81e --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,32 @@ +export type InvoiceStatus = "open" | "funded" | "settled"; + +export interface Invoice { + id: string; + title: string; + description: string; + amount: number; + currency: string; + status: InvoiceStatus; + sellerId: string; + sellerName: string; + deadline: string | null; + createdAt: string; + updatedAt: string; + riskScore?: number; +} + +export interface Investment { + id: string; + invoiceId: string; + investorId: string; + amount: number; + txHash: string | null; + status: "pending" | "confirmed" | "failed"; + createdAt: string; + confirmedAt: string | null; +} + +export interface InvestmentRequest { + invoiceId: string; + amount: number; +} From e29a0da0e1b0a6f5c5c50459fd55f018a6c6065b Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:13 +0100 Subject: [PATCH 03/10] Implement useInvoices hook with stale-while-revalidate caching Set staleTime to 60 seconds on the invoice list query so repeat visits serve cached data immediately and revalidate in background. --- hooks/useInvoices.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hooks/useInvoices.ts b/hooks/useInvoices.ts index ba773e6..cbd67ec 100644 --- a/hooks/useInvoices.ts +++ b/hooks/useInvoices.ts @@ -3,12 +3,16 @@ import { useInfiniteQuery } from "@tanstack/react-query"; import { fetchInvoices, type InvoicesResponse } from "@/lib/api"; +export const INVOICES_QUERY_KEY = ["invoices"] as const; +export const STALE_TIME = 60 * 1000; + export function useInvoices() { return useInfiniteQuery({ - queryKey: ["invoices"], + queryKey: INVOICES_QUERY_KEY, queryFn: ({ pageParam }) => fetchInvoices(pageParam as string | undefined), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.has_more ? lastPage.next_cursor ?? undefined : undefined, + staleTime: STALE_TIME, }); } From 78d7902997fe8151c4a269409eaf5bffb0be0eab Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:17 +0100 Subject: [PATCH 04/10] Add investment toast notifications and cache invalidation Show success toast with truncated tx hash and Stellar Expert link on confirmed investment. Show error toast on failure. Invalidate invoice list cache immediately after successful investment. --- hooks/useInvestments.ts | 66 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/hooks/useInvestments.ts b/hooks/useInvestments.ts index 915c72a..011f3d2 100644 --- a/hooks/useInvestments.ts +++ b/hooks/useInvestments.ts @@ -1,47 +1,51 @@ "use client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { investInInvoice } from "@/lib/api"; -import type { InvoiceDetail } from "@/lib/api"; +import { createInvestment } from "@/lib/api"; import { toast } from "sonner"; +import { INVOICES_QUERY_KEY } from "./useInvoices"; +import type { InvestmentRequest } from "@/lib/types"; -interface InvestMutationVars { - invoiceId: string; - amount: number; +function truncateTxHash(hash: string, chars = 8): string { + return hash.slice(0, chars); } -export function useInvestMutation() { +function getStellarExpertUrl(txHash: string): string { + const network = process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet"; + const host = + network === "mainnet" + ? "stellar.expert" + : "testnet.stellar.expert"; + return `https://${host}/explorer/tx/${txHash}`; +} + +export function useInvest() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ invoiceId, amount }: InvestMutationVars) => - investInInvoice(invoiceId, amount), - - onMutate: async ({ invoiceId, amount }) => { - await queryClient.cancelQueries({ queryKey: ["invoice", invoiceId] }); - - const previous = queryClient.getQueryData(["invoice", invoiceId]); - - if (previous) { - queryClient.setQueryData(["invoice", invoiceId], (old) => { - if (!old) return old; - const newRaised = Math.min(old.raised + amount, old.amount); - return { ...old, raised: newRaised, investor_count: old.investor_count + 1 }; + mutationFn: createInvestment, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: INVOICES_QUERY_KEY }); + + if (data.txHash) { + const truncated = truncateTxHash(data.txHash); + const expertUrl = getStellarExpertUrl(data.txHash); + + toast.success("Investment confirmed", { + description: `Tx: ${truncated}…`, + duration: 6000, + action: { + label: "View on Stellar Expert", + onClick: () => window.open(expertUrl, "_blank", "noopener"), + }, }); } - - return { previous }; }, - - onError: (_err, { invoiceId }, context) => { - if (context?.previous) { - queryClient.setQueryData(["invoice", invoiceId], context.previous); - } - toast.error("Investment failed. Your progress bar has been restored."); - }, - - onSettled: (_data, _error, { invoiceId }) => { - queryClient.invalidateQueries({ queryKey: ["invoice", invoiceId] }); + onError: (error: Error) => { + toast.error("Investment failed", { + description: error.message || "Transaction could not be completed.", + duration: 6000, + }); }, }); } From da0571f184dac3275b6553ba66f77e55b57a0284 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:22 +0100 Subject: [PATCH 05/10] Add shadcn ui components: sonner, select, badge Install toast (sonner), dropdown select, and badge components needed for marketplace filtering and investment notifications. --- components/ui/select.tsx | 190 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 components/ui/select.tsx diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..c0dc712 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,190 @@ +"use client" + +import * as React from "react" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" +import { Select as SelectPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "item-aligned", + align = "center", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} From 545fa7a90cc0e1d91ecad6a35b5e0c2dfcd3f9c7 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:27 +0100 Subject: [PATCH 06/10] Integrate Toaster provider for toast notifications Add sonner Toaster to the Providers component so toasts render across the entire application. --- components/providers.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/providers.tsx b/components/providers.tsx index 3ddbe84..7709e76 100644 --- a/components/providers.tsx +++ b/components/providers.tsx @@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useState } from "react"; +import { Toaster } from "@/components/ui/sonner"; export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( @@ -13,5 +14,10 @@ export function Providers({ children }: { children: React.ReactNode }) { }) ); - return {children}; + return ( + + {children} + + + ); } From 5288a0f62a0676f3349647b4de4df4bf5b3f7e02 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:31 +0100 Subject: [PATCH 07/10] Add marketplace components: filter bar, countdown timer, invoice card - Filter bar with status dropdown and debounced keyword search - Countdown timer showing DD:HH:MM with expired badge fallback - Invoice card component with status badge and invest button --- components/marketplace/countdown-timer.tsx | 53 +++++++++++++++++ components/marketplace/filter-bar.tsx | 67 ++++++++++++++++++++++ components/marketplace/index.ts | 4 +- components/marketplace/invoice-card.tsx | 57 ++++++++++++++++++ 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 components/marketplace/countdown-timer.tsx create mode 100644 components/marketplace/filter-bar.tsx create mode 100644 components/marketplace/invoice-card.tsx diff --git a/components/marketplace/countdown-timer.tsx b/components/marketplace/countdown-timer.tsx new file mode 100644 index 0000000..62fd90e --- /dev/null +++ b/components/marketplace/countdown-timer.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { differenceInSeconds } from "date-fns"; +import { Badge } from "@/components/ui/badge"; + +interface CountdownTimerProps { + deadline: string | null; + published: boolean; +} + +function formatCountdown(totalSeconds: number): string { + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + + return `${String(days).padStart(2, "0")}:${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; +} + +export function CountdownTimer({ deadline, published }: CountdownTimerProps) { + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + if (!deadline || !published) return; + + const id = setInterval(() => setNow(Date.now()), 60_000); + return () => clearInterval(id); + }, [deadline, published]); + + if (!published || !deadline) return null; + + const deadlineMs = new Date(deadline).getTime(); + const remaining = differenceInSeconds(deadlineMs, now); + + if (remaining <= 0) { + return ( + + Expired + + ); + } + + return ( + + {formatCountdown(remaining)} + + ); +} + +export function isExpired(deadline: string | null): boolean { + if (!deadline) return false; + return new Date(deadline).getTime() <= Date.now(); +} diff --git a/components/marketplace/filter-bar.tsx b/components/marketplace/filter-bar.tsx new file mode 100644 index 0000000..7ff6338 --- /dev/null +++ b/components/marketplace/filter-bar.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Search, X } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { InvoiceStatus } from "@/lib/types"; + +interface MarketplaceFilterBarProps { + status: InvoiceStatus | "all"; + search: string; + onStatusChange: (value: InvoiceStatus | "all") => void; + onSearchChange: (value: string) => void; + onClear: () => void; +} + +export function MarketplaceFilterBar({ + status, + search, + onStatusChange, + onSearchChange, + onClear, +}: MarketplaceFilterBarProps) { + const hasFilters = status !== "all" || search.length > 0; + + return ( +
+ + +
+ + onSearchChange(e.target.value)} + className="pl-9" + /> +
+ + {hasFilters && ( + + )} +
+ ); +} diff --git a/components/marketplace/index.ts b/components/marketplace/index.ts index cb0ff5c..eda6b4b 100644 --- a/components/marketplace/index.ts +++ b/components/marketplace/index.ts @@ -1 +1,3 @@ -export {}; +export { MarketplaceFilterBar } from "./filter-bar"; +export { InvoiceCard } from "./invoice-card"; +export { CountdownTimer, isExpired } from "./countdown-timer"; diff --git a/components/marketplace/invoice-card.tsx b/components/marketplace/invoice-card.tsx new file mode 100644 index 0000000..61dde3c --- /dev/null +++ b/components/marketplace/invoice-card.tsx @@ -0,0 +1,57 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { CountdownTimer, isExpired } from "./countdown-timer"; +import type { Invoice } from "@/lib/types"; + +const statusVariant: Record = { + open: "default", + funded: "secondary", + settled: "outline", +}; + +interface InvoiceCardProps { + invoice: Invoice; + onInvest?: (invoiceId: string) => void; +} + +export function InvoiceCard({ invoice, onInvest }: InvoiceCardProps) { + const published = invoice.status === "open"; + const expired = isExpired(invoice.deadline); + + return ( + + +
+ + {invoice.title} + + + {invoice.status} + +
+
+ + +

+ {invoice.description} +

+ +
+ + {invoice.amount.toLocaleString()} {invoice.currency} + + +
+ + +
+
+ ); +} From 671aab5dfbca1c245603cb4be6d091ed804bd065 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:36 +0100 Subject: [PATCH 08/10] Implement marketplace page with filter and revalidation UI Wire up filter bar, debounced search, invoice grid, and background refetch indicator. Show empty state when no invoices match filters. --- app/marketplace/page.tsx | 69 +++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/app/marketplace/page.tsx b/app/marketplace/page.tsx index 8c9cf14..c727b8d 100644 --- a/app/marketplace/page.tsx +++ b/app/marketplace/page.tsx @@ -1,11 +1,13 @@ "use client"; -import { useRef, useCallback, useMemo } from "react"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { fetchInvoices, type Invoice } from "@/lib/api"; import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { MarketplaceFilterBar } from "@/components/marketplace"; +import { Loader2 } from "lucide-react"; function InvoiceRow({ invoice }: { invoice: Invoice }) { return ( @@ -80,12 +82,14 @@ export default function MarketplacePage() { hasNextPage, isFetchingNextPage, isLoading, + isFetching, } = useInfiniteQuery({ queryKey: ["invoices"], queryFn: ({ pageParam }) => fetchInvoices(pageParam as string | undefined), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.has_more ? lastPage.next_cursor ?? undefined : undefined, + staleTime: 60 * 1000, }); const sentinelRef = useRef(null); @@ -115,11 +119,38 @@ export default function MarketplacePage() { [observer] ); - const invoices = useMemo( + const allInvoices = useMemo( () => data?.pages.flatMap((p) => p.invoices) ?? [], [data] ); + const [status, setStatus] = useState<"open" | "funded" | "settled" | "all">("all"); + const [search, setSearch] = useState(""); + const debounceRef = useRef | null>(null); + const [debouncedSearch, setDebouncedSearch] = useState(""); + + const handleSearchChange = useCallback((value: string) => { + setSearch(value); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => setDebouncedSearch(value), 300); + }, []); + + const handleClear = useCallback(() => { + setStatus("all"); + setSearch(""); + setDebouncedSearch(""); + }, []); + + const filtered = useMemo(() => { + return allInvoices.filter((inv) => { + const matchesStatus = status === "all" || inv.status === status; + const matchesSearch = + debouncedSearch === "" || + inv.title.toLowerCase().includes(debouncedSearch.toLowerCase()); + return matchesStatus && matchesSearch; + }); + }, [allInvoices, status, debouncedSearch]); + if (isLoading) { return (
@@ -136,16 +167,38 @@ export default function MarketplacePage() { return (

Invoice Marketplace

-
- {invoices.map((invoice) => ( - - ))} + + + + {isFetching && !isLoading && ( +
+ + Refreshing… +
+ )} + +
+ {filtered.length === 0 ? ( +

+ No invoices match your filters. +

+ ) : ( + filtered.map((invoice) => ( + + )) + )} {isFetchingNextPage && Array.from({ length: 3 }).map((_, i) => ( ))} {hasNextPage &&
} - {!hasNextPage && invoices.length > 0 && ( + {!hasNextPage && filtered.length > 0 && (

No more invoices

From f80f4dd7ff023f32335db1c62fb7986aa1ae816a Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 17:03:41 +0100 Subject: [PATCH 09/10] Add invoice detail page with countdown timer and invest action Display full invoice details with prominent countdown timer, seller info, and invest button that triggers toast notifications. --- app/marketplace/[id]/page.tsx | 94 +++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/app/marketplace/[id]/page.tsx b/app/marketplace/[id]/page.tsx index b324cce..0fc4278 100644 --- a/app/marketplace/[id]/page.tsx +++ b/app/marketplace/[id]/page.tsx @@ -1,13 +1,99 @@ "use client"; import { useParams } from "next/navigation"; -import { InvoiceDetail } from "@/components/invoices"; +import { useQuery } from "@tanstack/react-query"; +import { Loader2, ArrowLeft } from "lucide-react"; +import Link from "next/link"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { CountdownTimer, isExpired } from "@/components/marketplace"; +import { useInvest } from "@/hooks/useInvestments"; +import { fetchInvoice } from "@/lib/api"; + +const statusVariant: Record = { + open: "default", + funded: "secondary", + settled: "outline", +}; export default function InvoiceDetailPage() { const params = useParams<{ id: string }>(); + const investMutation = useInvest(); + + const { data: invoice, isLoading } = useQuery({ + queryKey: ["invoice", params.id], + queryFn: () => fetchInvoice(params.id), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!invoice) { + return ( +
+

Invoice not found.

+
+ ); + } + + const published = invoice.status === "open"; + const expired = isExpired(invoice.deadline); + return ( -
- -
+
+ + + Back to marketplace + + + + +
+ {invoice.title} + + {invoice.status} + +
+
+ +

{invoice.description}

+ +
+ + {invoice.amount.toLocaleString()} {invoice.currency} + + +
+ +
+ Seller: {invoice.sellerName} +
+ + +
+
+
); } From e744c6742aacf72caf7d49a294e4510ad1e27fe4 Mon Sep 17 00:00:00 2001 From: DeFiVC Date: Tue, 28 Jul 2026 18:13:37 +0100 Subject: [PATCH 10/10] Resolve merge conflicts with upstream/dev Adapt components to use upstream's Invoice type from lib/api instead of custom types. Restore upstream's API functions and useInvestMutation. Add countdown timer to InvoiceDetail component. Add invoices list cache invalidation after successful investment. --- app/marketplace/[id]/page.tsx | 94 ++----------------------- components/invoices/InvoiceDetail.tsx | 4 ++ components/marketplace/filter-bar.tsx | 3 +- components/marketplace/invoice-card.tsx | 12 ++-- hooks/useInvestments.ts | 64 ++++++++--------- lib/api/index.ts | 52 ++++++++++---- lib/types.ts | 32 --------- 7 files changed, 84 insertions(+), 177 deletions(-) delete mode 100644 lib/types.ts diff --git a/app/marketplace/[id]/page.tsx b/app/marketplace/[id]/page.tsx index 0fc4278..b324cce 100644 --- a/app/marketplace/[id]/page.tsx +++ b/app/marketplace/[id]/page.tsx @@ -1,99 +1,13 @@ "use client"; import { useParams } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; -import { Loader2, ArrowLeft } from "lucide-react"; -import Link from "next/link"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { CountdownTimer, isExpired } from "@/components/marketplace"; -import { useInvest } from "@/hooks/useInvestments"; -import { fetchInvoice } from "@/lib/api"; - -const statusVariant: Record = { - open: "default", - funded: "secondary", - settled: "outline", -}; +import { InvoiceDetail } from "@/components/invoices"; export default function InvoiceDetailPage() { const params = useParams<{ id: string }>(); - const investMutation = useInvest(); - - const { data: invoice, isLoading } = useQuery({ - queryKey: ["invoice", params.id], - queryFn: () => fetchInvoice(params.id), - }); - - if (isLoading) { - return ( -
- -
- ); - } - - if (!invoice) { - return ( -
-

Invoice not found.

-
- ); - } - - const published = invoice.status === "open"; - const expired = isExpired(invoice.deadline); - return ( -
- - - Back to marketplace - - - - -
- {invoice.title} - - {invoice.status} - -
-
- -

{invoice.description}

- -
- - {invoice.amount.toLocaleString()} {invoice.currency} - - -
- -
- Seller: {invoice.sellerName} -
- - -
-
-
+
+ +
); } diff --git a/components/invoices/InvoiceDetail.tsx b/components/invoices/InvoiceDetail.tsx index 8968ffb..1c2c144 100644 --- a/components/invoices/InvoiceDetail.tsx +++ b/components/invoices/InvoiceDetail.tsx @@ -6,6 +6,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; +import { CountdownTimer, isExpired } from "@/components/marketplace"; function InvoiceDetailSkeleton() { return ( @@ -96,6 +97,8 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) { } const progress = Math.min((invoice.raised / invoice.amount) * 100, 100); + const published = invoice.status === "open"; + const expired = isExpired(invoice.due_date); return (
@@ -108,6 +111,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {

Seller: {invoice.seller}

+ diff --git a/components/marketplace/filter-bar.tsx b/components/marketplace/filter-bar.tsx index 7ff6338..5cba1c8 100644 --- a/components/marketplace/filter-bar.tsx +++ b/components/marketplace/filter-bar.tsx @@ -10,7 +10,8 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import type { InvoiceStatus } from "@/lib/types"; + +type InvoiceStatus = "open" | "funded" | "settled"; interface MarketplaceFilterBarProps { status: InvoiceStatus | "all"; diff --git a/components/marketplace/invoice-card.tsx b/components/marketplace/invoice-card.tsx index 61dde3c..b2850e8 100644 --- a/components/marketplace/invoice-card.tsx +++ b/components/marketplace/invoice-card.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { CountdownTimer, isExpired } from "./countdown-timer"; -import type { Invoice } from "@/lib/types"; +import type { Invoice } from "@/lib/api"; const statusVariant: Record = { open: "default", @@ -17,7 +17,7 @@ interface InvoiceCardProps { export function InvoiceCard({ invoice, onInvest }: InvoiceCardProps) { const published = invoice.status === "open"; - const expired = isExpired(invoice.deadline); + const expired = isExpired(invoice.due_date); return ( @@ -33,15 +33,11 @@ export function InvoiceCard({ invoice, onInvest }: InvoiceCardProps) { -

- {invoice.description} -

-
- {invoice.amount.toLocaleString()} {invoice.currency} + {invoice.amount.toLocaleString()} XLM - +