feat: redesign site as a modern developer portfolio
- New home page: hero, stats, about, focus areas, stack, projects, contact - Light/dark theme with system default and persisted toggle - Content centralised in data/profile.js - Restyle /rates to match; remove it from nav and projects - Afriex: log a clear error now that its API requires a device id Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5c40a64c42
commit
3bb0a5e4a9
+75
-75
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Layout from "../components/Layout";
|
||||
import RateCard from "../components/RateCard";
|
||||
import Icon from "../components/Icon";
|
||||
import { fetchData } from "../utils/api";
|
||||
|
||||
export default function Rates() {
|
||||
@@ -49,94 +50,93 @@ export default function Rates() {
|
||||
|
||||
return (
|
||||
<Layout
|
||||
title="EUR to NGN Exchange Rates - Jesse Egbosionu"
|
||||
title="EUR → NGN Exchange Rates · Jesse Egbosionu"
|
||||
description="Compare live EUR to NGN exchange rates across multiple providers."
|
||||
showNavLinks={false}
|
||||
>
|
||||
<section className="rates-section">
|
||||
<div className="rates-header">
|
||||
<div className="rates-title-group">
|
||||
<h1 className="rates-title">Exchange Rates</h1>
|
||||
<p className="rates-subtitle">
|
||||
Compare live EUR to NGN rates across providers
|
||||
</p>
|
||||
</div>
|
||||
<section className="section rates-section">
|
||||
<div className="shell">
|
||||
<div className="rates-header">
|
||||
<div className="section-heading">
|
||||
<p className="eyebrow">
|
||||
<span className="live-dot" />
|
||||
Live tool
|
||||
</p>
|
||||
<h1 className="section-title">Exchange rates</h1>
|
||||
<p className="section-lead">
|
||||
Compare live EUR → NGN rates across remittance providers, fetched in real time.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="currency-display">
|
||||
<div className="currency-badge">
|
||||
<span className="currency-from">€1 EUR</span>
|
||||
<span className="currency-arrow">→</span>
|
||||
<span className="currency-to">NGN</span>
|
||||
<span>€1 EUR</span>
|
||||
<Icon name="arrowRight" size={16} />
|
||||
<span>NGN</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div className="rates-grid">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="rate-card skeleton">
|
||||
<div className="skeleton-header"></div>
|
||||
<div className="skeleton-rate"></div>
|
||||
<div className="skeleton-badge"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-state">
|
||||
<div className="error-icon">⚠️</div>
|
||||
<h3 className="error-title">Unable to load rates</h3>
|
||||
<p className="error-message">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="retry-button"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && sortedData && sortedData.length > 0 && (
|
||||
<>
|
||||
{loading && (
|
||||
<div className="rates-grid">
|
||||
{sortedData.map((item, index) => (
|
||||
<RateCard
|
||||
key={item.provider}
|
||||
provider={item.provider}
|
||||
rate={item.rate}
|
||||
href={item.href}
|
||||
isBest={item.bestRate || item.rate === bestRate}
|
||||
index={index}
|
||||
formatCurrency={formatCurrency}
|
||||
logo={item.logo}
|
||||
/>
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="rate-card skeleton">
|
||||
<div className="skeleton-line skeleton-header"></div>
|
||||
<div className="skeleton-line skeleton-rate"></div>
|
||||
<div className="skeleton-line skeleton-badge"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rates-info">
|
||||
<p className="info-text">
|
||||
Rates are updated regularly. Click on a provider to visit their website.
|
||||
</p>
|
||||
<p className="info-timestamp">
|
||||
Last updated: {new Date().toLocaleString('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
})}
|
||||
{error && (
|
||||
<div className="state-card">
|
||||
<h3 className="state-title">Unable to load rates</h3>
|
||||
<p className="state-message">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="button button-primary"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && sortedData && sortedData.length > 0 && (
|
||||
<>
|
||||
<div className="rates-grid">
|
||||
{sortedData.map((item, index) => (
|
||||
<RateCard
|
||||
key={item.provider}
|
||||
provider={item.provider}
|
||||
rate={item.rate}
|
||||
href={item.href}
|
||||
isBest={item.bestRate || item.rate === bestRate}
|
||||
index={index}
|
||||
formatCurrency={formatCurrency}
|
||||
logo={item.logo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rates-info">
|
||||
<p>Rates are fetched live on each visit. Click a provider to visit their website.</p>
|
||||
<p className="rates-timestamp">
|
||||
Last updated {new Date().toLocaleString('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && !error && (!data || data.length === 0) && (
|
||||
<div className="state-card">
|
||||
<h3 className="state-title">No rates available</h3>
|
||||
<p className="state-message">
|
||||
We couldn't find any exchange rates at the moment.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading && !error && (!data || data.length === 0) && (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">📊</div>
|
||||
<h3 className="empty-title">No rates available</h3>
|
||||
<p className="empty-message">
|
||||
We couldn't find any exchange rates at the moment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user