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>
);
}
+5 -49
View File
@@ -1,26 +1,9 @@
import Head from 'next/head';
import Layout from '../components/Layout';
import SocialLinks from '../components/SocialLinks';
export default function Home() {
return (
<div className="container">
<Head>
<title>Jesse Egbosionu - Software Engineer</title>
<link rel="icon" href="/favicon.ico" />
<meta name="description" content="Software engineer based in Amsterdam, focused on building scalable backend systems." />
</Head>
<header className="header">
<nav className="navbar">
<div className="logo">
<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">
<Layout showNavLinks={false}>
<section className="hero">
<div className="hero-content">
<h1 className="hero-title">
@@ -45,26 +28,7 @@ export default function Home() {
</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>
<SocialLinks />
<a href="mailto:me@jesseinit.dev" className="primary-cta">
<span>Let's work together</span>
@@ -81,14 +45,6 @@ export default function Home() {
/>
</div>
</section>
</main>
<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>
</div>
</Layout>
);
}
+15 -55
View File
@@ -1,5 +1,6 @@
import Head from "next/head";
import { useEffect, useState } from "react";
import Layout from "../components/Layout";
import RateCard from "../components/RateCard";
import { fetchData } from "../utils/api";
export default function Rates() {
@@ -47,28 +48,11 @@ export default function Rates() {
}) : null;
return (
<div className="container">
<Head>
<title>EUR to NGN Exchange Rates - Jesse Egbosionu</title>
<link rel="icon" href="/favicon.ico" />
<meta name="description" content="Compare live EUR to NGN exchange rates across multiple providers." />
</Head>
<header className="header">
<nav className="navbar">
<div className="logo">
<a href="/">
<span className="logo-text">JE</span>
</a>
</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">
<Layout
title="EUR to NGN Exchange Rates - Jesse Egbosionu"
description="Compare live EUR to NGN exchange rates across multiple providers."
showNavLinks={true}
>
<section className="rates-section">
<div className="rates-header">
<div className="rates-title-group">
@@ -117,31 +101,15 @@ export default function Rates() {
<>
<div className="rates-grid">
{sortedData.map((item, index) => (
<a
<RateCard
key={item.provider}
provider={item.provider}
rate={item.rate}
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>
isBest={item.bestRate || item.rate === bestRate}
index={index}
formatCurrency={formatCurrency}
/>
))}
</div>
@@ -169,14 +137,6 @@ export default function Rates() {
</div>
)}
</section>
</main>
<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>
</div>
</Layout>
);
}
+17 -17
View File
@@ -81,7 +81,7 @@ strong {
// HEADER
.header {
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);
backdrop-filter: blur(10px);
position: sticky;
@@ -105,7 +105,7 @@ strong {
background: var(--color-accent);
color: var(--color-primary);
padding: 0.25rem 0.75rem;
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-sm);
}
}
@@ -132,7 +132,7 @@ strong {
&.cta {
color: var(--color-primary);
background: var(--color-accent);
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
&:hover {
background: var(--color-accent-hover);
@@ -175,7 +175,7 @@ strong {
background: var(--color-accent);
color: var(--color-primary);
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);
padding: var(--spacing-xs) var(--spacing-md);
background: var(--color-background);
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-sm);
font-size: 0.9375rem;
font-weight: 600;
@@ -253,7 +253,7 @@ strong {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeInUp 0.6s ease 0.4s forwards;
opacity: 0;
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
&:hover {
background: var(--color-accent);
@@ -289,7 +289,7 @@ strong {
border-radius: var(--radius-lg);
transform: rotate(-6deg);
z-index: -1;
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
}
.hero-image {
@@ -300,7 +300,7 @@ strong {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
object-fit: cover;
transition: transform 0.3s ease;
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
&:hover {
transform: scale(1.02) rotate(2deg);
@@ -309,7 +309,7 @@ strong {
// FOOTER
.footer {
border-top: 2px solid var(--color-primary);
border-top: 1px solid var(--color-primary);
padding: var(--spacing-lg) 0;
margin-top: auto;
}
@@ -393,7 +393,7 @@ strong {
gap: var(--spacing-lg);
text-align: center;
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--color-primary);
border-bottom: 1px solid var(--color-primary);
}
.rates-title-group {
@@ -429,7 +429,7 @@ strong {
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-accent);
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-lg);
font-weight: 700;
font-size: 1.125rem;
@@ -461,7 +461,7 @@ strong {
gap: var(--spacing-md);
padding: var(--spacing-lg);
background: var(--color-background);
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-lg);
transition: all 0.3s ease;
cursor: pointer;
@@ -480,7 +480,7 @@ strong {
}
&.best {
border-width: 4px;
border-width: 2px;
background: var(--color-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
position: relative;
@@ -492,7 +492,7 @@ strong {
top: 0;
left: 0;
right: 0;
height: 6px;
height: 3px;
background: var(--color-primary);
}
@@ -537,7 +537,7 @@ strong {
letter-spacing: 0.75px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
animation: badgePulse 2s ease-in-out infinite;
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
&::before {
content: '★';
@@ -598,7 +598,7 @@ strong {
gap: var(--spacing-xs);
padding: var(--spacing-lg);
background: var(--color-surface);
border: 2px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-lg);
text-align: center;
animation: fadeInUp 0.6s ease 0.4s forwards;
@@ -700,7 +700,7 @@ strong {
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--color-primary);
color: var(--color-accent);
border: 3px solid var(--color-primary);
border: 1px solid var(--color-primary);
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 600;