feat: introduce interactive 3D globe with country borders and GitHub activity feed.
This commit is contained in:
57
package-lock.json
generated
57
package-lock.json
generated
@@ -11,11 +11,13 @@
|
|||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.4.2",
|
"@react-three/fiber": "^9.4.2",
|
||||||
"@studio-freight/lenis": "^1.0.42",
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
|
"d3-geo": "^3.1.1",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"simplex-noise": "^4.0.3",
|
"simplex-noise": "^4.0.3",
|
||||||
"three": "^0.182.0"
|
"three": "^0.182.0",
|
||||||
|
"topojson-client": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@@ -1899,6 +1901,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1951,6 +1959,30 @@
|
|||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-geo": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.5.0 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -2502,6 +2534,15 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -3233,6 +3274,20 @@
|
|||||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/topojson-client": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"topo2geo": "bin/topo2geo",
|
||||||
|
"topomerge": "bin/topomerge",
|
||||||
|
"topoquantize": "bin/topoquantize"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/troika-three-text": {
|
"node_modules/troika-three-text": {
|
||||||
"version": "0.52.4",
|
"version": "0.52.4",
|
||||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.4.2",
|
"@react-three/fiber": "^9.4.2",
|
||||||
"@studio-freight/lenis": "^1.0.42",
|
"@studio-freight/lenis": "^1.0.42",
|
||||||
|
"d3-geo": "^3.1.1",
|
||||||
"gsap": "^3.14.2",
|
"gsap": "^3.14.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"simplex-noise": "^4.0.3",
|
"simplex-noise": "^4.0.3",
|
||||||
"three": "^0.182.0"
|
"three": "^0.182.0",
|
||||||
|
"topojson-client": "^3.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
|||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import HeroModel from './canvas/HeroModel';
|
import HeroModel from './canvas/HeroModel';
|
||||||
import ProductGrid from './components/ProductGrid';
|
import ProductGrid from './components/ProductGrid';
|
||||||
|
import WhereAmI from './components/WhereAmI';
|
||||||
import Footer from './components/Footer';
|
import Footer from './components/Footer';
|
||||||
import './styles/index.css';
|
import './styles/index.css';
|
||||||
import gsap from 'gsap';
|
import gsap from 'gsap';
|
||||||
@@ -109,6 +110,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Product Grid Section */}
|
{/* Product Grid Section */}
|
||||||
<ProductGrid />
|
<ProductGrid />
|
||||||
|
<WhereAmI />
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
BIN
src/assets/earth_texture.jpg
Normal file
BIN
src/assets/earth_texture.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
139
src/canvas/Globe.jsx
Normal file
139
src/canvas/Globe.jsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import React, { useRef, useMemo, useState, useEffect } from 'react';
|
||||||
|
import { Canvas, useFrame } from '@react-three/fiber';
|
||||||
|
import { OrbitControls } from '@react-three/drei';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import * as d3 from 'd3-geo';
|
||||||
|
import * as topojson from 'topojson-client';
|
||||||
|
|
||||||
|
// Helper to convert Lat/Lon to Vector3
|
||||||
|
const calcPosFromLatLonRad = (lat, lon, radius) => {
|
||||||
|
const phi = (90 - lat) * (Math.PI / 180);
|
||||||
|
const theta = (lon + 180) * (Math.PI / 180);
|
||||||
|
const x = -(radius * Math.sin(phi) * Math.cos(theta));
|
||||||
|
const z = (radius * Math.sin(phi) * Math.sin(theta));
|
||||||
|
const y = (radius * Math.cos(phi));
|
||||||
|
return [x, y, z];
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobeMesh = () => {
|
||||||
|
const groupRef = useRef();
|
||||||
|
const [bordersTexture, setBordersTexture] = useState(null);
|
||||||
|
|
||||||
|
// Generate Borders Texture using D3
|
||||||
|
useEffect(() => {
|
||||||
|
const generateTexture = async () => {
|
||||||
|
try {
|
||||||
|
// Fetch World Topology (small 110m res)
|
||||||
|
const response = await fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json');
|
||||||
|
const world = await response.json();
|
||||||
|
const countries = topojson.feature(world, world.objects.countries);
|
||||||
|
|
||||||
|
// Setup Offscreen Canvas
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 2048;
|
||||||
|
canvas.height = 1024;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// D3 Projection (Equirectangular matches UV map of Sphere)
|
||||||
|
const projection = d3.geoEquirectangular()
|
||||||
|
.fitSize([2048, 1024], countries)
|
||||||
|
.translate([1024, 512]);
|
||||||
|
|
||||||
|
const path = d3.geoPath(projection, context);
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
context.strokeStyle = '#ffffff'; // White borders
|
||||||
|
context.lineWidth = 2; // Thicker lines
|
||||||
|
context.beginPath();
|
||||||
|
path(countries);
|
||||||
|
context.stroke();
|
||||||
|
|
||||||
|
// Create Texture
|
||||||
|
const texture = new THREE.CanvasTexture(canvas);
|
||||||
|
texture.needsUpdate = true;
|
||||||
|
setBordersTexture(texture);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load globe data", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateTexture();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ireland Coordinates
|
||||||
|
const irelandPos = useMemo(() => calcPosFromLatLonRad(53.35, -6.26, 2.02), []);
|
||||||
|
|
||||||
|
useFrame((state, delta) => {
|
||||||
|
if (groupRef.current) {
|
||||||
|
groupRef.current.rotation.y += delta * 0.05; // Gentle Auto-rotation
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
{/* 1. Base Dark Sphere (blocks background stars/wireframe from showing through backface) */}
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1.95, 64, 64]} />
|
||||||
|
<meshBasicMaterial color="#000000" />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* 2. Light Wireframe Sphere - Outer Cage */}
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[2.0, 32, 32]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#444"
|
||||||
|
wireframe={true}
|
||||||
|
transparent
|
||||||
|
opacity={0.15}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
{/* 3. Borders Sphere (Texture) */}
|
||||||
|
{bordersTexture && (
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[2.01, 64, 64]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={bordersTexture}
|
||||||
|
transparent={true}
|
||||||
|
opacity={0.8}
|
||||||
|
side={THREE.DoubleSide}
|
||||||
|
blending={THREE.AdditiveBlending}
|
||||||
|
depthWrite={false}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ireland Marker */}
|
||||||
|
<mesh position={irelandPos}>
|
||||||
|
<sphereGeometry args={[0.04, 16, 16]} />
|
||||||
|
<meshBasicMaterial color="#ff4d00" />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={irelandPos}>
|
||||||
|
<ringGeometry args={[0.06, 0.09, 32]} />
|
||||||
|
<meshBasicMaterial color="#ff4d00" side={THREE.DoubleSide} transparent opacity={0.6} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Globe = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', height: '100%', minHeight: '400px', cursor: 'grab' }}>
|
||||||
|
<Canvas camera={{ position: [0, 0, 5.5], fov: 45 }}>
|
||||||
|
<ambientLight intensity={1} />
|
||||||
|
<pointLight position={[10, 10, 10]} />
|
||||||
|
|
||||||
|
<GlobeMesh />
|
||||||
|
|
||||||
|
<OrbitControls
|
||||||
|
enableZoom={false}
|
||||||
|
enablePan={false}
|
||||||
|
minPolarAngle={Math.PI / 4}
|
||||||
|
maxPolarAngle={Math.PI / 1.5}
|
||||||
|
/>
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Globe;
|
||||||
117
src/components/GithubHistory.jsx
Normal file
117
src/components/GithubHistory.jsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const GithubHistory = () => {
|
||||||
|
const [events, setEvents] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEvents = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/users/MatissJurevics/events/public?per_page=5');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch GitHub history');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setEvents(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEvents();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getEventMessage = (event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'PushEvent':
|
||||||
|
return `Pushed to ${event.repo.name.replace('MatissJurevics/', '')}`;
|
||||||
|
case 'CreateEvent':
|
||||||
|
return `Created ${event.payload.ref_type} in ${event.repo.name.replace('MatissJurevics/', '')}`;
|
||||||
|
case 'WatchEvent':
|
||||||
|
return `Starred ${event.repo.name.replace('MatissJurevics/', '')}`;
|
||||||
|
case 'PullRequestEvent':
|
||||||
|
return `${event.payload.action} PR in ${event.repo.name.replace('MatissJurevics/', '')}`;
|
||||||
|
default:
|
||||||
|
return `${event.type} on ${event.repo.name.replace('MatissJurevics/', '')}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#666', fontFamily: 'monospace' }}>
|
||||||
|
LOADING HISTORY...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ff4d00', fontFamily: 'monospace' }}>
|
||||||
|
ERROR LOAD GITHUB DATA
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section style={{
|
||||||
|
height: '600px',
|
||||||
|
background: '#0a0a0a',
|
||||||
|
color: '#e4e4e4',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '60px 20px',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: '600px', // Narrower container for list
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<h2 className="uppercase" style={{ fontSize: '3rem', marginBottom: '40px', lineHeight: 1, textAlign: 'center' }}>
|
||||||
|
Github <br /> History
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
|
||||||
|
{events.map(event => (
|
||||||
|
<div key={event.id} style={{
|
||||||
|
borderLeft: '2px solid #333',
|
||||||
|
paddingLeft: '20px',
|
||||||
|
transition: 'border-color 0.3s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.borderColor = '#ff4d00'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#333'}
|
||||||
|
>
|
||||||
|
<p className="mono" style={{ fontSize: '0.8rem', color: '#666', marginBottom: '5px' }}>
|
||||||
|
{formatDate(event.created_at)}
|
||||||
|
</p>
|
||||||
|
<a href={`https://github.com/${event.repo.name}`} target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
|
<p style={{ fontSize: '1.2rem', fontWeight: 'bold' }}>
|
||||||
|
{getEventMessage(event)}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ marginTop: '40px', textAlign: 'center' }}>
|
||||||
|
<a href="https://github.com/MatissJurevics" target="_blank" rel="noopener noreferrer" className="mono" style={{ color: '#ff4d00', textDecoration: 'none', fontSize: '0.9rem' }}>
|
||||||
|
VIEW FULL PROFILE →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GithubHistory;
|
||||||
@@ -31,11 +31,7 @@ const Header = () => {
|
|||||||
mj
|
mj
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav style={navStyle}>
|
|
||||||
<a href="#work">Work</a>
|
|
||||||
<a href="#about">About</a>
|
|
||||||
<a href="#contact">Contact</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
63
src/components/InfoTabs.jsx
Normal file
63
src/components/InfoTabs.jsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import WhereAmI from './WhereAmI';
|
||||||
|
import GithubHistory from './GithubHistory';
|
||||||
|
|
||||||
|
const InfoTabs = () => {
|
||||||
|
const [activeTab, setActiveTab] = useState('location');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ background: '#0a0a0a' }}>
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: '40px',
|
||||||
|
paddingTop: '60px',
|
||||||
|
borderBottom: '1px solid #222'
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('location')}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: activeTab === 'location' ? '2px solid #ff4d00' : '2px solid transparent',
|
||||||
|
color: activeTab === 'location' ? '#fff' : '#666',
|
||||||
|
padding: '10px 20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Where Am I?
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('github')}
|
||||||
|
style={{
|
||||||
|
background: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: activeTab === 'github' ? '2px solid #ff4d00' : '2px solid transparent',
|
||||||
|
color: activeTab === 'github' ? '#fff' : '#666',
|
||||||
|
padding: '10px 20px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Github History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<div>
|
||||||
|
{activeTab === 'location' && <WhereAmI />}
|
||||||
|
{activeTab === 'github' && <GithubHistory />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoTabs;
|
||||||
64
src/components/WhereAmI.jsx
Normal file
64
src/components/WhereAmI.jsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Globe from '../canvas/Globe';
|
||||||
|
|
||||||
|
const WhereAmI = () => {
|
||||||
|
return (
|
||||||
|
<section style={{
|
||||||
|
height: '600px',
|
||||||
|
background: '#0a0a0a', // Dark Background
|
||||||
|
color: '#e4e4e4', // Light Text
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '60px 20px',
|
||||||
|
position: 'relative'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
maxWidth: '1400px',
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
|
flexWrap: 'wrap'
|
||||||
|
}}>
|
||||||
|
{/* Text Content */}
|
||||||
|
<div style={{ flex: '1', minWidth: '300px', paddingRight: '40px' }}>
|
||||||
|
<h2 className="uppercase" style={{ fontSize: '3rem', marginBottom: '20px', lineHeight: 1 }}>
|
||||||
|
Where <br /> Am I?
|
||||||
|
</h2>
|
||||||
|
<div style={{ borderLeft: '2px solid #ff4d00', paddingLeft: '20px' }}>
|
||||||
|
<p className="mono" style={{ fontSize: '1rem', color: '#888', marginBottom: '5px' }}>
|
||||||
|
CURRENT LOCATION
|
||||||
|
</p>
|
||||||
|
<p style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
|
||||||
|
Dublin, Ireland
|
||||||
|
</p>
|
||||||
|
<p style={{ marginTop: '20px', color: '#ccc', maxWidth: '400px' }}>
|
||||||
|
Based in the heart of Dublin. Working globally with clients to build digital products that matter.
|
||||||
|
</p>
|
||||||
|
<p className="mono" style={{ marginTop: '10px', fontSize: '0.8rem', color: '#666' }}>
|
||||||
|
53.3498° N, 6.2603° W
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Globe Visual */}
|
||||||
|
<div style={{
|
||||||
|
flex: '1',
|
||||||
|
minWidth: '300px',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: '400px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<Globe />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WhereAmI;
|
||||||
Reference in New Issue
Block a user