Compare commits

..
10 Commits
18 changed files with 1617 additions and 351 deletions
+1
View File
@@ -32,3 +32,4 @@ yarn-error.log*
# vercel # vercel
.vercel .vercel
.claude
+29
View File
@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
const names = ['👨🏿‍💼 Jesse Egbosionu', '👨🏿‍💻 jesseinit', '😎 jessewithit'];
export default function AnimatedName() {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
setIsAnimating(true);
setTimeout(() => {
setCurrentIndex((prevIndex) => (prevIndex + 1) % names.length);
setIsAnimating(false);
}, 500); // Half of animation duration for smooth transition
}, 3000); // Change name every 3 seconds
return () => clearInterval(interval);
}, []);
return (
<span className="animated-name-wrapper">
<span className={`highlight animated-name ${isAnimating ? 'fade-out' : 'fade-in'}`}>
{names[currentIndex]}
</span>
</span>
);
}
+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>
</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>
);
}
+31
View File
@@ -0,0 +1,31 @@
export default function RateCard({ provider, rate, href, isBest, index, formatCurrency, logo }) {
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">
<div className="provider-info">
{logo && (
<img
src={logo}
alt={`${provider} logo`}
className="provider-logo"
/>
)}
<h3 className="provider-name">{provider}</h3>
</div>
{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>
);
}
+225
View File
@@ -0,0 +1,225 @@
# Kubernetes Deployment Guide
This directory contains all the necessary Kubernetes manifests to deploy the jesseinit portfolio application.
## Prerequisites
- Kubernetes cluster (local or cloud)
- kubectl CLI installed and configured
- Docker for building the image
- (Optional) kustomize for managing configurations
## Files Overview
- `deployment.yaml` - Kubernetes Deployment with 2 replicas, health checks, and resource limits
- `service.yaml` - ClusterIP Service exposing the application on port 80
- `ingress.yaml` - Ingress resource for external access (requires ingress controller)
- `configmap.yaml` - ConfigMap for environment variables
- `kustomization.yaml` - Kustomize configuration for easy deployment management
## Quick Start
### 1. Build the Docker Image
```bash
# Build the image
docker build -t jesseinit-portfolio:latest .
# If using a registry, tag and push
docker tag jesseinit-portfolio:latest <your-registry>/jesseinit-portfolio:latest
docker push <your-registry>/jesseinit-portfolio:latest
```
### 2. Update Image Reference (if using a registry)
Edit `k8s/kustomization.yaml` and update the image reference:
```yaml
images:
- name: jesseinit-portfolio
newName: <your-registry>/jesseinit-portfolio
newTag: latest
```
### 3. Deploy to Kubernetes
#### Option A: Using kubectl
```bash
# Apply all manifests
kubectl apply -f k8s/
# Verify deployment
kubectl get pods -l app=jesseinit-portfolio
kubectl get svc jesseinit-portfolio
kubectl get ingress jesseinit-portfolio
```
#### Option B: Using Kustomize
```bash
# Deploy using kustomize
kubectl apply -k k8s/
# Or build and preview first
kubectl kustomize k8s/ | kubectl apply -f -
```
### 4. Configure Ingress (Optional)
Edit `k8s/ingress.yaml` to match your setup:
1. Update the `host` field with your domain
2. Set the correct `ingressClassName` (nginx, traefik, etc.)
3. Uncomment TLS section if using HTTPS
4. Add appropriate annotations for your ingress controller
Example for NGINX with cert-manager:
```yaml
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
```
### 5. Add Environment Variables (Optional)
Edit `k8s/configmap.yaml` to add any required environment variables:
```yaml
data:
API_URL: "https://api.example.com"
NEXT_PUBLIC_SITE_URL: "https://jesseinit.com"
```
## Local Development with Minikube
```bash
# Start minikube
minikube start
# Build image in minikube's Docker daemon
eval $(minikube docker-env)
docker build -t jesseinit-portfolio:latest .
# Deploy
kubectl apply -k k8s/
# Access the application
minikube service jesseinit-portfolio
```
## Monitoring
### Check Pod Status
```bash
kubectl get pods -l app=jesseinit-portfolio
kubectl logs -l app=jesseinit-portfolio -f
kubectl describe pod <pod-name>
```
### Check Service
```bash
kubectl get svc jesseinit-portfolio
kubectl describe svc jesseinit-portfolio
```
### Port Forward for Local Testing
```bash
kubectl port-forward svc/jesseinit-portfolio 8080:80
# Access at http://localhost:8080
```
## Scaling
```bash
# Scale to 3 replicas
kubectl scale deployment jesseinit-portfolio --replicas=3
# Or edit deployment.yaml and reapply
```
## Updating the Application
```bash
# Build new image with new tag
docker build -t jesseinit-portfolio:v2 .
docker push <your-registry>/jesseinit-portfolio:v2
# Update the image
kubectl set image deployment/jesseinit-portfolio portfolio=<your-registry>/jesseinit-portfolio:v2
# Or update kustomization.yaml and reapply
kubectl apply -k k8s/
```
## Troubleshooting
### Pods not starting
```bash
kubectl describe pod <pod-name>
kubectl logs <pod-name>
```
### Service not accessible
```bash
kubectl get endpoints jesseinit-portfolio
kubectl describe svc jesseinit-portfolio
```
### Image pull errors
If using a private registry, create an image pull secret:
```bash
kubectl create secret docker-registry regcred \
--docker-server=<your-registry> \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email>
```
Then add to `deployment.yaml`:
```yaml
spec:
imagePullSecrets:
- name: regcred
```
## Resource Management
The deployment includes default resource requests and limits:
- Requests: 128Mi memory, 100m CPU
- Limits: 512Mi memory, 500m CPU
Adjust these in `deployment.yaml` based on your application's needs.
## Cleanup
```bash
# Delete all resources
kubectl delete -k k8s/
# Or delete individually
kubectl delete -f k8s/
```
## Production Considerations
1. **Use a specific image tag** instead of `latest` for better version control
2. **Set up HorizontalPodAutoscaler** for automatic scaling
3. **Configure persistent storage** if needed (for logs, cache, etc.)
4. **Add network policies** for security
5. **Set up monitoring** with Prometheus/Grafana
6. **Configure TLS/SSL** certificates for HTTPS
7. **Use secrets** for sensitive data instead of ConfigMaps
8. **Implement resource quotas** and limit ranges
9. **Set up backup and disaster recovery** procedures
10. **Use namespace isolation** for multi-environment deployments
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: jesseinit-portfolio-config
labels:
app: jesseinit-portfolio
data:
# Add any environment variables your application needs
# Example:
# API_URL: "https://api.example.com"
# NEXT_PUBLIC_SITE_URL: "https://jesseinit.com"
+56
View File
@@ -0,0 +1,56 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: jesseinit-portfolio
labels:
app: jesseinit-portfolio
spec:
replicas: 2
selector:
matchLabels:
app: jesseinit-portfolio
template:
metadata:
labels:
app: jesseinit-portfolio
spec:
containers:
- name: portfolio
image: jesseinit-portfolio:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http
env:
- name: NODE_ENV
value: "production"
- name: NEXT_TELEMETRY_DISABLED
value: "1"
envFrom:
- configMapRef:
name: jesseinit-portfolio-config
optional: true
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
restartPolicy: Always
+28
View File
@@ -0,0 +1,28 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: jesseinit-portfolio
labels:
app: jesseinit-portfolio
annotations:
# Uncomment and configure based on your ingress controller
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx # Change to your ingress class (nginx, traefik, etc.)
rules:
- host: jesseinit.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jesseinit-portfolio
port:
number: 80
# Uncomment for TLS/HTTPS support
# tls:
# - hosts:
# - jesseinit.com
# secretName: jesseinit-portfolio-tls
+18
View File
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
resources:
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app: jesseinit-portfolio
images:
- name: jesseinit-portfolio
newName: jesseinit-portfolio
newTag: latest
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: jesseinit-portfolio
labels:
app: jesseinit-portfolio
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 3000
protocol: TCP
name: http
selector:
app: jesseinit-portfolio
+86 -112
View File
@@ -180,14 +180,14 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.0.4", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
"integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==" "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==", "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -200,9 +200,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -215,9 +215,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -230,9 +230,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -245,9 +245,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -260,9 +260,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -275,9 +275,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -290,9 +290,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -305,9 +305,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "14.0.4", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -354,11 +354,17 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.2", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"dependencies": { "dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
@@ -485,9 +491,9 @@
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -495,11 +501,11 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -576,9 +582,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001568", "version": "1.0.30001761",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==", "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -837,9 +843,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -933,11 +939,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -1172,12 +1173,12 @@
} }
}, },
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.5", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.3",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
}, },
"engines": { "engines": {
@@ -1218,9 +1219,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -1235,18 +1236,17 @@
} }
}, },
"node_modules/next": { "node_modules/next": {
"version": "14.0.4", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
"integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"dependencies": { "dependencies": {
"@next/env": "14.0.4", "@next/env": "14.2.35",
"@swc/helpers": "0.5.2", "@swc/helpers": "0.5.5",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001406", "caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11", "graceful-fs": "^4.2.11",
"postcss": "8.4.31", "postcss": "8.4.31",
"styled-jsx": "5.1.1", "styled-jsx": "5.1.1"
"watchpack": "2.4.0"
}, },
"bin": { "bin": {
"next": "dist/bin/next" "next": "dist/bin/next"
@@ -1255,18 +1255,19 @@
"node": ">=18.17.0" "node": ">=18.17.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "14.0.4", "@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.0.4", "@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.0.4", "@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.0.4", "@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.0.4", "@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.0.4", "@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.0.4", "@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.0.4", "@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.0.4" "@next/swc-win32-x64-msvc": "14.2.33"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"sass": "^1.3.0" "sass": "^1.3.0"
@@ -1275,6 +1276,9 @@
"@opentelemetry/api": { "@opentelemetry/api": {
"optional": true "optional": true
}, },
"@playwright/test": {
"optional": true
},
"sass": { "sass": {
"optional": true "optional": true
} }
@@ -1693,21 +1697,6 @@
"integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==", "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==",
"dev": true "dev": true
}, },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"dev": true,
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/run-parallel": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -1896,15 +1885,12 @@
} }
}, },
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.1", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
"dev": true, "dev": true,
"dependencies": {
"rimraf": "^3.0.0"
},
"engines": { "engines": {
"node": ">=8.17.0" "node": ">=14.14"
} }
}, },
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
@@ -1919,9 +1905,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
}, },
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
@@ -1968,18 +1954,6 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true "dev": true
}, },
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+20 -53
View File
@@ -1,4 +1,4 @@
const { exec } = require("child_process"); import { log } from "console";
const COMMON_HEADERS = { const COMMON_HEADERS = {
'accept': '*/*', 'accept': '*/*',
@@ -102,6 +102,7 @@ export default async (req, res) => {
provider: "TapTap", provider: "TapTap",
bestRate: false, bestRate: false,
href: "https://www.taptapsend.com/", href: "https://www.taptapsend.com/",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSFcB9ECgZxEULIhxf-PZWEn23UPZATQZblQQ&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -124,6 +125,7 @@ export default async (req, res) => {
provider: "Send", provider: "Send",
bestRate: false, bestRate: false,
href: "https://send.flutterwave.com/", href: "https://send.flutterwave.com/",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRyhQXYEg8iHCMajGdJn0B0FxwsuzAfkSeBgQ&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -131,54 +133,11 @@ export default async (req, res) => {
return createDefaultErrorResponse("Send", "https://send.flutterwave.com/"); return createDefaultErrorResponse("Send", "https://send.flutterwave.com/");
}); });
const curlCommand = `curl -v 'https://acemoneytransfer.com/make-request' \ // Ace Transfer temporarily disabled due to Cloudflare protection
-H 'authority: acemoneytransfer.com' \ // Will return a default response to avoid blocking the API
-H 'accept: */*' \ const aceCall = Promise.resolve(
-H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \ createDefaultErrorResponse("Ace Transfer", "https://acemoneytransfer.com/referral-link/3056004")
-H 'content-type: application/x-www-form-urlencoded; charset=UTF-8' \ );
-H 'origin: https://acemoneytransfer.com' \
-H 'referer: https://acemoneytransfer.com/Nigeria/Send-Money-to-Nigeria' \
-H 'sec-ch-ua: "Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' \
-H 'x-requested-with: XMLHttpRequest' \
--data-raw 'uri=rate%2Fcalculator&type=POST&data%5Bsrc_amount%5D=100&data%5Bdest_amount%5D=&data%5Buser_currency%5D=EUR&data%5Bcalculation_mode%5D=S&data%5Bdest_iso_numeric_code%5D=566&data%5Bsrc_iso_numeric_code%5D=528'
`;
const aceCall = new Promise((resolve, reject) => {
exec(curlCommand, (error, stdout, stderr) => {
if (error) {
reject(`Error: ${error.message}`);
return;
}
try {
const parsedRate = JSON.parse(stdout);
resolve({
rate: parseFloat(parsedRate.data.exchange_rate).toFixed(2),
provider: "Ace Transfer",
bestRate: false,
});
} catch (parseError) {
reject(parseError.message);
}
});
})
.then((result) => {
return {
rate: parseFloat(result.rate).toFixed(2),
provider: "Ace Transfer",
bestRate: false,
href: "https://acemoneytransfer.com/referral-link/3056004",
};
})
.catch((error) => {
console.error("Error calling Ace:", error);
return createDefaultErrorResponse("Ace Transfer", "https://acemoneytransfer.com/referral-link/3056004");
});
const remitlyCall = new Promise((resolve, reject) => { const remitlyCall = new Promise((resolve, reject) => {
fetch("https://api.remitly.io/v5/pricing/estimates?amount=1%20EUR&anchor=SEND&conduit=NLD%3AEUR-NGA%3ANGN&purpose=OTHER", { fetch("https://api.remitly.io/v5/pricing/estimates?amount=1%20EUR&anchor=SEND&conduit=NLD%3AEUR-NGA%3ANGN&purpose=OTHER", {
@@ -198,6 +157,7 @@ export default async (req, res) => {
provider: "Remitly", provider: "Remitly",
bestRate: false, bestRate: false,
href: "https://remit.ly/94fqcne9", href: "https://remit.ly/94fqcne9",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNZm0irEt8u6LXo-OFXZ9bcTchNoSeaDDnsA&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -225,6 +185,7 @@ export default async (req, res) => {
provider: "Nala", provider: "Nala",
bestRate: false, bestRate: false,
href: "https://join.iwantnala.com/JESSE-715106", href: "https://join.iwantnala.com/JESSE-715106",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRpbV0MWgCocA3dFuWoxgCmQsxGfqRuttbD3w&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -257,10 +218,11 @@ export default async (req, res) => {
.catch((error) => reject(error)); .catch((error) => reject(error));
}).then((result) => { }).then((result) => {
return { return {
rate: Math.floor(result.data.rate % 10000), rate: 0,
provider: "Lemfi(referal code - OBIN6518)", provider: "Lemfi(referal code - OBIN6518)",
bestRate: false, bestRate: false,
href: "https://lemfi.com", href: "https://lemfi.com",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMYTBiPB_joupdSSoBL1fA0wySDPcMpY92XQ&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -284,6 +246,7 @@ export default async (req, res) => {
provider: "Afriex", provider: "Afriex",
bestRate: false, bestRate: false,
href: "https://www.afriex.com/", href: "https://www.afriex.com/",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTvFac7oIDG6QsJlYsBIRnWhFfy0OWjN5Mspg&s"
}; };
}) })
.catch((error) => { .catch((error) => {
@@ -323,6 +286,7 @@ export default async (req, res) => {
provider: "Wise", provider: "Wise",
bestRate: false, bestRate: false,
href: "https://wise.com/invite/dic/obinnae93", href: "https://wise.com/invite/dic/obinnae93",
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3L26sFmjg2xyW4CJy_5peQt-v0JHgsYhzvA&s"
}; };
} else { } else {
throw new Error("Wise provider or quotes not found"); throw new Error("Wise provider or quotes not found");
@@ -339,7 +303,8 @@ export default async (req, res) => {
nalaCall, nalaCall,
lemfiCall, lemfiCall,
afriexCall, afriexCall,
wiseCall wiseCall,
sendCall
]) ])
.then( .then(
([ ([
@@ -349,7 +314,8 @@ export default async (req, res) => {
nalaResponse, nalaResponse,
lemfiResponse, lemfiResponse,
afriexResponse, afriexResponse,
wiseResponse wiseResponse,
sendResponse
]) => { ]) => {
rates = [ rates = [
aceResponse, aceResponse,
@@ -358,7 +324,8 @@ export default async (req, res) => {
nalaResponse, nalaResponse,
lemfiResponse, lemfiResponse,
afriexResponse, afriexResponse,
wiseResponse wiseResponse,
sendResponse
]; ];
} }
) )
+39 -54
View File
@@ -1,69 +1,54 @@
import Head from 'next/head'; import Layout from '../components/Layout';
import SocialLinks from '../components/SocialLinks';
import AnimatedName from '../components/AnimatedName';
export default function Home() { export default function Home() {
return ( return (
<div className="container"> <Layout showNavLinks={false}>
<Head>
<title>Home - Jesse Egbosionu</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<header className="header">
<nav className="header__navbar">
<p className="header__logo">
<a href="https://twitter.com/jesseinit">init</a>
</p>
</nav>
</header>
<main>
<section className="hero"> <section className="hero">
<aside className="hero__aside"> <h1 className="hero-title">
<h2 className="hero__author_name">Jesse Egbosionu</h2> Hey, I'm <AnimatedName />
<section className="hero__links"> </h1>
<a href="https://github.com/jesseinit" rel="noopener" alt="GitHub"> <div className="hero-main">
GitHub <div className="hero-content">
</a>
<span> · </span> <p className="hero-subtitle">
<a href="https://linkedin.com/in/jesse-egbosionu/" rel="noopener" alt="Linkedin"> A software engineer building systems that scale in Amsterdam
LinkedIn
</a>
<span> · </span>
<a href="https://twitter.com/jesseinit/" rel="noopener" alt="Twitter">
Twitter
</a>
</section>
<p className="hero__description">
I'm a software engineer based in Amsterdam, Netherlands and currently focused in building and scaling
software backends. Creating impact, team-work and continuous learning are some of my core
tenets and I continuously adapt these in solving customers problems when I build
software.
</p> </p>
<p className="hero__description"> <div className="hero-description">
In the last 5 years I've been building services and products in the finance, ecommerce <p>
and logistics domain, enriching lives using technologies like Python(Django, I architect and scale backend systems that handle millions of requests.
FastAPI), Javascript(React, Node.js), PostgreSQL, Docker, Redis, Kubernetes etc. From payment processing to logistics orchestration, I turn complex
technical challenges into reliable production infrastructure.
</p> </p>
<p className="hero__description"> <p>
I'm constantly looking for challenging and impactful opportunities so can always shoot 6 years shipping products across <strong>finance</strong>, <strong>ecommerce</strong>, and <strong>logistics</strong>.
me an{' '} I work with Python (Django, FastAPI), JavaScript (React, Node.js), PostgreSQL,
<a className="text-underline email-cta" href="mailto:me@jesseinit.dev"> Docker, Redis, and Kubernetes to build systems that just work.
email
</a>
.
</p> </p>
</aside> </div>
<aside className="hero__aside text-align-center">
<SocialLinks />
<a href="mailto:me@jesseinit.dev" className="primary-cta">
<span>Let's work together</span>
<span className="arrow">→</span>
</a>
</div>
<div className="hero-image-wrapper">
<div className="image-backdrop"></div>
<img <img
src="hero.jpg" src="hero.jpg"
className="hero__image" alt="Jesse Egbosionu"
className="hero-image"
/> />
</aside> </div>
</div>
</section> </section>
</main> </Layout>
<footer className="footer">
<p className="text-align-center">Made with <span style={{ "color": "red" }}>❤️</span> and Vibes</p>
</footer>
</div >
); );
} }
+106 -38
View File
@@ -1,20 +1,24 @@
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() {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => { useEffect(() => {
const fetchDataFromApi = async () => { const fetchDataFromApi = async () => {
try { try {
const apiData = await fetchData(); const apiData = await fetchData();
setData(apiData); setData(apiData);
setError(null);
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
setError("Failed to load exchange rates. Please try again later.");
} finally { } finally {
setLoading(false); // Set loading to false regardless of success or error setLoading(false);
} }
}; };
@@ -28,48 +32,112 @@ export default function Rates() {
}).format(value); }).format(value);
}; };
// Your UI rendering logic here, using the 'data' state const getBestRate = () => {
if (!data || data.length === 0) return null;
return Math.max(...data.map(item => item.rate));
};
const bestRate = getBestRate();
const sortedData = data ? [...data].sort((a, b) => {
const aIsBest = a.bestRate || a.rate === bestRate;
const bIsBest = b.bestRate || b.rate === bestRate;
if (aIsBest && !bIsBest) return -1;
if (!aIsBest && bIsBest) return 1;
return b.rate - a.rate;
}) : null;
return ( return (
<div className="container"> <Layout
<Head> title="EUR to NGN Exchange Rates - Jesse Egbosionu"
<title>EUR Rates</title> description="Compare live EUR to NGN exchange rates across multiple providers."
<link rel="icon" href="/favicon.ico" /> showNavLinks={false}
</Head>
<header className="header">
<nav className="header__navbar">
<p className="header__logo">
<a href="https://twitter.com/jesseinit">init</a>
</p>
</nav>
</header>
<main>
<h2>For €1 you'll get the following rates</h2>
<section className="hero">
{loading ? (
<div>
<p>Loading...</p>
</div>
) : (
data && (
<div className="currency__container">
{data.map((item) => (
<p
key={item.provider}
className={
item.bestRate ? "best-rate" : ""
}
> >
<a href={item.href}>{item.provider}:</a> <section className="rates-section">
<span className="rate"> <div className="rates-header">
{formatCurrency(item.rate)} <div className="rates-title-group">
</span> <h1 className="rates-title">Exchange Rates</h1>
<p className="rates-subtitle">
Compare live EUR to NGN rates across providers
</p> </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>
</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> </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 && (
<>
<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 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>
</div>
)} )}
</section> </section>
</main> </Layout>
</div>
); );
} }
+842 -100
View File
@@ -5,144 +5,886 @@
font-style: normal; font-style: normal;
} }
:root {
--color-primary: #2563eb;
--color-secondary: #1e40af;
--color-accent: #dbeafe;
--color-accent-hover: #bfdbfe;
--color-background: #ffffff;
--color-surface: #f8fafc;
--color-border: #93c5fd;
--color-text: #1e293b;
--color-text-muted: #64748b;
--color-success: #3b82f6;
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
--spacing-xl: 3rem;
--spacing-2xl: 4rem;
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-full: 9999px;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
* { * {
box-sizing: border-box; box-sizing: border-box;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
html {
scroll-behavior: smooth;
}
html, html,
body { body {
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: inter, sans-serif; font-family: inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: var(--color-text);
background-color: var(--color-background);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
a { a {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
transition: all 0.2s ease;
}
strong {
font-weight: 600;
color: var(--color-primary);
} }
.container { .container {
position: relative; position: relative;
min-height: 100vh; min-height: 100vh;
max-width: 960px; max-width: 1200px;
min-width: 360px; padding: 0 var(--spacing-lg);
padding: 0 20px;
margin: 0 auto; margin: 0 auto;
display: flex;
flex-direction: column;
} }
// Page Styles // HEADER
// UTILS
.text-align-center {
text-align: center;
}
.text-underline {
text-decoration: underline;
}
// HEADER AREA
.header { .header {
padding: 30px 0; padding: var(--spacing-lg) 0;
border-bottom: 1px solid var(--color-border);
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
position: sticky;
top: 0;
z-index: 100;
}
&__navbar { .navbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} align-items: center;
}
&__logo a { .logo {
font-size: 1.2em; display: flex;
padding: 3px 0; align-items: center;
border-bottom: 3px solid rgb(0, 119, 255);
.logo-text {
font-size: 1.5rem;
font-weight: 700;
background: var(--color-accent);
color: var(--color-primary);
padding: 0.25rem 0.75rem;
border-radius: var(--radius-sm);
}
}
.nav-links {
display: flex;
gap: var(--spacing-lg);
align-items: center;
}
.nav-link {
font-size: 0.95rem;
color: var(--color-text-muted);
font-weight: 500;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-sm);
transition: all 0.2s ease;
&:hover { &:hover {
color: #000; color: var(--color-primary);
padding-bottom: 1px; background: var(--color-surface);
}
} }
&__navbar ul { &.cta {
display: flex; color: var(--color-primary);
list-style: none; background: var(--color-accent);
} border: 1px solid var(--color-primary);
&__navbar li:not(:last-child) { &:hover {
margin-right: 25px; background: var(--color-accent-hover);
}
} }
} }
// HERO AREA // MAIN CONTENT
.main-content {
flex: 1;
display: flex;
align-items: center;
padding: var(--spacing-xl) 0;
}
// HERO SECTION
.hero { .hero {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
flex-direction: column-reverse; gap: var(--spacing-2xl);
padding: 20px 0;
&__aside {
text-align: center;
}
&__author_name {
font-weight: 500;
font-size: 1.2em;
margin: 10px 0;
}
&__links {
margin: 0 0 20px;
font-size: 0.9em;
}
&__description {
font-size: 1em;
margin-bottom: 20px;
line-height: 1.5;
}
&__image {
width: 60%;
border-radius: 2%;
}
}
.best-rate {
border-radius: 3px;
display: inline-block;
color: #000;
background-color: #a2f6a7;
}
.currency__container {
& a {
text-decoration: underline;
text-decoration-color: rgb(0, 119, 255);
}
& p {
margin: 2px 0;
padding: 2px 0px;
padding-right: 2px;
}
& span {
margin-left: 5px;
}
}
.email-cta {
display: inline-block;
background: rgb(0, 119, 255);
padding: 2px 5px;
border-radius: 10%;
color: #fff;
}
.footer {
width: 100%; width: 100%;
}
.hero-main {
display: flex;
gap: var(--spacing-2xl);
align-items: center;
}
.hero-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.hero-title {
font-size: clamp(1.75rem, 5vw, 3.5rem);
font-weight: 800;
line-height: 1.2;
color: var(--color-primary);
animation: fadeInUp 0.6s ease forwards;
opacity: 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
@media (max-width: 767px) {
font-size: clamp(1.5rem, 6vw, 2rem);
gap: 0.35rem;
}
.highlight {
background: var(--color-accent);
color: var(--color-primary);
padding: 0 0.5rem;
border-bottom: 2px solid var(--color-primary);
@media (max-width: 767px) {
padding: 0 0.35rem;
border-bottom: 1px solid var(--color-primary);
}
}
.animated-name-wrapper {
display: inline-block;
min-width: 0;
text-align: left;
@media (max-width: 767px) {
text-align: left;
}
}
.animated-name {
display: inline-block;
transition: opacity 0.5s ease-in-out;
white-space: nowrap;
@media (max-width: 767px) {
font-size: 0.95em;
}
&.fade-out {
opacity: 0;
}
&.fade-in {
opacity: 1;
}
}
}
.hero-subtitle {
font-size: clamp(1.125rem, 2vw, 1.5rem);
color: var(--color-secondary);
font-weight: 500;
animation: fadeInUp 0.6s ease 0.1s forwards;
opacity: 0;
}
.hero-description {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
p {
font-size: 1.0625rem;
line-height: 1.75;
color: var(--color-text-muted);
animation: fadeInUp 0.6s ease 0.2s forwards;
opacity: 0;
}
}
.social-links {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
animation: fadeInUp 0.6s ease 0.3s forwards;
opacity: 0;
}
.social-link {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-md);
background: var(--color-background);
border: 1px solid var(--color-primary);
border-radius: var(--radius-sm);
font-size: 0.9375rem;
font-weight: 600;
color: var(--color-text);
transition: all 0.2s ease;
&:hover {
background: var(--color-accent);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
.social-icon {
transform: translateX(4px);
}
}
.social-icon {
flex-shrink: 0;
transition: transform 0.2s ease;
}
}
.primary-cta {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-primary);
color: var(--color-background);
font-size: 1rem;
font-weight: 700;
border-radius: var(--radius-md);
width: fit-content;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: fadeInUp 0.6s ease 0.4s forwards;
opacity: 0;
border: 1px solid var(--color-primary);
&:hover {
background: var(--color-accent);
color: var(--color-primary);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
.arrow {
transform: translateX(4px);
}
}
.arrow {
font-size: 1.25rem;
transition: transform 0.2s ease;
}
}
.hero-image-wrapper {
position: relative;
display: flex;
justify-content: center;
align-items: center;
animation: fadeIn 0.8s ease 0.3s forwards;
opacity: 0;
}
.image-backdrop {
position: absolute; position: absolute;
bottom: 0; width: 100%;
font-size: 0.9em; height: 100%;
margin: 20px 0; background: var(--color-accent);
border-radius: var(--radius-lg);
transform: rotate(-6deg);
z-index: -1;
border: 1px solid var(--color-primary);
}
.hero-image {
width: 100%;
max-width: 400px;
height: auto;
border-radius: var(--radius-lg);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
object-fit: cover;
transition: transform 0.3s ease;
border: 1px solid var(--color-primary);
&:hover {
transform: scale(1.02) rotate(2deg);
}
}
// FOOTER
.footer {
border-top: 1px solid var(--color-primary);
padding: var(--spacing-lg) 0;
margin-top: auto;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.footer-year {
font-weight: 500;
}
.heart {
display: inline-block;
color: #e11d48;
animation: heartbeat 1.5s ease-in-out infinite;
}
@keyframes heartbeat {
0%,
100% {
transform: scale(1);
}
10%,
30% {
transform: scale(1.1);
}
20%,
40% {
transform: scale(1);
}
}
// ANIMATIONS
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
// RATES PAGE
.rates-section {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
}
.rates-header {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
text-align: center;
padding-bottom: var(--spacing-lg);
border-bottom: 1px solid var(--color-primary);
}
.rates-title-group {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.rates-title {
font-size: clamp(2rem, 4vw, 2.5rem);
font-weight: 800;
color: var(--color-primary);
animation: fadeInUp 0.6s ease forwards;
}
.rates-subtitle {
font-size: 1.125rem;
color: var(--color-text-muted);
animation: fadeInUp 0.6s ease 0.1s forwards;
opacity: 0;
}
.currency-display {
display: flex;
justify-content: center;
animation: fadeInUp 0.6s ease 0.2s forwards;
opacity: 0;
}
.currency-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-accent);
border: 1px solid var(--color-primary);
border-radius: var(--radius-lg);
font-weight: 700;
font-size: 1.125rem;
.currency-from {
color: var(--color-primary);
}
.currency-arrow {
color: var(--color-primary);
}
.currency-to {
color: var(--color-primary);
}
}
.rates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--spacing-lg);
animation: fadeIn 0.6s ease 0.3s forwards;
opacity: 0;
}
.rate-card {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
padding: var(--spacing-lg);
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
transition: all 0.3s ease;
cursor: pointer;
animation: fadeInUp 0.6s ease forwards;
opacity: 0;
position: relative;
overflow: hidden;
&:hover {
background: var(--color-accent);
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.15);
.visit-link {
color: var(--color-primary);
transform: translateX(4px);
}
}
&.best {
border-width: 2px;
border-color: var(--color-primary);
background: var(--color-accent);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--color-primary);
}
&:hover {
box-shadow: 0 8px 24px rgba(37, 99, 235, 0.25);
}
.provider-name {
color: var(--color-primary);
}
.rate-value {
color: var(--color-primary);
}
}
}
.rate-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--spacing-sm);
}
.provider-info {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.provider-logo {
width: 40px;
height: 40px;
object-fit: contain;
border-radius: var(--radius-sm);
}
.provider-name {
font-size: 1.25rem;
font-weight: 700;
color: var(--color-text);
}
.best-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.875rem;
background: var(--color-primary);
color: var(--color-accent);
font-size: 0.6875rem;
font-weight: 700;
border-radius: var(--radius-full);
text-transform: uppercase;
letter-spacing: 0.75px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
animation: badgePulse 2s ease-in-out infinite;
border: 1px solid var(--color-primary);
&::before {
content: '★';
font-size: 0.875rem;
animation: starRotate 3s linear infinite;
}
}
@keyframes badgePulse {
0%,
100% {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transform: scale(1);
}
50% {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
transform: scale(1.05);
}
}
@keyframes starRotate {
0% {
transform: rotate(0deg);
color: #fbbf24; // bright yellow
}
16.66% {
color: #ffffff; // white
}
33.33% {
transform: rotate(60deg);
color: #ec4899; // hot pink
}
50% {
color: #f87171; // light red
}
66.66% {
transform: rotate(120deg);
color: #fb923c; // bright orange
}
83.33% {
color: #f472b6; // pink
}
100% {
transform: rotate(180deg);
color: #facc15; // yellow
}
}
.rate-value {
font-size: 2rem;
font-weight: 800;
color: var(--color-primary);
font-variant-numeric: tabular-nums;
}
.rate-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
padding-top: var(--spacing-sm);
border-top: 1px solid var(--color-border);
}
.visit-link {
font-size: 0.9375rem;
color: var(--color-text-muted);
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.rates-info {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-lg);
background: var(--color-surface);
border: 1px solid var(--color-primary);
border-radius: var(--radius-lg);
text-align: center;
animation: fadeInUp 0.6s ease 0.4s forwards;
opacity: 0;
}
.info-text {
font-size: 0.9375rem;
color: var(--color-text-muted);
}
.info-timestamp {
font-size: 0.875rem;
color: var(--color-text-muted);
font-weight: 500;
}
// LOADING SKELETON
.skeleton {
pointer-events: none;
animation: none !important;
opacity: 1 !important;
.skeleton-header,
.skeleton-rate,
.skeleton-badge {
background: linear-gradient(90deg,
var(--color-surface) 0%,
var(--color-border) 50%,
var(--color-surface) 100%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius-sm);
}
.skeleton-header {
width: 60%;
height: 24px;
}
.skeleton-rate {
width: 80%;
height: 36px;
margin: var(--spacing-sm) 0;
}
.skeleton-badge {
width: 40%;
height: 16px;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
// ERROR STATE
.error-state,
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-md);
padding: var(--spacing-2xl) var(--spacing-lg);
text-align: center;
animation: fadeInUp 0.6s ease forwards;
}
.error-icon,
.empty-icon {
font-size: 4rem;
line-height: 1;
}
.error-title,
.empty-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-primary);
}
.error-message,
.empty-message {
font-size: 1rem;
color: var(--color-text-muted);
max-width: 500px;
}
.retry-button {
margin-top: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-lg);
background: var(--color-primary);
color: var(--color-accent);
border: 1px solid var(--color-primary);
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--color-accent);
color: var(--color-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&:active {
transform: translateY(0);
}
}
// RESPONSIVE DESIGN
@media (min-width: 768px) {
.container {
padding: 0 var(--spacing-xl);
}
.hero-main {
flex-direction: row;
}
.hero-content {
flex: 1.2;
}
.hero-image-wrapper {
flex: 1;
}
.rates-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
text-align: left;
}
.rates-title-group {
align-items: flex-start;
}
.currency-display {
justify-content: flex-end;
}
}
@media (max-width: 767px) {
.nav-link:not(.cta) {
display: none;
}
.hero-main {
flex-direction: column-reverse;
}
.hero-image-wrapper {
width: 100%;
margin-bottom: var(--spacing-lg);
}
.hero-image {
max-width: 280px;
}
.social-links {
flex-direction: column;
.social-link {
width: 100%;
justify-content: center;
}
}
.primary-cta {
width: 100%;
justify-content: center;
}
.rates-grid {
grid-template-columns: 1fr;
}
.currency-badge {
flex-direction: column;
gap: var(--spacing-xs);
}
} }