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
100 changes: 99 additions & 1 deletion src/components/QuoteCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,50 @@
border: 1px solid #1f2a3a;
border-radius: 12px;
padding: 1.25rem 1.5rem;
transition: border-color 0.2s;
}

.quote-card--expired {
border-color: var(--color-error, #e53935);
opacity: 0.75;
}

.quote-title {
margin: 0 0 1rem;
font-size: 1rem;
}

/* Currency corridor: "USD (🇺🇸 US Dollar) → NGN (🇳🇬 Nigerian Naira)" */
.quote-corridor {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
margin-bottom: 1rem;
font-size: 0.85rem;
color: var(--color-muted);
}

.quote-currency {
display: flex;
align-items: center;
gap: 0.25rem;
}

.quote-flag {
font-size: 1.1em;
}

.quote-code {
color: var(--color-muted);
font-size: 0.8em;
}

.quote-corridor-arrow {
font-size: 0.9rem;
}

/* Main quote lines */
.quote-line {
display: flex;
justify-content: space-between;
Expand All @@ -32,13 +69,66 @@
font-size: 1.1rem;
}

/* Metadata row: source · timestamp · expiry */
.quote-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 0.75rem;
font-size: 0.75rem;
color: var(--color-muted);
}

.quote-source {
font-style: italic;
}

.quote-timestamp {
/* no extra style needed */
}

.quote-expiry {
margin-left: auto;
font-weight: 500;
}

.quote-expiry--soon {
color: var(--color-warning, #f59e0b);
font-weight: 600;
}

.quote-expiry--expired {
color: var(--color-error, #e53935);
font-weight: 700;
}

/* Footer note */
.quote-note {
margin: 1rem 0 0;
margin: 0.75rem 0 0;
font-size: 0.8rem;
line-height: 1.4;
color: var(--color-muted);
}

/* Quote expired / warning banners (rendered in SendMoney, not QuoteCard) */
.send-quote-expired {
font-size: 0.875rem;
color: var(--color-error, #e53935);
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--color-error, #e53935) 10%, transparent);
border-radius: 6px;
margin-bottom: 0.75rem;
}

.send-quote-warning {
font-size: 0.875rem;
color: var(--color-warning, #f59e0b);
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, var(--color-warning, #f59e0b) 10%, transparent);
border-radius: 6px;
margin-bottom: 0.75rem;
}

@media (prefers-contrast: more) {
.quote-card {
border-width: 2px;
Expand All @@ -47,4 +137,12 @@
.quote-divider {
height: 2px;
}

.quote-expiry--soon {
text-decoration: underline;
}

.quote-expiry--expired {
text-decoration: underline;
}
}
97 changes: 92 additions & 5 deletions src/components/QuoteCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,82 @@ import './QuoteCard.css';

/**
* Displays the breakdown of an FX quote: rate, fee and amount received.
*
* In addition to the core send/fee/receive fields, it shows:
* - The currency flags and full names for both source and destination
* - The quote source (provider tag)
* - A human-readable timestamp ("obtained at HH:MM:SS")
* - The remaining time until the quote expires
*
* @param {object} props
* @param {object} props.quote - quote object from buildQuote()
* @param {object} props.quote - enhanced quote object from buildQuote()
* @param {string} [props.locale] - locale used for currency formatting
* @param {number|null} [props.secsLeft] - seconds until the quote expires
*/
export default function QuoteCard({ quote, locale = DEFAULT_LOCALE }) {
export default function QuoteCard({ quote, locale = DEFAULT_LOCALE, secsLeft = null }) {
if (!quote) return null;

const { from, to, rate, sendAmount, fee, receiveAmount } = quote;
const {
from,
to,
fromMeta,
toMeta,
rate,
sendAmount,
fee,
receiveAmount,
source,
timestamp,
} = quote;

// Format the timestamp for display (e.g. "14:03:27").
const obtainedAt = timestamp
? new Date(timestamp).toLocaleTimeString(locale, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
: null;

// Determine expiry display state.
const isExpired = secsLeft !== null && secsLeft <= 0;
const isExpiringSoon = !isExpired && secsLeft !== null && secsLeft <= 10;

// Build a descriptive expiry label.
let expiryLabel = null;
if (isExpired) {
expiryLabel = 'Expired';
} else if (secsLeft !== null) {
expiryLabel = `${secsLeft}s`;
}

return (
<div className="quote-card">
<div className={`quote-card${isExpired ? ' quote-card--expired' : ''}`}>
<h3 className="quote-title">Transfer summary</h3>

{/* Currency corridor header */}
<div className="quote-corridor">
<span className="quote-currency">
{fromMeta?.flag && (
<span className="quote-flag" aria-hidden="true">
{fromMeta.flag}
</span>
)}
<span>{fromMeta?.name ?? from}</span>
<span className="quote-code">({from})</span>
</span>
<span className="quote-corridor-arrow" aria-hidden="true">→</span>
<span className="quote-currency">
{toMeta?.flag && (
<span className="quote-flag" aria-hidden="true">
{toMeta.flag}
</span>
)}
<span>{toMeta?.name ?? to}</span>
<span className="quote-code">({to})</span>
</span>
</div>

<div className="quote-line">
<span>You send</span>
<span>{formatAmount(sendAmount, from, locale)}</span>
Expand All @@ -40,9 +103,33 @@ export default function QuoteCard({ quote, locale = DEFAULT_LOCALE }) {
<span>{formatAmount(receiveAmount, to, locale)}</span>
</div>

{/* Quote metadata footer */}
<div className="quote-meta">
{source && (
<span className="quote-source" title="Quote provider">
{source}
</span>
)}
{obtainedAt && (
<span className="quote-timestamp" title="Quote obtained at">
obtained {obtainedAt}
</span>
)}
{expiryLabel && (
<span
className={`quote-expiry${isExpired ? ' quote-expiry--expired' : isExpiringSoon ? ' quote-expiry--soon' : ''}`}
aria-live="polite"
aria-atomic="true"
title="Time until quote expires"
>
{isExpired ? 'Quote expired' : `expires in ${expiryLabel}`}
</span>
)}
</div>

<p className="quote-note">
Fees cover the RemitFlow service and Stellar network cost. Rates are
indicative and update at confirmation.
indicative and locked for the duration shown above.
</p>
</div>
);
Expand Down
Loading