Skip to content
Open
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
1 change: 1 addition & 0 deletions components/molecules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './wallet-data'
export * from './deposits'
export * from './usd-converter'
export * from './file-upload'
export * from './milestone-progress'
93 changes: 93 additions & 0 deletions components/molecules/milestone-progress/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react'
import styles from './style.module.css'

export type MilestoneStatus = 'released' | 'current' | 'locked'

export interface MilestoneProgressItem {
id: string
title: string
amount: string
status: MilestoneStatus
description?: string
dueLabel?: string
}

export interface MilestoneProgressProps {
milestones: MilestoneProgressItem[]
title?: string
summary?: string
}

const STATUS_LABELS: Record<MilestoneStatus, string> = {
released: 'Released',
current: 'Locked - in review',
locked: 'Locked',
}

function getStatusClass(status: MilestoneStatus) {
if (status === 'released') return styles.released
if (status === 'current') return styles.current
return styles.locked
}

export function MilestoneProgress({
milestones,
title = 'Milestone release timeline',
summary,
}: MilestoneProgressProps) {
if (milestones.length === 0) {
return null
}

const releasedCount = milestones.filter((milestone) => milestone.status === 'released').length
const displaySummary =
summary ?? `${releasedCount} of ${milestones.length} escrow tranches released`

return (
<section className={styles.wrapper} aria-labelledby="milestone-progress-heading">
<div className={styles.header}>
<div>
<h3 id="milestone-progress-heading" className={styles.title}>
{title}
</h3>
<p className={styles.summary}>{displaySummary}</p>
</div>
<span className={styles.count}>
{releasedCount}/{milestones.length}
</span>
</div>

<ol className={styles.timeline}>
{milestones.map((milestone, index) => (
<li key={milestone.id} className={styles.item}>
<div className={styles.rail} aria-hidden="true">
<span className={`${styles.dot} ${getStatusClass(milestone.status)}`}>
{index + 1}
</span>
{index < milestones.length - 1 && (
<span className={`${styles.line} ${getStatusClass(milestone.status)}`} />
)}
</div>
<div className={styles.card}>
<div className={styles.cardHeader}>
<div>
<h4 className={styles.milestoneTitle}>{milestone.title}</h4>
{milestone.description && (
<p className={styles.description}>{milestone.description}</p>
)}
</div>
<div className={styles.meta}>
<span className={styles.amount}>{milestone.amount}</span>
<span className={`${styles.badge} ${getStatusClass(milestone.status)}`}>
{STATUS_LABELS[milestone.status]}
</span>
</div>
</div>
{milestone.dueLabel && <p className={styles.dueLabel}>{milestone.dueLabel}</p>}
</div>
</li>
))}
</ol>
</section>
)
}
178 changes: 178 additions & 0 deletions components/molecules/milestone-progress/style.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
.wrapper {
margin-top: 1.5rem;
}

.header {
align-items: flex-start;
display: flex;
gap: 1rem;
justify-content: space-between;
margin-bottom: 1rem;
}

.title {
color: #111827;
font-size: 1rem;
font-weight: 700;
line-height: 1.4;
margin: 0;
}

.summary {
color: #6b7280;
font-size: 0.875rem;
line-height: 1.5;
margin: 0.25rem 0 0;
}

.count {
background: #eef2ff;
border: 1px solid #c7d2fe;
border-radius: 999px;
color: #4338ca;
flex: 0 0 auto;
font-size: 0.875rem;
font-weight: 700;
line-height: 1;
padding: 0.5rem 0.625rem;
}

.timeline {
display: grid;
gap: 0;
list-style: none;
margin: 0;
padding: 0;
}

.item {
display: grid;
grid-template-columns: 2.25rem minmax(0, 1fr);
min-height: 4.75rem;
}

.rail {
align-items: center;
display: flex;
flex-direction: column;
}

.dot {
align-items: center;
border-radius: 999px;
display: flex;
font-size: 0.75rem;
font-weight: 800;
height: 2rem;
justify-content: center;
width: 2rem;
}

.line {
flex: 1 1 auto;
margin: 0.25rem 0;
min-height: 1.75rem;
width: 0.25rem;
}

.card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 0.75rem;
margin-bottom: 0.75rem;
padding: 0.875rem;
}

.cardHeader {
align-items: flex-start;
display: flex;
gap: 0.75rem;
justify-content: space-between;
}

.milestoneTitle {
color: #111827;
font-size: 0.9375rem;
font-weight: 700;
line-height: 1.4;
margin: 0;
}

.description {
color: #6b7280;
font-size: 0.8125rem;
line-height: 1.5;
margin: 0.25rem 0 0;
}

.meta {
align-items: flex-end;
display: flex;
flex-direction: column;
flex-shrink: 0;
gap: 0.375rem;
}

.amount {
color: #111827;
font-size: 0.875rem;
font-weight: 800;
white-space: nowrap;
}

.badge {
border-radius: 999px;
font-size: 0.75rem;
font-weight: 700;
line-height: 1;
padding: 0.375rem 0.5rem;
white-space: nowrap;
}

.dueLabel {
color: #6b7280;
font-size: 0.75rem;
line-height: 1.4;
margin: 0.5rem 0 0;
}

.released {
background: #dcfce7;
border-color: #86efac;
color: #166534;
}

.current {
background: #fef3c7;
border-color: #f59e0b;
color: #92400e;
}

.locked {
background: #e5e7eb;
border-color: #d1d5db;
color: #374151;
}

.line.released {
background: #22c55e;
}

.line.current {
background: #f59e0b;
}

.line.locked {
background: #d1d5db;
}

@media (max-width: 640px) {
.header,
.cardHeader {
flex-direction: column;
}

.meta {
align-items: flex-start;
}
}
33 changes: 32 additions & 1 deletion components/organisms/pledge/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { FunctionComponent } from 'react'
import { Card, ConnectButton, Loading, ProgressBar } from '../../atoms'
import { MilestoneProgress, type MilestoneProgressItem } from '../../molecules'
import { Spacer } from '../../atoms/spacer'
import { Utils } from '../../../shared/utils'
import { useAccount } from '../../../hooks'
Expand All @@ -21,9 +22,35 @@ const EscrowPanel: FunctionComponent = () => {
symbol: 'XLM',
deadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
loaded: true,
milestones: [
{
id: 'brief',
title: 'Project brief approved',
amount: '150 XLM',
status: 'released',
description: 'Client approved the brief and the first tranche was released.',
dueLabel: 'Released after kickoff approval',
},
{
id: 'build',
title: 'Implementation review',
amount: '500 XLM',
status: 'current',
description: 'Funds remain locked until the active deliverable is accepted.',
dueLabel: 'Current escrow checkpoint',
},
{
id: 'handoff',
title: 'Final handoff',
amount: '350 XLM',
status: 'locked',
description: 'Final tranche stays locked until completion is confirmed.',
dueLabel: 'Queued after implementation review',
},
] satisfies MilestoneProgressItem[],
}

const { balance, target, decimals, symbol, deadline, loaded } = mockEscrow
const { balance, target, decimals, symbol, deadline, loaded, milestones } = mockEscrow

return (
<Card>
Expand All @@ -40,6 +67,10 @@ const EscrowPanel: FunctionComponent = () => {
</span>
<Spacer rem={1} />
<ProgressBar value={Utils.percentage(balance, target, decimals)} />
<MilestoneProgress
milestones={milestones}
summary="1 tranche released; 2 tranches remain locked in escrow."
/>
<Spacer rem={1.5} />
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>
Expand Down
36 changes: 36 additions & 0 deletions pages/escrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { NextPage } from 'next'
import Head from 'next/head'
import { Navbar, Pledge } from '../components/organisms'

const Escrow: NextPage = () => {
return (
<>
<Head>
<title>Escrow - TrustFlow</title>
<meta
name="description"
content="Track locked and released escrow milestones on TrustFlow"
/>
</Head>

<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<Navbar />

<main className="max-w-4xl mx-auto px-6 py-8">
<div className="mb-8">
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-2">
Escrow Milestones
</h1>
<p className="text-gray-600 dark:text-gray-400">
Review which payment tranches have been released and which remain locked.
</p>
</div>

<Pledge />
</main>
</div>
</>
)
}

export default Escrow