status dashboard and other things
This commit is contained in:
parent
fdd851489c
commit
0a8d7355e0
|
|
@ -0,0 +1,2 @@
|
|||
VITE_CG_API_KEY=CG-6tRLtAyJ9t5YdthDmRPhwKRD
|
||||
VITE_CG_API_URL=https://pro-api.coingecko.com/api/v3
|
||||
43
src/App.tsx
43
src/App.tsx
|
|
@ -6,39 +6,44 @@ import { Achievements } from './components/Achievements';
|
|||
import { Skills } from './components/Skills';
|
||||
import { NFTGrid } from './components/NFTGrid';
|
||||
import { Contact } from './components/Contact';
|
||||
import ReactorStatus from './components/ReactorStatus';
|
||||
|
||||
const App: FC = () => {
|
||||
return (
|
||||
// Add base font for general content
|
||||
<div className="bg-black font-['Space_Mono']">
|
||||
{/* Navigation can use Space Mono for clean reading */}
|
||||
<Navigation />
|
||||
|
||||
<main className="relative">
|
||||
{/* Hero section uses Courier Prime for the terminal look */}
|
||||
<section id="home" className="relative z-10 font-['Courier_Prime']">
|
||||
{/* Hero keeps min-h-screen as it's the landing section */}
|
||||
<section id="home" className="relative z-10 min-h-screen font-['Courier_Prime']">
|
||||
<Hero />
|
||||
</section>
|
||||
|
||||
{/* Content sections use Space Mono for better readability */}
|
||||
<div className="relative z-10">
|
||||
<Expertise />
|
||||
</div>
|
||||
{/* Content sections with reduced spacing */}
|
||||
<div className="space-y-16 relative z-10"> {/* Main content wrapper with consistent spacing */}
|
||||
<section className="py-12">
|
||||
<Expertise />
|
||||
</section>
|
||||
|
||||
<div className="relative z-10">
|
||||
<Achievements />
|
||||
</div>
|
||||
<section className="py-12">
|
||||
<ReactorStatus />
|
||||
</section>
|
||||
|
||||
<div className="relative z-10">
|
||||
<Skills />
|
||||
</div>
|
||||
<section className="py-12">
|
||||
<Achievements />
|
||||
</section>
|
||||
|
||||
<div className="relative z-10">
|
||||
<NFTGrid />
|
||||
</div>
|
||||
<section className="py-12">
|
||||
<Skills />
|
||||
</section>
|
||||
|
||||
<div className="relative z-10">
|
||||
<Contact />
|
||||
<section className="py-12">
|
||||
<NFTGrid />
|
||||
</section>
|
||||
|
||||
<section className="py-12">
|
||||
<Contact />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Background Elements */}
|
||||
|
|
|
|||
|
|
@ -59,12 +59,11 @@ const GrantChart: React.FC<GrantChartProps> = ({ data, className = '' }) => (
|
|||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="name"
|
||||
type="category"
|
||||
width={200}
|
||||
tick={(props) => <CustomYAxisTick {...props} />}
|
||||
dataKey="name"
|
||||
type="category"
|
||||
width={200}
|
||||
tick={(props) => <CustomYAxisTick {...props} />}
|
||||
/>
|
||||
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Bar dataKey="amount" fill="#3b82f6" radius={[0, 4, 4, 0]} />
|
||||
</BarChart>
|
||||
|
|
@ -86,9 +85,9 @@ const AchievementCard: React.FC<AchievementCardProps> = ({ title, description, d
|
|||
|
||||
export const Achievements: React.FC<AchievementsProps> = ({ className = '', grants = defaultGrants, achievements = defaultAchievements }) => {
|
||||
return (
|
||||
<section id="achievements" className={`min-h-screen bg-black text-white p-8 ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-16">
|
||||
<div className="space-y-8">
|
||||
<div className={`bg-black text-white ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-12">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<BarChart3 size={32} className="text-green-500" />
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-green-500 to-blue-500 bg-clip-text text-transparent">
|
||||
|
|
@ -97,7 +96,7 @@ export const Achievements: React.FC<AchievementsProps> = ({ className = '', gran
|
|||
</div>
|
||||
<GrantChart data={grants} />
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Award size={32} className="text-yellow-500" />
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-yellow-500 to-orange-500 bg-clip-text text-transparent">
|
||||
|
|
@ -111,6 +110,6 @@ export const Achievements: React.FC<AchievementsProps> = ({ className = '', gran
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
@ -44,7 +44,7 @@ const CategorySection: FC<CategorySectionProps> = ({
|
|||
links,
|
||||
className = ''
|
||||
}) => (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
<h3 className="text-xl font-medium text-gray-400">
|
||||
{category}
|
||||
</h3>
|
||||
|
|
@ -61,14 +61,14 @@ export const Contact: FC<ContactProps> = ({
|
|||
socialLinks = defaultSocialLinks
|
||||
}) => {
|
||||
return (
|
||||
<section id="contact" className={`min-h-screen bg-black text-white p-8 ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`bg-black text-white ${className}`}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-green-500 to-blue-500 bg-clip-text text-transparent">
|
||||
Connect With Me
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid gap-12 mt-8">
|
||||
<div className="grid gap-8">
|
||||
{socialLinks.map((category, index) => (
|
||||
<CategorySection
|
||||
key={index}
|
||||
|
|
@ -78,7 +78,7 @@ export const Contact: FC<ContactProps> = ({
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const Section: React.FC<SectionProps> = ({
|
|||
gradientTo,
|
||||
id
|
||||
}) => (
|
||||
<section id={id} className="space-y-8">
|
||||
<section id={id} className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Icon size={32} className={`text-${gradientFrom}`} />
|
||||
<h2 className={`text-3xl font-bold bg-gradient-to-r from-${gradientFrom} to-${gradientTo} bg-clip-text text-transparent`}>
|
||||
|
|
@ -107,8 +107,8 @@ export const Expertise: React.FC<ExpertiseProps> = ({
|
|||
web3Experience = defaultWeb3Experience
|
||||
}) => {
|
||||
return (
|
||||
<div className={`min-h-screen bg-black text-white p-8 ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-16">
|
||||
<div className={`bg-black text-white ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-12">
|
||||
<Section
|
||||
id="industry"
|
||||
title="Industrial Expertise"
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ export const Hero: React.FC<HeroProps> = ({
|
|||
}, [introText]);
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen bg-black text-white flex flex-col items-center justify-center p-4 ${className}`}>
|
||||
<div className="w-full max-w-3xl">
|
||||
<div className={`min-h-screen bg-black text-white flex flex-col items-center justify-center ${className}`}>
|
||||
<div className="w-full max-w-3xl px-4">
|
||||
{/* Terminal Window */}
|
||||
<div className="bg-gray-900 rounded-lg overflow-hidden shadow-xl mb-8">
|
||||
<div className="bg-gray-900 rounded-lg overflow-hidden shadow-xl mb-6">
|
||||
<div className="bg-gray-800 px-4 py-2 flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
|
|
@ -95,9 +95,9 @@ export const Hero: React.FC<HeroProps> = ({
|
|||
|
||||
{/* Quick Links Section */}
|
||||
{animationState.showContent && (
|
||||
<div className="space-y-8 opacity-0 animate-fade-in">
|
||||
<div className="space-y-6 opacity-0 animate-fade-in">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent mb-4">
|
||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent mb-3">
|
||||
Bridging Industry & Web3
|
||||
</h1>
|
||||
<p className="text-gray-400 text-lg">
|
||||
|
|
@ -105,7 +105,7 @@ export const Hero: React.FC<HeroProps> = ({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{quickLinks.map((link, index) => (
|
||||
<a
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,11 @@ const NFTCard: React.FC<NFTCardProps> = ({
|
|||
className="w-full h-full object-cover transform group-hover:scale-105 transition-transform duration-500"
|
||||
/>
|
||||
{/* Overlay */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent
|
||||
transition-opacity duration-300 flex items-end justify-between p-6
|
||||
${isHovered ? 'opacity-100' : 'opacity-0'}`}>
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent
|
||||
transition-opacity duration-300 flex items-end justify-between p-6
|
||||
${isHovered ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<h3 className="text-white text-xl font-medium">{name}</h3>
|
||||
{link && (
|
||||
<a
|
||||
|
|
@ -80,11 +82,11 @@ export const NFTGrid: React.FC<NFTGridProps> = ({
|
|||
nfts = defaultNFTs
|
||||
}) => {
|
||||
const [hoveredNFT, setHoveredNFT] = useState<number | null>(null);
|
||||
|
||||
|
||||
return (
|
||||
<section id="nfts" className={`min-h-screen bg-black p-8 flex items-center justify-center ${className}`}>
|
||||
<div className="max-w-6xl w-full">
|
||||
<div className="mb-8">
|
||||
<div className={`bg-black ${className}`}>
|
||||
<div className="max-w-6xl w-full mx-auto">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
|
||||
PFP's
|
||||
|
|
@ -93,7 +95,7 @@ export const NFTGrid: React.FC<NFTGridProps> = ({
|
|||
<p className="text-gray-400 text-lg mt-2">My Online Appearances</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{nfts.map((nft) => (
|
||||
<NFTCard
|
||||
key={nft.id}
|
||||
|
|
@ -104,6 +106,6 @@ export const NFTGrid: React.FC<NFTGridProps> = ({
|
|||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Activity, Power, Atom, Coffee, TrendingUp, Wallet } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import type { ReactorStatusProps, TooltipProps, PriceStat } from './types';
|
||||
import { getEthereumData, formatPrice, getHighLowPrices, formatVolume } from '../../utils/api';
|
||||
|
||||
const statusMessages = [
|
||||
'Staking valves open',
|
||||
'Consensus achieved',
|
||||
'Smart contracts cooling',
|
||||
'Tokens well-contained',
|
||||
'DAOs running smoothly',
|
||||
'Radiation levels nominal',
|
||||
'Governance protocols stable',
|
||||
'Core temperature optimal',
|
||||
'NFTs properly shielded',
|
||||
'Blockchain fully operational'
|
||||
];
|
||||
|
||||
const CustomTooltip = ({ active, payload, label }: TooltipProps) => {
|
||||
if (active && payload && payload.length && label) {
|
||||
return (
|
||||
<div className="bg-gray-800 p-2 rounded border border-gray-700">
|
||||
<p className="text-blue-400">{formatPrice(payload[0].value)}</p>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{new Date(parseInt(label)).toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const ReactorStatus = ({ className = '' }: ReactorStatusProps) => {
|
||||
const [power, setPower] = useState(0);
|
||||
const [status, setStatus] = useState('Initializing...');
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [priceData, setPriceData] = useState<Array<{ time: number; price: number }>>([]);
|
||||
const [priceStats, setPriceStats] = useState<PriceStat[]>([
|
||||
{ label: '24h High', value: '...' },
|
||||
{ label: '24h Low', value: '...' },
|
||||
{ label: '24h Volume', value: '...' }
|
||||
]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const data = await getEthereumData();
|
||||
console.log('Received ETH data:', data);
|
||||
|
||||
if (!data?.prices?.length) {
|
||||
throw new Error('Invalid price data received');
|
||||
}
|
||||
|
||||
const formattedPrices = data.prices.map(([timestamp, price]) => ({
|
||||
time: timestamp,
|
||||
price
|
||||
}));
|
||||
|
||||
const { high, low } = getHighLowPrices(data.prices);
|
||||
const volume = data.total_volumes[data.total_volumes.length - 1][1];
|
||||
|
||||
console.log('Formatted prices:', formattedPrices);
|
||||
setPriceData(formattedPrices);
|
||||
setPriceStats([
|
||||
{ label: '24h High', value: formatPrice(high) },
|
||||
{ label: '24h Low', value: formatPrice(low) },
|
||||
{ label: '24h Volume', value: formatVolume(volume) }
|
||||
]);
|
||||
} catch (err) {
|
||||
console.error('Error fetching ETH data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch price data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial fetch
|
||||
fetchData();
|
||||
|
||||
// Set up intervals
|
||||
const intervals = [
|
||||
setInterval(() => setCurrentTime(new Date()), 1000),
|
||||
setInterval(() => {
|
||||
setPower(prev => {
|
||||
const fluctuation = Math.random() * 10 - 5;
|
||||
return Math.min(Math.max(prev + fluctuation, 60), 100);
|
||||
});
|
||||
}, 3000),
|
||||
setInterval(() => {
|
||||
const randomStatus = statusMessages[Math.floor(Math.random() * statusMessages.length)];
|
||||
setStatus(randomStatus);
|
||||
setIsAnimating(true);
|
||||
setTimeout(() => setIsAnimating(false), 500);
|
||||
}, 5000),
|
||||
setInterval(fetchData, 300000) // Fetch new data every 5 minutes
|
||||
];
|
||||
|
||||
// Cleanup
|
||||
return () => intervals.forEach(clearInterval);
|
||||
}, []);
|
||||
|
||||
const statusIndicators = [
|
||||
{ label: 'Core Temp', value: '37.2°C' },
|
||||
{ label: 'Block Height', value: '#18M' },
|
||||
{ label: 'Rad Level', value: '0.12 μSv' }
|
||||
];
|
||||
|
||||
const renderPriceChart = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-blue-500 flex items-center gap-2">
|
||||
<Atom className="animate-spin" size={20} />
|
||||
<span>Loading price data...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-red-500">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!priceData.length) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-gray-400">No price data available</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={priceData}>
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke="#4B5563"
|
||||
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||
tickFormatter={(timestamp) => new Date(timestamp).toLocaleTimeString()}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#4B5563"
|
||||
tick={{ fill: '#9CA3AF', fontSize: 12 }}
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={(value) => formatPrice(value)}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="price"
|
||||
stroke="#3B82F6"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${className}`}>
|
||||
{/* Reactor Status Panel */}
|
||||
<div className="bg-gray-900 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Atom className="text-blue-500 animate-spin" />
|
||||
<h3 className="text-lg font-bold text-white">Reactor Status</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="text-green-500" />
|
||||
<span className="text-sm text-gray-400">
|
||||
{currentTime.toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Power Level */}
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-400">Power Level</span>
|
||||
<Power className="text-blue-500" />
|
||||
</div>
|
||||
<div className="relative h-4 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="absolute h-full bg-blue-500 transition-all duration-1000"
|
||||
style={{ width: `${power}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-right text-sm text-gray-400">
|
||||
{power.toFixed(1)}% Capacity
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-400">Current Status</span>
|
||||
<Coffee className="text-green-500" />
|
||||
</div>
|
||||
<div className={`text-lg font-medium text-blue-400 ${isAnimating ? 'animate-pulse' : ''}`}>
|
||||
{status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity Indicators */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{statusIndicators.map((item, index) => (
|
||||
<div key={index} className="bg-gray-800 rounded-lg p-3 text-center">
|
||||
<div className="text-sm text-gray-400">{item.label}</div>
|
||||
<div className="text-white font-medium">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ETH Price Monitor */}
|
||||
<div className="bg-gray-900 rounded-lg p-6">
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="text-blue-500" />
|
||||
<h3 className="text-lg font-bold text-white">ETH Price Monitor</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="text-green-500" />
|
||||
<span className="text-sm text-gray-400">Live Feed</span>
|
||||
</div>
|
||||
</div>
|
||||
{priceData.length > 0 && (
|
||||
<div className="bg-gray-800 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Current Price</div>
|
||||
<div className="text-2xl font-bold text-blue-400">
|
||||
{formatPrice(priceData[priceData.length - 1].price)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Price Chart */}
|
||||
<div className="bg-gray-800 rounded-lg p-4 h-[300px]">
|
||||
{renderPriceChart()}
|
||||
</div>
|
||||
|
||||
{/* Price Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{priceStats.map((item, index) => (
|
||||
<div key={index} className="bg-gray-800 rounded-lg p-3 text-center">
|
||||
<div className="text-sm text-gray-400">{item.label}</div>
|
||||
<div className="text-white font-medium">{item.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReactorStatus;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// src/components/ReactorStatus/types.ts
|
||||
|
||||
export interface ReactorStatusProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface TooltipProps {
|
||||
active?: boolean;
|
||||
payload?: Array<{
|
||||
value: number;
|
||||
dataKey?: string;
|
||||
}>;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface PriceStat {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PriceDataPoint {
|
||||
time: number;
|
||||
price: number;
|
||||
}
|
||||
|
|
@ -3,20 +3,20 @@ import { ChevronRight } from 'lucide-react';
|
|||
import type { SkillsProps, SkillCardProps, SkillBarProps, Skill } from './types';
|
||||
|
||||
const SkillBar: FC<SkillBarProps> = ({
|
||||
level,
|
||||
maxLevel = 5,
|
||||
className = ''
|
||||
}) => {
|
||||
const percentage = (level / maxLevel) * 100;
|
||||
return (
|
||||
<div className={`h-2 w-32 bg-gray-800 rounded-full overflow-hidden ${className}`}>
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-emerald-400 to-cyan-500 rounded-full transform origin-left transition-transform duration-1000 ease-out"
|
||||
style={{ transform: `scaleX(${percentage / 100})` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
level,
|
||||
maxLevel = 5,
|
||||
className = ''
|
||||
}) => {
|
||||
const percentage = (level / maxLevel) * 100;
|
||||
return (
|
||||
<div className={`h-2 w-32 bg-gray-800 rounded-full overflow-hidden ${className}`}>
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-emerald-400 to-cyan-500 rounded-full transform origin-left transition-transform duration-1000 ease-out"
|
||||
style={{ transform: `scaleX(${percentage / 100})` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SkillCard: FC<SkillCardProps> = ({
|
||||
skill,
|
||||
|
|
@ -114,25 +114,25 @@ const defaultSkills: Skill[] = [
|
|||
];
|
||||
|
||||
export const Skills: FC<SkillsProps> = ({
|
||||
className = '',
|
||||
skills = defaultSkills
|
||||
}) => {
|
||||
return (
|
||||
<section id="skills" className={`min-h-screen bg-black text-white p-8 ${className}`}>
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-emerald-400 to-cyan-500 bg-clip-text text-transparent">
|
||||
Professional Skills
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
|
||||
{skills.map((skill, index) => (
|
||||
<SkillCard key={index} skill={skill} />
|
||||
))}
|
||||
</div>
|
||||
className = '',
|
||||
skills = defaultSkills
|
||||
}) => {
|
||||
return (
|
||||
<div className={`bg-black text-white ${className}`}>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center gap-4">
|
||||
<h2 className="text-3xl font-bold bg-gradient-to-r from-emerald-400 to-cyan-500 bg-clip-text text-transparent">
|
||||
Professional Skills
|
||||
</h2>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
{skills.map((skill, index) => (
|
||||
<SkillCard key={index} skill={skill} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Skills;
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
// src/utils/api.ts
|
||||
|
||||
const API_KEY = import.meta.env.VITE_CG_API_KEY;
|
||||
|
||||
export interface PriceData {
|
||||
prices: [number, number][];
|
||||
market_caps: [number, number][];
|
||||
total_volumes: [number, number][];
|
||||
}
|
||||
|
||||
export async function getEthereumData(): Promise<PriceData> {
|
||||
try {
|
||||
console.log('Fetching ETH price data...');
|
||||
const response = await fetch(
|
||||
`https://pro-api.coingecko.com/api/v3/coins/ethereum/market_chart?vs_currency=usd&days=1&interval=hourly&x_cg_pro_api_key=${API_KEY}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('API Error Response:', errorText);
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Raw API response:', data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error in getEthereumData:', error);
|
||||
// Return mock data in case of API failure
|
||||
const now = Date.now();
|
||||
const mockData: PriceData = {
|
||||
prices: Array.from({ length: 24 }, (_, i) => {
|
||||
const timestamp = now - (23 - i) * 3600000;
|
||||
const basePrice = 2300;
|
||||
const randomChange = Math.random() * 100 - 50;
|
||||
return [timestamp, basePrice + randomChange] as [number, number];
|
||||
}),
|
||||
market_caps: Array.from({ length: 24 }, (_, i) => {
|
||||
const timestamp = now - (23 - i) * 3600000;
|
||||
return [timestamp, 2300000000] as [number, number];
|
||||
}),
|
||||
total_volumes: Array.from({ length: 24 }, (_, i) => {
|
||||
const timestamp = now - (23 - i) * 3600000;
|
||||
return [timestamp, 500000000] as [number, number];
|
||||
})
|
||||
};
|
||||
return mockData;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatPrice(price: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(price);
|
||||
}
|
||||
|
||||
export function getHighLowPrices(prices: [number, number][]): { high: number; low: number } {
|
||||
const priceValues = prices.map(([, price]) => price);
|
||||
return {
|
||||
high: Math.max(...priceValues),
|
||||
low: Math.min(...priceValues)
|
||||
};
|
||||
}
|
||||
|
||||
export function formatVolume(volume: number): string {
|
||||
if (volume >= 1e9) {
|
||||
return `$${(volume / 1e9).toFixed(1)}B`;
|
||||
}
|
||||
if (volume >= 1e6) {
|
||||
return `$${(volume / 1e6).toFixed(1)}M`;
|
||||
}
|
||||
return `$${volume.toFixed(0)}`;
|
||||
}
|
||||
Loading…
Reference in New Issue