init
This commit is contained in:
commit
04ec42bdce
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
.codegpt
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||||
|
|
||||||
|
- Configure the top-level `parserOptions` property like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||||
|
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||||
|
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import react from 'eslint-plugin-react'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
// Set the react version
|
||||||
|
settings: { react: { version: '18.3' } },
|
||||||
|
plugins: {
|
||||||
|
// Add the react plugin
|
||||||
|
react,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended rules
|
||||||
|
...react.configs.recommended.rules,
|
||||||
|
...react.configs['jsx-runtime'].rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"recharts": "^2.15.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.17.0",
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^9.17.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.16",
|
||||||
|
"globals": "^15.14.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"typescript-eslint": "^8.18.2",
|
||||||
|
"vite": "^6.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,42 @@
|
||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import { Hero } from './components/Hero';
|
||||||
|
import { Navigation } from './components/Navigation';
|
||||||
|
import { Expertise } from './components/Expertise';
|
||||||
|
import { Achievements } from './components/Achievements';
|
||||||
|
import { Skills } from './components/Skills';
|
||||||
|
import { NFTGrid } from './components/NFTGrid';
|
||||||
|
import { Contact } from './components/Contact';
|
||||||
|
|
||||||
|
const App: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-black">
|
||||||
|
<Navigation />
|
||||||
|
|
||||||
|
<main className="relative">
|
||||||
|
<section id="home" className="relative z-10">
|
||||||
|
<Hero />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Expertise />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Achievements />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Skills />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<NFTGrid />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<Contact />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Background Elements */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none">
|
||||||
|
{/* Gradient Orbs */}
|
||||||
|
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-float" />
|
||||||
|
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-10 animate-float-delayed" />
|
||||||
|
|
||||||
|
{/* Grid Pattern */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#1a1a1a_1px,transparent_1px),linear-gradient(to_bottom,#1a1a1a_1px,transparent_1px)] bg-[size:4rem_4rem] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { BarChart3, Award } from 'lucide-react';
|
||||||
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
|
||||||
|
import { AchievementsProps, GrantChartProps, AchievementCardProps, TooltipProps } from './types';
|
||||||
|
import { Grant, Achievement } from '../../types';
|
||||||
|
|
||||||
|
const defaultGrants: Grant[] = [
|
||||||
|
{ name: 'DAOMasons(Plurality Labs)', amount: 150000, color: '#2aae91' },
|
||||||
|
{ name: 'DAOHaus(Moloch DAO)', amount: 90000, color: '#6366f1' },
|
||||||
|
{ name: 'DAOhaus(Optimism)', amount: 90000, color: '#ec4899' },
|
||||||
|
{ name: 'Farcastle(Public Nouns)', amount: 17000, color: '#06b6d4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultAchievements: Achievement[] = [
|
||||||
|
{ title: "Grant Ships Launch", description: "Distributed $100K ARB tokens to fund Web3 game development on Arbitrum", date: "2024" },
|
||||||
|
{ title: "DAO Masons", description: "Co-founded DAO Masons, a product and sevices DAO, designed to make DAOs better", date: "2023" },
|
||||||
|
{ title: "Public HAUS Champion", description: "Led governance activities and secured multiple major grants", date: "2022" },
|
||||||
|
{ title: "Metaguide", description: "Guided Web3 projects in securing grants, building communities, and facilitating funding for innovative blockchain initiatives.", date: "2021" },
|
||||||
|
{ title: "Radiation Protection Technician: EPRI Certification", description: "Qualified as Radiation Protection Technician at OPG and later at Bruce Power", date: "2016/2019" },
|
||||||
|
{ title: "Boilermaker Red Seal", description: "Completed Boilermaker apprenticship and passed Red Seal Exam", date: "2006" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const CustomTooltip: React.FC<TooltipProps> = ({ active, payload }) => {
|
||||||
|
if (active && payload && payload.length) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700">
|
||||||
|
<p className="text-white font-medium">{payload[0].payload.name}</p>
|
||||||
|
<p className="text-blue-400">${payload[0].value.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomYAxisTickProps {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
payload: { value: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomYAxisTick: React.FC<CustomYAxisTickProps> = ({ x, y, payload }) => {
|
||||||
|
const truncatedText = payload.value.length > 20 ? `${payload.value.slice(0, 20)}...` : payload.value;
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fill="#FFFFFF" fontSize={12} dy={5} textAnchor="end">
|
||||||
|
{truncatedText}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GrantChart: React.FC<GrantChartProps> = ({ data, className = '' }) => (
|
||||||
|
<div className={`bg-gray-900 rounded-lg p-6 h-80 ${className}`}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart data={data} layout="vertical">
|
||||||
|
<XAxis type="number" domain={[0, 150000]} tickFormatter={(val) => `$${val.toLocaleString()}`} />
|
||||||
|
<YAxis dataKey="name" type="category" width={200} tick={CustomYAxisTick} />
|
||||||
|
<Tooltip content={<CustomTooltip />} />
|
||||||
|
<Bar dataKey="amount" fill="#3b82f6" radius={[0, 4, 4, 0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const AchievementCard: React.FC<AchievementCardProps> = ({ title, description, date, className = '' }) => (
|
||||||
|
<div className={`group bg-gray-900 rounded-lg p-6 hover:bg-gray-800 transition-colors duration-300 ${className}`}>
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<h3 className="text-xl font-semibold text-white">{title}</h3>
|
||||||
|
<span className="text-sm text-gray-500">{date}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 flex-grow">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
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="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">
|
||||||
|
Grants Secured
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<GrantChart data={grants} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-8">
|
||||||
|
<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">
|
||||||
|
Key Achievements
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<AchievementCard key={index} {...achievement} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import type { Grant, Achievement, TooltipProps as SharedTooltipProps } from '../../types';
|
||||||
|
|
||||||
|
export interface AchievementsProps {
|
||||||
|
className?: string;
|
||||||
|
grants?: Grant[];
|
||||||
|
achievements?: Achievement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrantChartProps {
|
||||||
|
data: Grant[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AchievementCardProps extends Achievement {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TooltipProps = SharedTooltipProps;
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import type { FC } from 'react';
|
||||||
|
import {
|
||||||
|
MdEmail,
|
||||||
|
MdCalendarToday,
|
||||||
|
MdLanguage
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import {
|
||||||
|
FaGithub,
|
||||||
|
FaLinkedin,
|
||||||
|
FaTelegram
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { FaXTwitter } from "react-icons/fa6";
|
||||||
|
import { SiFarcaster } from "react-icons/si";
|
||||||
|
import { BiSearch } from "react-icons/bi";
|
||||||
|
import type { ContactProps, SocialLinkCardProps, CategorySectionProps } from './types';
|
||||||
|
|
||||||
|
const SocialLinkCard: FC<SocialLinkCardProps> = ({
|
||||||
|
name,
|
||||||
|
icon: Icon,
|
||||||
|
url,
|
||||||
|
username,
|
||||||
|
className = ''
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={`group bg-gray-900 rounded-lg p-4 hover:bg-gray-800 transition-all duration-300 ${className}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2 bg-gray-800 rounded-lg group-hover:bg-gray-700 transition-colors">
|
||||||
|
<Icon size={20} className="text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-gray-400">{name}</div>
|
||||||
|
<div className="text-white">{username}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CategorySection: FC<CategorySectionProps> = ({
|
||||||
|
category,
|
||||||
|
links,
|
||||||
|
className = ''
|
||||||
|
}) => (
|
||||||
|
<div className={`space-y-6 ${className}`}>
|
||||||
|
<h3 className="text-xl font-medium text-gray-400">
|
||||||
|
{category}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<SocialLinkCard key={index} {...link} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Contact: FC<ContactProps> = ({
|
||||||
|
className = '',
|
||||||
|
socialLinks = defaultSocialLinks
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section id="contact" className={`min-h-screen bg-black text-white p-8 ${className}`}>
|
||||||
|
<div className="max-w-4xl mx-auto space-y-16">
|
||||||
|
<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 className="grid gap-12">
|
||||||
|
{socialLinks.map((category, index) => (
|
||||||
|
<CategorySection
|
||||||
|
key={index}
|
||||||
|
category={category.category}
|
||||||
|
links={category.links}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSocialLinks = [
|
||||||
|
{
|
||||||
|
category: "Professional",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
name: "Email",
|
||||||
|
icon: MdEmail,
|
||||||
|
url: "mailto:128boilerrat@gmail.com",
|
||||||
|
username: "128boilerrat@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Calendar",
|
||||||
|
icon: MdCalendarToday,
|
||||||
|
url: "https://calendly.com/boiler-chris/call-with-boilerrat",
|
||||||
|
username: "Schedule a call"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Resume",
|
||||||
|
icon: MdLanguage,
|
||||||
|
url: "//https://rxresu.me/boilerrat/victorious-present-lungfish",
|
||||||
|
username: "Download PDF"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Social",
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
name: "X",
|
||||||
|
icon: FaXTwitter,
|
||||||
|
url: "https://twitter.com/boilerrat",
|
||||||
|
username: "@boilerrat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub",
|
||||||
|
icon: FaGithub,
|
||||||
|
url: "https://github.com/boilerrat",
|
||||||
|
username: "boilerrat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LinkedIn",
|
||||||
|
icon: FaLinkedin,
|
||||||
|
url: "https://www.linkedin.com/in/christopherwylde/",
|
||||||
|
username: "christopherwylde"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Telegram",
|
||||||
|
icon: FaTelegram,
|
||||||
|
url: "https://t.me/boilerrat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Farcaster",
|
||||||
|
icon: SiFarcaster,
|
||||||
|
url: "https://warpcast.com/boiler",
|
||||||
|
username: "boiler"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lens",
|
||||||
|
icon: BiSearch, // You might want to find a more specific Lens icon
|
||||||
|
url: "https://lenster.xyz/u/boilerrat",
|
||||||
|
username: "boilerrat"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { LinkCategory, SocialLink } from '../../types';
|
||||||
|
|
||||||
|
export interface ContactProps {
|
||||||
|
className?: string;
|
||||||
|
socialLinks?: LinkCategory[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocialLinkCardProps extends SocialLink {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySectionProps {
|
||||||
|
category: string;
|
||||||
|
links: SocialLink[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Atom, Database, Shield, Binary, Users, Award } from 'lucide-react';
|
||||||
|
import { ExpertiseProps, ExperienceCardProps, SectionProps } from './types';
|
||||||
|
import { ExperienceItem } from '../../types';
|
||||||
|
|
||||||
|
const ExperienceCard: React.FC<ExperienceCardProps> = ({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
company,
|
||||||
|
period,
|
||||||
|
description,
|
||||||
|
className = ''
|
||||||
|
}) => (
|
||||||
|
<div className={`bg-gray-900 rounded-lg p-6 hover:bg-gray-800 transition-colors duration-300 ${className}`}>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="p-3 bg-blue-500/10 rounded-lg">
|
||||||
|
<Icon className="text-blue-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-white">{title}</h3>
|
||||||
|
<p className="text-gray-400 mt-1">{company}</p>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">{period}</p>
|
||||||
|
<p className="text-gray-300 mt-3">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Section: React.FC<SectionProps> = ({
|
||||||
|
title,
|
||||||
|
icon: Icon,
|
||||||
|
items,
|
||||||
|
gradientFrom,
|
||||||
|
gradientTo,
|
||||||
|
id
|
||||||
|
}) => (
|
||||||
|
<section id={id} className="space-y-8">
|
||||||
|
<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`}>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{items.map((exp, index) => (
|
||||||
|
<ExperienceCard key={index} {...exp} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultNuclearExperience: ExperienceItem[] = [
|
||||||
|
{
|
||||||
|
title: "Radiation Protection",
|
||||||
|
company: "Makwa-Cahill/Nuvia",
|
||||||
|
period: "2019 - Present",
|
||||||
|
description: "Delivering comprehensive Radiation Protection services for Bruce Nuclear's Major Component Replacement Project.",
|
||||||
|
icon: Shield
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Journeyman Boilermaker",
|
||||||
|
company: "International Brotherhood of Boilermakers",
|
||||||
|
period: "2003 - Present",
|
||||||
|
description: "Leading diverse projects in nuclear facilities, focusing on reactor refurbishment at top facilities.",
|
||||||
|
icon: Binary
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultWeb3Experience: ExperienceItem[] = [
|
||||||
|
{
|
||||||
|
title: "Public HAUS Champion",
|
||||||
|
company: "Public HAUS",
|
||||||
|
period: "2022 - Present",
|
||||||
|
description: "Secured $197K in grants from Moloch DAO, Optimism, and Public Nouns. Leading onboarding and governance.",
|
||||||
|
icon: Users
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Founder",
|
||||||
|
company: "DAO Masons",
|
||||||
|
period: "2022 - 2024",
|
||||||
|
description: "Distributed $100K ARB tokens through Grant Ships, empowering decentralized organizations.",
|
||||||
|
icon: Award
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Expertise: React.FC<ExpertiseProps> = ({
|
||||||
|
className = '',
|
||||||
|
nuclearExperience = defaultNuclearExperience,
|
||||||
|
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">
|
||||||
|
{/* Nuclear Section */}
|
||||||
|
<Section
|
||||||
|
id="nuclear"
|
||||||
|
title="Nuclear Expertise"
|
||||||
|
icon={Atom}
|
||||||
|
items={nuclearExperience}
|
||||||
|
gradientFrom="blue-500"
|
||||||
|
gradientTo="purple-500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Web3 Section */}
|
||||||
|
<Section
|
||||||
|
id="web3"
|
||||||
|
title="Web3 Innovation"
|
||||||
|
icon={Database}
|
||||||
|
items={web3Experience}
|
||||||
|
gradientFrom="blue-500"
|
||||||
|
gradientTo="purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import type { ExperienceItem } from '../../types';
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface ExpertiseProps {
|
||||||
|
className?: string;
|
||||||
|
nuclearExperience?: ExperienceItem[];
|
||||||
|
web3Experience?: ExperienceItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceCardProps extends ExperienceItem {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectionProps {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
items: ExperienceItem[];
|
||||||
|
gradientFrom: string;
|
||||||
|
gradientTo: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Terminal, ExternalLink, AtSign, Calendar, Binary } from 'lucide-react';
|
||||||
|
import { HeroProps, QuickLink, AnimationState } from './types';
|
||||||
|
|
||||||
|
const defaultIntroText = `> const profile = {
|
||||||
|
name: "Christopher Wylde",
|
||||||
|
alias: "boilerrat",
|
||||||
|
roles: [
|
||||||
|
"Radiation Protection Specialist",
|
||||||
|
"Web3 Innovator",
|
||||||
|
"DAO Designer"
|
||||||
|
],
|
||||||
|
expertise: {
|
||||||
|
nuclear: "19+ years in Radiation Protection",
|
||||||
|
web3: "Lead multiple DAOs, $150K+ in grants"
|
||||||
|
}
|
||||||
|
};`;
|
||||||
|
|
||||||
|
const quickLinks: QuickLink[] = [
|
||||||
|
{
|
||||||
|
icon: ExternalLink,
|
||||||
|
label: "Full Resume",
|
||||||
|
href: "https://rxresu.me/boilerrat/victorious-present-lungfish",
|
||||||
|
external: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: AtSign,
|
||||||
|
label: "Contact",
|
||||||
|
href: "mailto:128boilerrat@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Calendar,
|
||||||
|
label: "Schedule a Call",
|
||||||
|
href: "https://calendly.com/boiler-chris/call-with-boilerrat",
|
||||||
|
external: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Binary,
|
||||||
|
label: "DAO Masons",
|
||||||
|
href: "https://www.daomasons.com",
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Hero: React.FC<HeroProps> = ({
|
||||||
|
className = '',
|
||||||
|
customIntroText
|
||||||
|
}) => {
|
||||||
|
const [animationState, setAnimationState] = useState<AnimationState>({
|
||||||
|
text: '',
|
||||||
|
showContent: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const introText = customIntroText || defaultIntroText;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let currentIndex = 0;
|
||||||
|
const typeWriter = setInterval(() => {
|
||||||
|
if (currentIndex < introText.length) {
|
||||||
|
setAnimationState(prev => ({
|
||||||
|
...prev,
|
||||||
|
text: prev.text + introText[currentIndex]
|
||||||
|
}));
|
||||||
|
currentIndex++;
|
||||||
|
} else {
|
||||||
|
clearInterval(typeWriter);
|
||||||
|
setTimeout(() => setAnimationState(prev => ({
|
||||||
|
...prev,
|
||||||
|
showContent: true
|
||||||
|
})), 500);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
return () => clearInterval(typeWriter);
|
||||||
|
}, [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">
|
||||||
|
{/* Terminal Window */}
|
||||||
|
<div className="bg-gray-900 rounded-lg overflow-hidden shadow-xl mb-8">
|
||||||
|
<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>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||||
|
<Terminal className="ml-4" size={14} />
|
||||||
|
</div>
|
||||||
|
<div className="p-4 font-mono text-sm">
|
||||||
|
<pre className="whitespace-pre-wrap">{animationState.text}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links Section */}
|
||||||
|
{animationState.showContent && (
|
||||||
|
<div className="space-y-8 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">
|
||||||
|
Bridging Nuclear & Web3
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
Building the future of decentralized governance while ensuring nuclear safety
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{quickLinks.map((link, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={link.href}
|
||||||
|
target={link.external ? "_blank" : undefined}
|
||||||
|
rel={link.external ? "noopener noreferrer" : undefined}
|
||||||
|
className="flex items-center gap-3 p-4 bg-gray-800 rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<link.icon size={20} />
|
||||||
|
<span>{link.label}</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface HeroProps {
|
||||||
|
className?: string;
|
||||||
|
customIntroText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QuickLink {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
external?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimationState {
|
||||||
|
text: string;
|
||||||
|
showContent: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import { NFTGridProps, NFTCardProps } from './types';
|
||||||
|
import { NFT } from '../../types';
|
||||||
|
|
||||||
|
const NFTCard: React.FC<NFTCardProps> = ({
|
||||||
|
image,
|
||||||
|
name,
|
||||||
|
link,
|
||||||
|
isHovered,
|
||||||
|
onHover,
|
||||||
|
id,
|
||||||
|
className = ''
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className={`relative group ${className}`}
|
||||||
|
onMouseEnter={() => onHover(id)}
|
||||||
|
onMouseLeave={() => onHover(null)}
|
||||||
|
>
|
||||||
|
{/* NFT Card */}
|
||||||
|
<div className="relative overflow-hidden rounded-lg bg-gray-900 aspect-square">
|
||||||
|
{/* Image */}
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={name}
|
||||||
|
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'}`}>
|
||||||
|
<h3 className="text-white text-xl font-medium">{name}</h3>
|
||||||
|
{link && (
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-white hover:text-blue-400 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink size={24} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Glow Effect */}
|
||||||
|
<div className={`absolute inset-0 rounded-lg bg-blue-500 opacity-0 group-hover:opacity-20
|
||||||
|
blur-xl transition-opacity duration-500 -z-10`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultNFTs: NFT[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
image: "https://i.imgur.com/Clv3Y1d.png",
|
||||||
|
name: "Public Noun 237",
|
||||||
|
link: "https://etherscan.io/nft/0x93ecac71499147627dfec6d0e494d50fcfff10ee/237"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
image: "https://i.imgur.com/qvJL6Rd.png",
|
||||||
|
name: "Public Noun 111",
|
||||||
|
link: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
image: "https://i.imgur.com/BRXIjWr.png",
|
||||||
|
name: "Public Noun 65",
|
||||||
|
link: "https://etherscan.io/nft/0x93ecac71499147627dfec6d0e494d50fcfff10ee/65"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
image: "https://i.imgur.com/adQSaCJ.png",
|
||||||
|
name: "Boiler's Jerry",
|
||||||
|
link: "https://etherscan.io/nft/0x42c4a46bd0f9c642e852a848a5b730b6e3086ccf/32"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NFTGrid: React.FC<NFTGridProps> = ({
|
||||||
|
className = '',
|
||||||
|
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="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
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
{nfts.map((nft) => (
|
||||||
|
<NFTCard
|
||||||
|
key={nft.id}
|
||||||
|
{...nft}
|
||||||
|
isHovered={hoveredNFT === nft.id}
|
||||||
|
onHover={setHoveredNFT}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { NFT } from '../../types';
|
||||||
|
|
||||||
|
export interface NFTGridProps {
|
||||||
|
className?: string;
|
||||||
|
nfts?: NFT[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NFTCardProps extends NFT {
|
||||||
|
isHovered: boolean;
|
||||||
|
onHover: (id: number | null) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Home, Atom, Database, Award, Image, User } from 'lucide-react';
|
||||||
|
import { NavItem } from '../../types';
|
||||||
|
import { NavigationProps, ScrollState } from './types';
|
||||||
|
|
||||||
|
export const Navigation: React.FC<NavigationProps> = ({
|
||||||
|
className = '',
|
||||||
|
initialActiveSection = 'home',
|
||||||
|
navItems: customNavItems
|
||||||
|
}) => {
|
||||||
|
const [scrollState, setScrollState] = useState<ScrollState>({
|
||||||
|
progress: 0,
|
||||||
|
activeSection: initialActiveSection
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultNavItems: NavItem[] = [
|
||||||
|
{ id: 'home', icon: Home, label: 'Home' },
|
||||||
|
{ id: 'nuclear', icon: Atom, label: 'Nuclear' },
|
||||||
|
{ id: 'web3', icon: Database, label: 'Web3' },
|
||||||
|
{ id: 'achievements', icon: Award, label: 'Achievements' },
|
||||||
|
{ id: 'nfts', icon: Image, label: 'NFTs' },
|
||||||
|
{ id: 'contact', icon: User, label: 'Contact' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const navItems = customNavItems || defaultNavItems;
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
// Calculate scroll progress
|
||||||
|
const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||||
|
const progress = (window.scrollY / totalHeight) * 100;
|
||||||
|
|
||||||
|
// Update active section based on scroll position
|
||||||
|
const sections = document.querySelectorAll('section');
|
||||||
|
let newActiveSection = scrollState.activeSection;
|
||||||
|
|
||||||
|
sections.forEach(section => {
|
||||||
|
const rect = section.getBoundingClientRect();
|
||||||
|
if (rect.top >= 0 && rect.top <= window.innerHeight / 2) {
|
||||||
|
newActiveSection = section.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setScrollState({ progress, activeSection: newActiveSection });
|
||||||
|
}, [scrollState.activeSection]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, [handleScroll]);
|
||||||
|
|
||||||
|
const scrollToSection = (id: string) => {
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`fixed right-4 top-1/2 transform -translate-y-1/2 z-50 ${className}`}>
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="absolute left-0 top-0 w-0.5 h-full bg-gray-800">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500"
|
||||||
|
style={{ height: `${scrollState.progress}%`, transition: 'height 0.1s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Items */}
|
||||||
|
<div className="relative flex flex-col gap-6">
|
||||||
|
{navItems.map(({ id, icon: Icon, label }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
onClick={() => scrollToSection(id)}
|
||||||
|
className={`group relative flex items-center ${
|
||||||
|
scrollState.activeSection === id ? 'text-blue-500' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-2 rounded-full transition-all duration-300
|
||||||
|
${scrollState.activeSection === id
|
||||||
|
? 'bg-gray-800 text-blue-500'
|
||||||
|
: 'hover:bg-gray-800 hover:text-blue-500'}`}>
|
||||||
|
<Icon size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="absolute right-full mr-4 px-2 py-1 bg-gray-800
|
||||||
|
rounded-md text-sm opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300 whitespace-nowrap">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { NavItem } from '../../types';
|
||||||
|
|
||||||
|
export interface NavigationProps {
|
||||||
|
className?: string; // Optional: for additional styling
|
||||||
|
initialActiveSection?: string; // Optional: to set initial active section
|
||||||
|
navItems?: NavItem[]; // Optional: to override default nav items
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local component-specific types
|
||||||
|
export interface ScrollState {
|
||||||
|
progress: number;
|
||||||
|
activeSection: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import type { FC } from 'react';
|
||||||
|
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-blue-500 to-purple-500 rounded-full transform origin-left transition-transform duration-1000 ease-out"
|
||||||
|
style={{ transform: `scaleX(${percentage / 100})` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SkillCard: FC<SkillCardProps> = ({
|
||||||
|
skill,
|
||||||
|
className = ''
|
||||||
|
}) => (
|
||||||
|
<div className={`bg-gray-900 rounded-lg p-6 hover:bg-gray-800 transition-all duration-300 ${className}`}>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-white">{skill.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{skill.description}</p>
|
||||||
|
</div>
|
||||||
|
<SkillBar level={skill.level} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{skill.keywords.map((keyword, index) => (
|
||||||
|
<div key={index} className="flex items-center text-sm text-gray-400">
|
||||||
|
<ChevronRight size={16} className="text-blue-500 mr-2" />
|
||||||
|
<span>{keyword}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultSkills: Skill[] = [
|
||||||
|
{
|
||||||
|
name: "Radiation Protection",
|
||||||
|
description: "Expert",
|
||||||
|
level: 5,
|
||||||
|
keywords: [
|
||||||
|
"Radiation Monitoring",
|
||||||
|
"Contamination Control",
|
||||||
|
"Radiological Safety",
|
||||||
|
"Documentation and Procedure"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rigging",
|
||||||
|
description: "Advanced",
|
||||||
|
level: 4,
|
||||||
|
keywords: [
|
||||||
|
"Heavy Machinery",
|
||||||
|
"Safety Protocols",
|
||||||
|
"Equipment Setup",
|
||||||
|
"Planning"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Project Management",
|
||||||
|
description: "Intermediate",
|
||||||
|
level: 3,
|
||||||
|
keywords: [
|
||||||
|
"Task Prioritization",
|
||||||
|
"Time Management",
|
||||||
|
"Team Collaboration",
|
||||||
|
"Project Scheduling",
|
||||||
|
"Milestone Tracking",
|
||||||
|
"Problem Solving"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Grant Writing",
|
||||||
|
description: "Expert",
|
||||||
|
level: 4,
|
||||||
|
keywords: [
|
||||||
|
"Governance",
|
||||||
|
"Operations",
|
||||||
|
"Community Building",
|
||||||
|
"Grants"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Community Building",
|
||||||
|
description: "Expert",
|
||||||
|
level: 3,
|
||||||
|
keywords: [
|
||||||
|
"Onboarding",
|
||||||
|
"Content Creation",
|
||||||
|
"Networking"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Web Development",
|
||||||
|
description: "Novice",
|
||||||
|
level: 2,
|
||||||
|
keywords: [
|
||||||
|
"Python",
|
||||||
|
"Javascript",
|
||||||
|
"Typescript",
|
||||||
|
"HTML",
|
||||||
|
"CSS",
|
||||||
|
"REACT"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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">
|
||||||
|
Professional Skills
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{skills.map((skill, index) => (
|
||||||
|
<SkillCard key={index} skill={skill} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
export interface Skill {
|
||||||
|
id?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
level: number;
|
||||||
|
keywords: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillsProps {
|
||||||
|
className?: string;
|
||||||
|
skills?: Skill[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillCardProps {
|
||||||
|
skill: Skill;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillBarProps {
|
||||||
|
level: number;
|
||||||
|
maxLevel?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Custom animations */
|
||||||
|
@keyframes float {
|
||||||
|
0% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(-10px, 10px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatDelayed {
|
||||||
|
0% { transform: translate(0, 0); }
|
||||||
|
50% { transform: translate(10px, -10px); }
|
||||||
|
100% { transform: translate(0, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float-delayed {
|
||||||
|
animation: floatDelayed 15s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import type { LucideIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExperienceItem {
|
||||||
|
title: string;
|
||||||
|
company: string;
|
||||||
|
period: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Grant {
|
||||||
|
name: string;
|
||||||
|
amount: number;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Achievement {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TooltipProps {
|
||||||
|
active?: boolean;
|
||||||
|
payload?: Array<{
|
||||||
|
value: number;
|
||||||
|
payload: Grant;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NFT {
|
||||||
|
id: number;
|
||||||
|
image: string;
|
||||||
|
name: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocialLink {
|
||||||
|
name: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkCategory {
|
||||||
|
category: string;
|
||||||
|
links: SocialLink[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue