diff --git a/components/molecules/index.tsx b/components/molecules/index.tsx index 8ad6fde..9597abb 100644 --- a/components/molecules/index.tsx +++ b/components/molecules/index.tsx @@ -4,3 +4,4 @@ export * from './wallet-data' export * from './deposits' export * from './usd-converter' export * from './file-upload' +export * from './milestone-progress' diff --git a/components/molecules/milestone-progress/index.tsx b/components/molecules/milestone-progress/index.tsx new file mode 100644 index 0000000..6806ff9 --- /dev/null +++ b/components/molecules/milestone-progress/index.tsx @@ -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 = { + 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 ( +
+
+
+

+ {title} +

+

{displaySummary}

+
+ + {releasedCount}/{milestones.length} + +
+ +
    + {milestones.map((milestone, index) => ( +
  1. + +
    +
    +
    +

    {milestone.title}

    + {milestone.description && ( +

    {milestone.description}

    + )} +
    +
    + {milestone.amount} + + {STATUS_LABELS[milestone.status]} + +
    +
    + {milestone.dueLabel &&

    {milestone.dueLabel}

    } +
    +
  2. + ))} +
+
+ ) +} diff --git a/components/molecules/milestone-progress/style.module.css b/components/molecules/milestone-progress/style.module.css new file mode 100644 index 0000000..db3d42e --- /dev/null +++ b/components/molecules/milestone-progress/style.module.css @@ -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; + } +} diff --git a/components/organisms/pledge/index.tsx b/components/organisms/pledge/index.tsx index 208e352..dbe9e57 100644 --- a/components/organisms/pledge/index.tsx +++ b/components/organisms/pledge/index.tsx @@ -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' @@ -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 ( @@ -40,6 +67,10 @@ const EscrowPanel: FunctionComponent = () => { +
diff --git a/pages/escrow.tsx b/pages/escrow.tsx new file mode 100644 index 0000000..7fcb5fc --- /dev/null +++ b/pages/escrow.tsx @@ -0,0 +1,36 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import { Navbar, Pledge } from '../components/organisms' + +const Escrow: NextPage = () => { + return ( + <> + + Escrow - TrustFlow + + + +
+ + +
+
+

+ Escrow Milestones +

+

+ Review which payment tranches have been released and which remain locked. +

+
+ + +
+
+ + ) +} + +export default Escrow