Compare commits
10
Commits
e9e47b10a6
...
5c40a64c42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c40a64c42 | ||
|
|
0d59363cdc | ||
|
|
148dbad7e8 | ||
|
|
fa297b1ef3 | ||
|
|
41c1b68a3b | ||
|
|
caa4c1202f | ||
|
|
c460ea27d4 | ||
|
|
5c22b9b844 | ||
|
|
b6265c4f0f | ||
|
|
cca249fa9a |
@@ -32,3 +32,4 @@ yarn-error.log*
|
||||
|
||||
# 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": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz",
|
||||
"integrity": "sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ=="
|
||||
"version": "14.2.35",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
|
||||
"integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.4.tgz",
|
||||
"integrity": "sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
|
||||
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -200,9 +200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz",
|
||||
"integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
|
||||
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -215,9 +215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz",
|
||||
"integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
|
||||
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -230,9 +230,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz",
|
||||
"integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
|
||||
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -245,9 +245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz",
|
||||
"integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
|
||||
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -260,9 +260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz",
|
||||
"integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
|
||||
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -275,9 +275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz",
|
||||
"integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
|
||||
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -290,9 +290,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz",
|
||||
"integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
|
||||
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -305,9 +305,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz",
|
||||
"integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==",
|
||||
"version": "14.2.33",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
|
||||
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -354,11 +354,17 @@
|
||||
"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": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz",
|
||||
"integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==",
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
|
||||
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
@@ -485,9 +491,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -495,11 +501,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -576,9 +582,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001568",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz",
|
||||
"integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==",
|
||||
"version": "1.0.30001761",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
|
||||
"integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -837,9 +843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -933,11 +939,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": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@@ -1172,12 +1173,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1218,9 +1219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -1235,18 +1236,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.0.4",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.0.4.tgz",
|
||||
"integrity": "sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==",
|
||||
"version": "14.2.35",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
|
||||
"integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
|
||||
"dependencies": {
|
||||
"@next/env": "14.0.4",
|
||||
"@swc/helpers": "0.5.2",
|
||||
"@next/env": "14.2.35",
|
||||
"@swc/helpers": "0.5.5",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001406",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"postcss": "8.4.31",
|
||||
"styled-jsx": "5.1.1",
|
||||
"watchpack": "2.4.0"
|
||||
"styled-jsx": "5.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"next": "dist/bin/next"
|
||||
@@ -1255,18 +1255,19 @@
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.0.4",
|
||||
"@next/swc-darwin-x64": "14.0.4",
|
||||
"@next/swc-linux-arm64-gnu": "14.0.4",
|
||||
"@next/swc-linux-arm64-musl": "14.0.4",
|
||||
"@next/swc-linux-x64-gnu": "14.0.4",
|
||||
"@next/swc-linux-x64-musl": "14.0.4",
|
||||
"@next/swc-win32-arm64-msvc": "14.0.4",
|
||||
"@next/swc-win32-ia32-msvc": "14.0.4",
|
||||
"@next/swc-win32-x64-msvc": "14.0.4"
|
||||
"@next/swc-darwin-arm64": "14.2.33",
|
||||
"@next/swc-darwin-x64": "14.2.33",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.33",
|
||||
"@next/swc-linux-arm64-musl": "14.2.33",
|
||||
"@next/swc-linux-x64-gnu": "14.2.33",
|
||||
"@next/swc-linux-x64-musl": "14.2.33",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.33",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.33",
|
||||
"@next/swc-win32-x64-msvc": "14.2.33"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.3.0"
|
||||
@@ -1275,6 +1276,9 @@
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"@playwright/test": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
}
|
||||
@@ -1693,21 +1697,6 @@
|
||||
"integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==",
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -1896,15 +1885,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"rimraf": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.17.0"
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
@@ -1919,9 +1905,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
@@ -1968,18 +1954,6 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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 = {
|
||||
'accept': '*/*',
|
||||
@@ -102,6 +102,7 @@ export default async (req, res) => {
|
||||
provider: "TapTap",
|
||||
bestRate: false,
|
||||
href: "https://www.taptapsend.com/",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSFcB9ECgZxEULIhxf-PZWEn23UPZATQZblQQ&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -124,6 +125,7 @@ export default async (req, res) => {
|
||||
provider: "Send",
|
||||
bestRate: false,
|
||||
href: "https://send.flutterwave.com/",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRyhQXYEg8iHCMajGdJn0B0FxwsuzAfkSeBgQ&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -131,54 +133,11 @@ export default async (req, res) => {
|
||||
return createDefaultErrorResponse("Send", "https://send.flutterwave.com/");
|
||||
});
|
||||
|
||||
const curlCommand = `curl -v 'https://acemoneytransfer.com/make-request' \
|
||||
-H 'authority: acemoneytransfer.com' \
|
||||
-H 'accept: */*' \
|
||||
-H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \
|
||||
-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");
|
||||
});
|
||||
// Ace Transfer temporarily disabled due to Cloudflare protection
|
||||
// Will return a default response to avoid blocking the API
|
||||
const aceCall = Promise.resolve(
|
||||
createDefaultErrorResponse("Ace Transfer", "https://acemoneytransfer.com/referral-link/3056004")
|
||||
);
|
||||
|
||||
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", {
|
||||
@@ -198,6 +157,7 @@ export default async (req, res) => {
|
||||
provider: "Remitly",
|
||||
bestRate: false,
|
||||
href: "https://remit.ly/94fqcne9",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRNZm0irEt8u6LXo-OFXZ9bcTchNoSeaDDnsA&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -225,6 +185,7 @@ export default async (req, res) => {
|
||||
provider: "Nala",
|
||||
bestRate: false,
|
||||
href: "https://join.iwantnala.com/JESSE-715106",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRpbV0MWgCocA3dFuWoxgCmQsxGfqRuttbD3w&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -257,10 +218,11 @@ export default async (req, res) => {
|
||||
.catch((error) => reject(error));
|
||||
}).then((result) => {
|
||||
return {
|
||||
rate: Math.floor(result.data.rate % 10000),
|
||||
rate: 0,
|
||||
provider: "Lemfi(referal code - OBIN6518)",
|
||||
bestRate: false,
|
||||
href: "https://lemfi.com",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSMYTBiPB_joupdSSoBL1fA0wySDPcMpY92XQ&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -284,6 +246,7 @@ export default async (req, res) => {
|
||||
provider: "Afriex",
|
||||
bestRate: false,
|
||||
href: "https://www.afriex.com/",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTvFac7oIDG6QsJlYsBIRnWhFfy0OWjN5Mspg&s"
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -323,6 +286,7 @@ export default async (req, res) => {
|
||||
provider: "Wise",
|
||||
bestRate: false,
|
||||
href: "https://wise.com/invite/dic/obinnae93",
|
||||
logo: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3L26sFmjg2xyW4CJy_5peQt-v0JHgsYhzvA&s"
|
||||
};
|
||||
} else {
|
||||
throw new Error("Wise provider or quotes not found");
|
||||
@@ -339,7 +303,8 @@ export default async (req, res) => {
|
||||
nalaCall,
|
||||
lemfiCall,
|
||||
afriexCall,
|
||||
wiseCall
|
||||
wiseCall,
|
||||
sendCall
|
||||
])
|
||||
.then(
|
||||
([
|
||||
@@ -349,7 +314,8 @@ export default async (req, res) => {
|
||||
nalaResponse,
|
||||
lemfiResponse,
|
||||
afriexResponse,
|
||||
wiseResponse
|
||||
wiseResponse,
|
||||
sendResponse
|
||||
]) => {
|
||||
rates = [
|
||||
aceResponse,
|
||||
@@ -358,7 +324,8 @@ export default async (req, res) => {
|
||||
nalaResponse,
|
||||
lemfiResponse,
|
||||
afriexResponse,
|
||||
wiseResponse
|
||||
wiseResponse,
|
||||
sendResponse
|
||||
];
|
||||
}
|
||||
)
|
||||
|
||||
+39
-54
@@ -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() {
|
||||
return (
|
||||
<div className="container">
|
||||
<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>
|
||||
<Layout showNavLinks={false}>
|
||||
<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.
|
||||
<h1 className="hero-title">
|
||||
Hey, I'm <AnimatedName />
|
||||
</h1>
|
||||
<div className="hero-main">
|
||||
<div className="hero-content">
|
||||
|
||||
<p className="hero-subtitle">
|
||||
A software engineer building systems that scale in Amsterdam
|
||||
</p>
|
||||
|
||||
<p className="hero__description">
|
||||
In the last 5 years I've been building services and products in the finance, ecommerce
|
||||
and logistics domain, enriching lives using technologies like Python(Django,
|
||||
FastAPI), Javascript(React, Node.js), PostgreSQL, Docker, Redis, Kubernetes etc.
|
||||
<div className="hero-description">
|
||||
<p>
|
||||
I architect and scale backend systems that handle millions of requests.
|
||||
From payment processing to logistics orchestration, I turn complex
|
||||
technical challenges into reliable production infrastructure.
|
||||
</p>
|
||||
|
||||
<p className="hero__description">
|
||||
I'm constantly looking for challenging and impactful opportunities so can always shoot
|
||||
me an{' '}
|
||||
<a className="text-underline email-cta" href="mailto:me@jesseinit.dev">
|
||||
email
|
||||
</a>
|
||||
.
|
||||
<p>
|
||||
6 years shipping products across <strong>finance</strong>, <strong>ecommerce</strong>, and <strong>logistics</strong>.
|
||||
I work with Python (Django, FastAPI), JavaScript (React, Node.js), PostgreSQL,
|
||||
Docker, Redis, and Kubernetes to build systems that just work.
|
||||
</p>
|
||||
</aside>
|
||||
<aside className="hero__aside text-align-center">
|
||||
</div>
|
||||
|
||||
<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
|
||||
src="hero.jpg"
|
||||
className="hero__image"
|
||||
alt="Jesse Egbosionu"
|
||||
className="hero-image"
|
||||
/>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
<footer className="footer">
|
||||
<p className="text-align-center">Made with <span style={{ "color": "red" }}>❤️</span> and Vibes</p>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+106
-38
@@ -1,20 +1,24 @@
|
||||
import Head from "next/head";
|
||||
import { useEffect, useState } from "react";
|
||||
import Layout from "../components/Layout";
|
||||
import RateCard from "../components/RateCard";
|
||||
import { fetchData } from "../utils/api";
|
||||
|
||||
export default function Rates() {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDataFromApi = async () => {
|
||||
try {
|
||||
const apiData = await fetchData();
|
||||
setData(apiData);
|
||||
setError(null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
setError("Failed to load exchange rates. Please try again later.");
|
||||
} finally {
|
||||
setLoading(false); // Set loading to false regardless of success or error
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,48 +32,112 @@ export default function Rates() {
|
||||
}).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 (
|
||||
<div className="container">
|
||||
<Head>
|
||||
<title>EUR Rates</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>
|
||||
<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" : ""
|
||||
}
|
||||
<Layout
|
||||
title="EUR to NGN Exchange Rates - Jesse Egbosionu"
|
||||
description="Compare live EUR to NGN exchange rates across multiple providers."
|
||||
showNavLinks={false}
|
||||
>
|
||||
<a href={item.href}>{item.provider}:</a>
|
||||
<span className="rate">
|
||||
{formatCurrency(item.rate)}
|
||||
</span>
|
||||
<section className="rates-section">
|
||||
<div className="rates-header">
|
||||
<div className="rates-title-group">
|
||||
<h1 className="rates-title">Exchange Rates</h1>
|
||||
<p className="rates-subtitle">
|
||||
Compare live EUR to NGN rates across providers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
)
|
||||
)}
|
||||
|
||||
{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>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
+841
-99
@@ -5,144 +5,886 @@
|
||||
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;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 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 {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
max-width: 960px;
|
||||
min-width: 360px;
|
||||
padding: 0 20px;
|
||||
max-width: 1200px;
|
||||
padding: 0 var(--spacing-lg);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// Page Styles
|
||||
|
||||
// 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;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__logo a {
|
||||
font-size: 1.2em;
|
||||
padding: 3px 0;
|
||||
border-bottom: 3px solid rgb(0, 119, 255);
|
||||
.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: #000;
|
||||
padding-bottom: 1px;
|
||||
color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
&.cta {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-accent);
|
||||
border: 1px solid var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__navbar ul {
|
||||
// MAIN CONTENT
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
padding: var(--spacing-xl) 0;
|
||||
}
|
||||
|
||||
&__navbar li:not(:last-child) {
|
||||
margin-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
// HERO AREA
|
||||
// HERO SECTION
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column-reverse;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
bottom: 0;
|
||||
font-size: 0.9em;
|
||||
margin: 20px 0;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user