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
@@ -0,0 +1,19 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
// Set the theme before first paint to avoid a light/dark flash.
|
||||
const themeScript = `(function(){try{var t=localStorage.getItem('theme');if(!t){t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}document.documentElement.dataset.theme=t}catch(e){}})();`;
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<link rel="preload" href="/fonts/inter_font.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
||||
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -240,7 +240,11 @@ export default async (req, res) => {
|
||||
.catch((error) => reject(error));
|
||||
})
|
||||
.then((result) => {
|
||||
const ngnRate = result.rates.EUR.NGN;
|
||||
// Afriex now rejects unauthenticated requests with {"message":"Device id missing"}
|
||||
const ngnRate = result?.rates?.EUR?.NGN;
|
||||
if (!ngnRate) {
|
||||
throw new Error(`Unexpected response: ${result?.message || JSON.stringify(result).slice(0, 200)}`);
|
||||
}
|
||||
return {
|
||||
rate: parseFloat(ngnRate).toFixed(2),
|
||||
provider: "Afriex",
|
||||
@@ -250,7 +254,7 @@ export default async (req, res) => {
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error calling Afriex:", error);
|
||||
console.error("Error calling Afriex:", error.message);
|
||||
return createDefaultErrorResponse("Afriex", "https://www.afriex.com/");
|
||||
});
|
||||
|
||||
|
||||
+160
-33
@@ -1,52 +1,179 @@
|
||||
import Layout from '../components/Layout';
|
||||
import SocialLinks from '../components/SocialLinks';
|
||||
import AnimatedName from '../components/AnimatedName';
|
||||
import SectionHeading from '../components/SectionHeading';
|
||||
import ProjectCard from '../components/ProjectCard';
|
||||
import Reveal from '../components/Reveal';
|
||||
import Icon from '../components/Icon';
|
||||
import { profile, stats, focusAreas, stack, projects } from '../data/profile';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout showNavLinks={false}>
|
||||
<Layout>
|
||||
{/* HERO */}
|
||||
<section className="hero">
|
||||
<h1 className="hero-title">
|
||||
Hey, I'm <AnimatedName />
|
||||
</h1>
|
||||
<div className="hero-main">
|
||||
<div className="hero-backdrop" aria-hidden="true" />
|
||||
<div className="shell hero-grid">
|
||||
<div className="hero-content">
|
||||
|
||||
<p className="hero-subtitle">
|
||||
A software engineer building systems that scale in Amsterdam
|
||||
</p>
|
||||
|
||||
<div className="hero-description">
|
||||
<p>
|
||||
I architect and scale backend systems that handle millions of requests.
|
||||
From payment processing to logistics orchestration, I turn complex
|
||||
technical challenges into reliable production infrastructure.
|
||||
{profile.available && (
|
||||
<p className="status-pill">
|
||||
<span className="live-dot" />
|
||||
Open to new opportunities
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p>
|
||||
6 years shipping products across <strong>finance</strong>, <strong>ecommerce</strong>, and <strong>logistics</strong>.
|
||||
I work with Python (Django, FastAPI), JavaScript (React, Node.js), PostgreSQL,
|
||||
Docker, Redis, and Kubernetes to build systems that just work.
|
||||
</p>
|
||||
<h1 className="hero-title">
|
||||
Hey, I’m {profile.firstName}.
|
||||
<span className="hero-title-muted">{profile.tagline}</span>
|
||||
</h1>
|
||||
|
||||
<p className="hero-subtitle">{profile.intro}</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<a href={`mailto:${profile.email}`} className="button button-primary">
|
||||
Let’s work together
|
||||
<Icon name="arrowRight" size={16} />
|
||||
</a>
|
||||
<a href="#projects" className="button button-ghost">
|
||||
View projects
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<SocialLinks />
|
||||
<div className="hero-meta">
|
||||
<span className="meta-item">
|
||||
<Icon name="pin" size={16} /> {profile.location}
|
||||
</span>
|
||||
<span className="meta-divider" />
|
||||
<SocialLinks variant="icons" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="mailto:me@jesseinit.dev" className="primary-cta">
|
||||
<span>Let's work together</span>
|
||||
<span className="arrow">→</span>
|
||||
<div className="hero-visual">
|
||||
<div className="portrait">
|
||||
<img src="/hero.jpg" alt={profile.name} className="portrait-image" />
|
||||
</div>
|
||||
<div className="terminal-card" aria-hidden="true">
|
||||
<div className="terminal-bar">
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
<em>~/whoami</em>
|
||||
</div>
|
||||
<pre className="terminal-body">
|
||||
<code>
|
||||
<span className="t-prompt">$</span> whoami{'\n'}
|
||||
<span className="t-accent">
|
||||
<AnimatedName />
|
||||
</span>
|
||||
{'\n'}
|
||||
<span className="t-prompt">$</span> cat role.txt{'\n'}
|
||||
<span className="t-muted">{profile.role.toLowerCase()} @ {profile.location.split(',')[0].toLowerCase()}</span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shell">
|
||||
<dl className="stats">
|
||||
{stats.map((stat, i) => (
|
||||
<Reveal key={stat.label} className="stat" delay={i * 80}>
|
||||
<dt className="stat-label">{stat.label}</dt>
|
||||
<dd className="stat-value">{stat.value}</dd>
|
||||
</Reveal>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ABOUT */}
|
||||
<section id="about" className="section">
|
||||
<div className="shell">
|
||||
<SectionHeading index="01" eyebrow="About" title="Engineering for the long run." />
|
||||
<div className="about-grid">
|
||||
<Reveal className="about-copy">
|
||||
{profile.about.map((paragraph) => (
|
||||
<p key={paragraph}>{paragraph}</p>
|
||||
))}
|
||||
</Reveal>
|
||||
<div className="focus-grid">
|
||||
{focusAreas.map((area, i) => (
|
||||
<Reveal key={area.title} className="focus-card" delay={i * 80}>
|
||||
<span className="focus-icon">
|
||||
<Icon name={area.icon} size={20} />
|
||||
</span>
|
||||
<h3 className="focus-title">{area.title}</h3>
|
||||
<p className="focus-description">{area.description}</p>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* STACK */}
|
||||
<section id="stack" className="section">
|
||||
<div className="shell">
|
||||
<SectionHeading index="02" eyebrow="Stack" title="Tools I reach for.">
|
||||
The technologies I use day to day to design, build and run production systems.
|
||||
</SectionHeading>
|
||||
<div className="stack-grid">
|
||||
{stack.map((group, i) => (
|
||||
<Reveal key={group.group} className="stack-group" delay={i * 80}>
|
||||
<h3 className="stack-title">{group.group}</h3>
|
||||
<ul className="tag-list">
|
||||
{group.items.map((item) => (
|
||||
<li key={item} className="tag tag-large">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* PROJECTS */}
|
||||
<section id="projects" className="section">
|
||||
<div className="shell">
|
||||
<SectionHeading index="03" eyebrow="Projects" title="Things I’ve built.">
|
||||
A mix of production tools and side projects. More on{' '}
|
||||
<a href="https://github.com/jesseinit" target="_blank" rel="noopener noreferrer" className="text-link">
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
.
|
||||
</SectionHeading>
|
||||
<div className="project-grid">
|
||||
{projects.map((project, i) => (
|
||||
<Reveal key={project.title} delay={(i % 3) * 80} className={project.featured ? 'span-2' : ''}>
|
||||
<ProjectCard project={project} />
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="hero-image-wrapper">
|
||||
<div className="image-backdrop"></div>
|
||||
<img
|
||||
src="hero.jpg"
|
||||
alt="Jesse Egbosionu"
|
||||
className="hero-image"
|
||||
/>
|
||||
</div>
|
||||
{/* CONTACT */}
|
||||
<section id="contact" className="section">
|
||||
<div className="shell">
|
||||
<Reveal className="contact-card">
|
||||
<p className="eyebrow">
|
||||
<span className="eyebrow-index">04</span>
|
||||
Contact
|
||||
</p>
|
||||
<h2 className="contact-title">Have a system that needs to scale?</h2>
|
||||
<p className="contact-lead">
|
||||
I’m always happy to talk about backend architecture, interesting problems or new roles. My inbox is open.
|
||||
</p>
|
||||
<div className="hero-actions">
|
||||
<a href={`mailto:${profile.email}`} className="button button-primary">
|
||||
<Icon name="mail" size={16} />
|
||||
{profile.email}
|
||||
</a>
|
||||
<SocialLinks variant="pills" />
|
||||
</div>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
+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