Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c40a64c42 | ||
|
|
0d59363cdc | ||
|
|
148dbad7e8 | ||
|
|
fa297b1ef3 | ||
|
|
41c1b68a3b | ||
|
|
caa4c1202f | ||
|
|
c460ea27d4 | ||
|
|
5c22b9b844 | ||
|
|
b6265c4f0f | ||
|
|
cca249fa9a |
@@ -32,3 +32,4 @@ yarn-error.log*
|
|||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Generated
+86
-112
@@ -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
@@ -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
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
+43
-58
@@ -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>
|
<section className="hero">
|
||||||
<title>Home - Jesse Egbosionu</title>
|
<h1 className="hero-title">
|
||||||
<link rel="icon" href="/favicon.ico" />
|
Hey, I'm <AnimatedName />
|
||||||
</Head>
|
</h1>
|
||||||
<header className="header">
|
<div className="hero-main">
|
||||||
<nav className="header__navbar">
|
<div className="hero-content">
|
||||||
<p className="header__logo">
|
|
||||||
<a href="https://twitter.com/jesseinit">init</a>
|
<p className="hero-subtitle">
|
||||||
</p>
|
A software engineer building systems that scale in Amsterdam
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<section className="hero">
|
|
||||||
<aside className="hero__aside">
|
|
||||||
<h2 className="hero__author_name">Jesse Egbosionu</h2>
|
|
||||||
<section className="hero__links">
|
|
||||||
<a href="https://github.com/jesseinit" rel="noopener" alt="GitHub">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
<span> · </span>
|
|
||||||
<a href="https://linkedin.com/in/jesse-egbosionu/" rel="noopener" alt="Linkedin">
|
|
||||||
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
|
||||||
</p>
|
technical challenges into reliable production infrastructure.
|
||||||
|
</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
|
</p>
|
||||||
</a>
|
</div>
|
||||||
.
|
|
||||||
</p>
|
<SocialLinks />
|
||||||
</aside>
|
|
||||||
<aside className="hero__aside text-align-center">
|
<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>
|
||||||
</section>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
<footer className="footer">
|
</Layout>
|
||||||
<p className="text-align-center">Made with <span style={{ "color": "red" }}>❤️</span> and Vibes</p>
|
|
||||||
</footer>
|
|
||||||
</div >
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-42
@@ -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">
|
<section className="rates-section">
|
||||||
<nav className="header__navbar">
|
<div className="rates-header">
|
||||||
<p className="header__logo">
|
<div className="rates-title-group">
|
||||||
<a href="https://twitter.com/jesseinit">init</a>
|
<h1 className="rates-title">Exchange Rates</h1>
|
||||||
</p>
|
<p className="rates-subtitle">
|
||||||
</nav>
|
Compare live EUR to NGN rates across providers
|
||||||
</header>
|
</p>
|
||||||
<main>
|
</div>
|
||||||
<h2>For €1 you'll get the following rates</h2>
|
|
||||||
<section className="hero">
|
<div className="currency-display">
|
||||||
{loading ? (
|
<div className="currency-badge">
|
||||||
<div>
|
<span className="currency-from">€1 EUR</span>
|
||||||
<p>Loading...</p>
|
<span className="currency-arrow">→</span>
|
||||||
|
<span className="currency-to">NGN</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
data && (
|
</div>
|
||||||
<div className="currency__container">
|
|
||||||
{data.map((item) => (
|
{loading && (
|
||||||
<p
|
<div className="rates-grid">
|
||||||
key={item.provider}
|
{[...Array(6)].map((_, index) => (
|
||||||
className={
|
<div key={index} className="rate-card skeleton">
|
||||||
item.bestRate ? "best-rate" : ""
|
<div className="skeleton-header"></div>
|
||||||
}
|
<div className="skeleton-rate"></div>
|
||||||
>
|
<div className="skeleton-badge"></div>
|
||||||
<a href={item.href}>{item.provider}:</a>
|
|
||||||
<span className="rate">
|
|
||||||
{formatCurrency(item.rate)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
))}
|
||||||
)}
|
</div>
|
||||||
</section>
|
)}
|
||||||
</main>
|
|
||||||
</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>
|
||||||
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+828
-86
@@ -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 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: var(--color-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__logo a {
|
&.cta {
|
||||||
font-size: 1.2em;
|
color: var(--color-primary);
|
||||||
padding: 3px 0;
|
background: var(--color-accent);
|
||||||
border-bottom: 3px solid rgb(0, 119, 255);
|
border: 1px solid var(--color-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #000;
|
background: var(--color-accent-hover);
|
||||||
padding-bottom: 1px;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAIN CONTENT
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xl) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HERO SECTION
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2xl);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__navbar ul {
|
.animated-name-wrapper {
|
||||||
display: flex;
|
display: inline-block;
|
||||||
list-style: none;
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__navbar li:not(:last-child) {
|
.animated-name {
|
||||||
margin-right: 25px;
|
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 AREA
|
.hero-subtitle {
|
||||||
.hero {
|
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;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: column-reverse;
|
align-items: center;
|
||||||
padding: 20px 0;
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
&__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 {
|
.footer-year {
|
||||||
border-radius: 3px;
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #000;
|
color: #e11d48;
|
||||||
background-color: #a2f6a7;
|
animation: heartbeat 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.currency__container {
|
@keyframes heartbeat {
|
||||||
& a {
|
|
||||||
text-decoration: underline;
|
0%,
|
||||||
text-decoration-color: rgb(0, 119, 255);
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
& p {
|
10%,
|
||||||
margin: 2px 0;
|
30% {
|
||||||
padding: 2px 0px;
|
transform: scale(1.1);
|
||||||
padding-right: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
& span {
|
20%,
|
||||||
margin-left: 5px;
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-cta {
|
// ANIMATIONS
|
||||||
display: inline-block;
|
@keyframes fadeIn {
|
||||||
background: rgb(0, 119, 255);
|
from {
|
||||||
padding: 2px 5px;
|
opacity: 0;
|
||||||
border-radius: 10%;
|
}
|
||||||
color: #fff;
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
@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%;
|
width: 100%;
|
||||||
position: absolute;
|
display: flex;
|
||||||
bottom: 0;
|
flex-direction: column;
|
||||||
font-size: 0.9em;
|
gap: var(--spacing-xl);
|
||||||
margin: 20px 0;
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user