chore: modularize ui components

This commit is contained in:
Jesse Egbosionu
2025-12-01 22:57:12 +01:00
parent 5c22b9b844
commit c460ea27d4
8 changed files with 270 additions and 216 deletions
+12
View File
@@ -0,0 +1,12 @@
export default function Footer() {
return (
<footer className="footer">
<div className="footer-content">
<p>
Built with <span className="heart">❤️</span> and Amara Chukwu
</p>
<p className="footer-year">© {new Date().getFullYear()}</p>
</div>
</footer>
);
}
+32
View File
@@ -0,0 +1,32 @@
import Link from 'next/link';
export default function Header({ showNavLinks = true }) {
return (
<header className="header">
<nav className="navbar">
<div className="logo">
<Link href="/">
<span className="logo-text">JE</span>
</Link>
</div>
{showNavLinks && (
<div className="nav-links">
<Link href="/" className="nav-link">
Home
</Link>
<Link href="/rates" className="nav-link cta">
Rates
</Link>
</div>
)}
{!showNavLinks && (
<div className="nav-links">
<a href="mailto:me@jesseinit.dev" className="nav-link cta">
Get in touch
</a>
</div>
)}
</nav>
</header>
);
}
+24
View File
@@ -0,0 +1,24 @@
import Head from 'next/head';
import Header from './Header';
import Footer from './Footer';
export default function Layout({ children, title, description, showNavLinks = true }) {
return (
<div className="container">
<Head>
<title>{title || 'Jesse Egbosionu - Software Engineer'}</title>
<link rel="icon" href="/favicon.ico" />
<meta
name="description"
content={description || 'Software engineer based in Amsterdam, focused on building scalable backend systems.'}
/>
</Head>
<Header showNavLinks={showNavLinks} />
<main className="main-content">{children}</main>
<Footer />
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
export default function RateCard({ provider, rate, href, isBest, index, formatCurrency }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={`rate-card ${isBest ? 'best' : ''}`}
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="rate-card-header">
<h3 className="provider-name">{provider}</h3>
{isBest && <span className="best-badge">Best Rate</span>}
</div>
<div className="rate-value">{formatCurrency(rate)}</div>
<div className="rate-card-footer">
<span className="visit-link">Visit provider →</span>
</div>
</a>
);
}
+48
View File
@@ -0,0 +1,48 @@
const socialData = [
{
name: 'GitHub',
url: 'https://github.com/jesseinit',
icon: (
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
),
},
{
name: 'LinkedIn',
url: 'https://linkedin.com/in/jesse-egbosionu/',
icon: (
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
),
},
{
name: 'Twitter',
url: 'https://twitter.com/jesseinit/',
icon: (
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
),
},
];
export default function SocialLinks() {
return (
<div className="social-links">
{socialData.map((social) => (
<a
key={social.name}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="social-link"
>
{social.icon}
{social.name}
</a>
))}
</div>
);
}
+37 -81
View File
@@ -1,94 +1,50 @@
import Head from 'next/head'; import Layout from '../components/Layout';
import SocialLinks from '../components/SocialLinks';
export default function Home() { export default function Home() {
return ( return (
<div className="container"> <Layout showNavLinks={false}>
<Head> <section className="hero">
<title>Jesse Egbosionu - Software Engineer</title> <div className="hero-content">
<link rel="icon" href="/favicon.ico" /> <h1 className="hero-title">
<meta name="description" content="Software engineer based in Amsterdam, focused on building scalable backend systems." /> Hey, I'm <span className="highlight">Jesse Egbosionu</span>
</Head> </h1>
<header className="header"> <p className="hero-subtitle">
<nav className="navbar"> Software Engineer crafting scalable backend systems in Amsterdam
<div className="logo"> </p>
<span className="logo-text">JE</span>
</div>
<div className="nav-links">
<a href="mailto:me@jesseinit.dev" className="nav-link cta">Get in touch</a>
</div>
</nav>
</header>
<main className="main-content"> <div className="hero-description">
<section className="hero"> <p>
<div className="hero-content"> I build and scale software backends with a focus on creating impact through
<h1 className="hero-title"> teamwork and continuous learning. My passion lies in solving complex problems
Hey, I'm <span className="highlight">Jesse Egbosionu</span> and delivering solutions that enrich people's lives.
</h1>
<p className="hero-subtitle">
Software Engineer crafting scalable backend systems in Amsterdam
</p> </p>
<div className="hero-description"> <p>
<p> Over the past 6 years, I've shipped products across <strong>finance</strong>, <strong>ecommerce</strong>,
I build and scale software backends with a focus on creating impact through and <strong>logistics</strong>, working with technologies like Python (Django, FastAPI),
teamwork and continuous learning. My passion lies in solving complex problems JavaScript (React, Node.js), PostgreSQL, Docker, Redis, and Kubernetes.
and delivering solutions that enrich people's lives. </p>
</p>
<p>
Over the past 6 years, I've shipped products across <strong>finance</strong>, <strong>ecommerce</strong>,
and <strong>logistics</strong>, working with technologies like Python (Django, FastAPI),
JavaScript (React, Node.js), PostgreSQL, Docker, Redis, and Kubernetes.
</p>
</div>
<div className="social-links">
<a href="https://github.com/jesseinit" target="_blank" rel="noopener noreferrer" className="social-link">
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitHub
</a>
<a href="https://linkedin.com/in/jesse-egbosionu/" target="_blank" rel="noopener noreferrer" className="social-link">
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z" />
</svg>
LinkedIn
</a>
<a href="https://twitter.com/jesseinit/" target="_blank" rel="noopener noreferrer" className="social-link">
<svg className="social-icon" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
Twitter
</a>
</div>
<a href="mailto:me@jesseinit.dev" className="primary-cta">
<span>Let's work together</span>
<span className="arrow">→</span>
</a>
</div> </div>
<div className="hero-image-wrapper"> <SocialLinks />
<div className="image-backdrop"></div>
<img
src="hero.jpg"
alt="Jesse Egbosionu"
className="hero-image"
/>
</div>
</section>
</main>
<footer className="footer"> <a href="mailto:me@jesseinit.dev" className="primary-cta">
<div className="footer-content"> <span>Let's work together</span>
<p>Built with <span className="heart">❤️</span> and Amara Chukwu</p> <span className="arrow">→</span>
<p className="footer-year">© {new Date().getFullYear()}</p> </a>
</div> </div>
</footer>
</div> <div className="hero-image-wrapper">
<div className="image-backdrop"></div>
<img
src="hero.jpg"
alt="Jesse Egbosionu"
className="hero-image"
/>
</div>
</section>
</Layout>
); );
} }
+78 -118
View File
@@ -1,5 +1,6 @@
import Head from "next/head";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Layout from "../components/Layout";
import RateCard from "../components/RateCard";
import { fetchData } from "../utils/api"; import { fetchData } from "../utils/api";
export default function Rates() { export default function Rates() {
@@ -47,136 +48,95 @@ export default function Rates() {
}) : null; }) : null;
return ( return (
<div className="container"> <Layout
<Head> title="EUR to NGN Exchange Rates - Jesse Egbosionu"
<title>EUR to NGN Exchange Rates - Jesse Egbosionu</title> description="Compare live EUR to NGN exchange rates across multiple providers."
<link rel="icon" href="/favicon.ico" /> showNavLinks={true}
<meta name="description" content="Compare live EUR to NGN exchange rates across multiple providers." /> >
</Head> <section className="rates-section">
<div className="rates-header">
<header className="header"> <div className="rates-title-group">
<nav className="navbar"> <h1 className="rates-title">Exchange Rates</h1>
<div className="logo"> <p className="rates-subtitle">
<a href="/"> Compare live EUR to NGN rates across providers
<span className="logo-text">JE</span> </p>
</a>
</div> </div>
<div className="nav-links">
<a href="/" className="nav-link">Home</a>
<a href="/rates" className="nav-link cta">Rates</a>
</div>
</nav>
</header>
<main className="main-content"> <div className="currency-display">
<section className="rates-section"> <div className="currency-badge">
<div className="rates-header"> <span className="currency-from">€1 EUR</span>
<div className="rates-title-group"> <span className="currency-arrow">→</span>
<h1 className="rates-title">Exchange Rates</h1> <span className="currency-to">NGN</span>
<p className="rates-subtitle">
Compare live EUR to NGN rates across providers
</p>
</div> </div>
</div>
</div>
<div className="currency-display"> {loading && (
<div className="currency-badge"> <div className="rates-grid">
<span className="currency-from">€1 EUR</span> {[...Array(6)].map((_, index) => (
<span className="currency-arrow">→</span> <div key={index} className="rate-card skeleton">
<span className="currency-to">NGN</span> <div className="skeleton-header"></div>
<div className="skeleton-rate"></div>
<div className="skeleton-badge"></div>
</div> </div>
</div> ))}
</div> </div>
)}
{loading && ( {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 && (
<>
<div className="rates-grid"> <div className="rates-grid">
{[...Array(6)].map((_, index) => ( {sortedData.map((item, index) => (
<div key={index} className="rate-card skeleton"> <RateCard
<div className="skeleton-header"></div> key={item.provider}
<div className="skeleton-rate"></div> provider={item.provider}
<div className="skeleton-badge"></div> rate={item.rate}
</div> href={item.href}
isBest={item.bestRate || item.rate === bestRate}
index={index}
formatCurrency={formatCurrency}
/>
))} ))}
</div> </div>
)}
{error && ( <div className="rates-info">
<div className="error-state"> <p className="info-text">
<div className="error-icon">⚠️</div> Rates are updated regularly. Click on a provider to visit their website.
<h3 className="error-title">Unable to load rates</h3> </p>
<p className="error-message">{error}</p> <p className="info-timestamp">
<button Last updated: {new Date().toLocaleString('en-GB', {
onClick={() => window.location.reload()} dateStyle: 'medium',
className="retry-button" timeStyle: 'short'
> })}
Try again
</button>
</div>
)}
{!loading && !error && sortedData && sortedData.length > 0 && (
<>
<div className="rates-grid">
{sortedData.map((item, index) => (
<a
key={item.provider}
href={item.href}
target="_blank"
rel="noopener noreferrer"
className={`rate-card ${item.bestRate || item.rate === bestRate ? 'best' : ''}`}
style={{ animationDelay: `${index * 0.1}s` }}
>
<div className="rate-card-header">
<h3 className="provider-name">{item.provider}</h3>
{(item.bestRate || item.rate === bestRate) && (
<span className="best-badge">Best Rate</span>
)}
</div>
<div className="rate-value">
{formatCurrency(item.rate)}
</div>
<div className="rate-card-footer">
<span className="visit-link">
Visit provider →
</span>
</div>
</a>
))}
</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'
})}
</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> </p>
</div> </div>
)} </>
</section> )}
</main>
<footer className="footer"> {!loading && !error && (!data || data.length === 0) && (
<div className="footer-content"> <div className="empty-state">
<p>Built with <span className="heart">❤️</span> and Amara Chukwu</p> <div className="empty-icon">📊</div>
<p className="footer-year">© {new Date().getFullYear()}</p> <h3 className="empty-title">No rates available</h3>
</div> <p className="empty-message">
</footer> We couldn't find any exchange rates at the moment.
</div> </p>
</div>
)}
</section>
</Layout>
); );
} }
+17 -17
View File
@@ -81,7 +81,7 @@ strong {
// HEADER // HEADER
.header { .header {
padding: var(--spacing-lg) 0; padding: var(--spacing-lg) 0;
border-bottom: 2px solid var(--color-primary); border-bottom: 1px solid var(--color-primary);
background: rgba(255, 254, 240, 0.95); background: rgba(255, 254, 240, 0.95);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
position: sticky; position: sticky;
@@ -105,7 +105,7 @@ strong {
background: var(--color-accent); background: var(--color-accent);
color: var(--color-primary); color: var(--color-primary);
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border: 2px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
} }
@@ -132,7 +132,7 @@ strong {
&.cta { &.cta {
color: var(--color-primary); color: var(--color-primary);
background: var(--color-accent); background: var(--color-accent);
border: 2px solid var(--color-primary); border: 1px solid var(--color-primary);
&:hover { &:hover {
background: var(--color-accent-hover); background: var(--color-accent-hover);
@@ -175,7 +175,7 @@ strong {
background: var(--color-accent); background: var(--color-accent);
color: var(--color-primary); color: var(--color-primary);
padding: 0 0.5rem; padding: 0 0.5rem;
border-bottom: 4px solid var(--color-primary); border-bottom: 2px solid var(--color-primary);
} }
} }
@@ -215,7 +215,7 @@ strong {
gap: var(--spacing-xs); gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-md); padding: var(--spacing-xs) var(--spacing-md);
background: var(--color-background); background: var(--color-background);
border: 2px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 600; font-weight: 600;
@@ -253,7 +253,7 @@ strong {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeInUp 0.6s ease 0.4s forwards; animation: fadeInUp 0.6s ease 0.4s forwards;
opacity: 0; opacity: 0;
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
&:hover { &:hover {
background: var(--color-accent); background: var(--color-accent);
@@ -289,7 +289,7 @@ strong {
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
transform: rotate(-6deg); transform: rotate(-6deg);
z-index: -1; z-index: -1;
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
} }
.hero-image { .hero-image {
@@ -300,7 +300,7 @@ strong {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
object-fit: cover; object-fit: cover;
transition: transform 0.3s ease; transition: transform 0.3s ease;
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
&:hover { &:hover {
transform: scale(1.02) rotate(2deg); transform: scale(1.02) rotate(2deg);
@@ -309,7 +309,7 @@ strong {
// FOOTER // FOOTER
.footer { .footer {
border-top: 2px solid var(--color-primary); border-top: 1px solid var(--color-primary);
padding: var(--spacing-lg) 0; padding: var(--spacing-lg) 0;
margin-top: auto; margin-top: auto;
} }
@@ -393,7 +393,7 @@ strong {
gap: var(--spacing-lg); gap: var(--spacing-lg);
text-align: center; text-align: center;
padding-bottom: var(--spacing-lg); padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--color-primary); border-bottom: 1px solid var(--color-primary);
} }
.rates-title-group { .rates-title-group {
@@ -429,7 +429,7 @@ strong {
gap: var(--spacing-md); gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg); padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-accent); background: var(--color-accent);
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
font-weight: 700; font-weight: 700;
font-size: 1.125rem; font-size: 1.125rem;
@@ -461,7 +461,7 @@ strong {
gap: var(--spacing-md); gap: var(--spacing-md);
padding: var(--spacing-lg); padding: var(--spacing-lg);
background: var(--color-background); background: var(--color-background);
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
transition: all 0.3s ease; transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
@@ -480,7 +480,7 @@ strong {
} }
&.best { &.best {
border-width: 4px; border-width: 2px;
background: var(--color-accent); background: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative; position: relative;
@@ -492,7 +492,7 @@ strong {
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
height: 6px; height: 3px;
background: var(--color-primary); background: var(--color-primary);
} }
@@ -537,7 +537,7 @@ strong {
letter-spacing: 0.75px; letter-spacing: 0.75px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
animation: badgePulse 2s ease-in-out infinite; animation: badgePulse 2s ease-in-out infinite;
border: 2px solid var(--color-primary); border: 1px solid var(--color-primary);
&::before { &::before {
content: '★'; content: '★';
@@ -598,7 +598,7 @@ strong {
gap: var(--spacing-xs); gap: var(--spacing-xs);
padding: var(--spacing-lg); padding: var(--spacing-lg);
background: var(--color-surface); background: var(--color-surface);
border: 2px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
text-align: center; text-align: center;
animation: fadeInUp 0.6s ease 0.4s forwards; animation: fadeInUp 0.6s ease 0.4s forwards;
@@ -700,7 +700,7 @@ strong {
padding: var(--spacing-sm) var(--spacing-lg); padding: var(--spacing-sm) var(--spacing-lg);
background: var(--color-primary); background: var(--color-primary);
color: var(--color-accent); color: var(--color-accent);
border: 3px solid var(--color-primary); border: 1px solid var(--color-primary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;