ALL fonctionel
This commit is contained in:
commit
23534054ed
24 changed files with 4846 additions and 0 deletions
3
.bolt/config.json
Normal file
3
.bolt/config.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"template": "bolt-vite-react-ts"
|
||||
}
|
8
.bolt/prompt
Normal file
8
.bolt/prompt
Normal file
|
@ -0,0 +1,8 @@
|
|||
For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production.
|
||||
|
||||
By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them.
|
||||
|
||||
Use icons from lucide-react for logos.
|
||||
|
||||
Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags.
|
||||
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
# 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?
|
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
|
@ -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 },
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -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>Kaby_Kun PortFolio</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
4120
package-lock.json
generated
Normal file
4120
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
package.json
Normal file
35
package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "kabykun-portfolio",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^11.0.8",
|
||||
"lucide-react": "^0.344.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.11",
|
||||
"globals": "^15.9.0",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.3.0",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
26
src/App.tsx
Normal file
26
src/App.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import Navbar from './components/Navbar';
|
||||
import Home from './pages/Home';
|
||||
import Projects from './pages/Projects';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-900 text-white">
|
||||
<Navbar />
|
||||
<AnimatePresence mode="wait">
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/projects" element={<Projects />} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
51
src/components/Footer.tsx
Normal file
51
src/components/Footer.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react';
|
||||
import { Github, Mail, Globe, MessageSquare } from 'lucide-react';
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-gray-800 py-8 mt-24">
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between">
|
||||
<p className="text-gray-400">© {new Date().getFullYear()} Kaby_Kun. All rights reserved.</p>
|
||||
|
||||
<div className="flex items-center space-x-6 mt-4 md:mt-0">
|
||||
<a
|
||||
href="https://github.com/ByakuraRinne"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="GitHub Profile"
|
||||
>
|
||||
<Github className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="mailto:gabriel@nazuna.ovh"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="Email"
|
||||
>
|
||||
<Mail className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/rcqUj2mFBm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="Discord Server"
|
||||
>
|
||||
<MessageSquare className="w-6 h-6" />
|
||||
</a>
|
||||
<a
|
||||
href="https://astrohosting.ovh"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="Website"
|
||||
>
|
||||
<Globe className="w-6 h-6" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
55
src/components/Hero.tsx
Normal file
55
src/components/Hero.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Terminal, Server, Database } from 'lucide-react';
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-900 via-gray-800 to-black opacity-90" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 px-6 py-24 mx-auto max-w-7xl"
|
||||
>
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="p-2 rounded-full bg-cyan-500/10 mb-6"
|
||||
>
|
||||
<Terminal className="w-12 h-12 text-cyan-400" />
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-5xl md:text-6xl font-bold text-white mb-6"
|
||||
>
|
||||
Kaby_Kun
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="text-xl text-gray-400 max-w-2xl mb-8"
|
||||
>
|
||||
Backend Developer & Infrastructure Specialist
|
||||
</motion.p>
|
||||
|
||||
<div className="flex space-x-6">
|
||||
<Server className="w-8 h-8 text-cyan-400" />
|
||||
<Database className="w-8 h-8 text-cyan-400" />
|
||||
<Terminal className="w-8 h-8 text-cyan-400" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
58
src/components/Infrastructure.tsx
Normal file
58
src/components/Infrastructure.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Server, HardDrive } from 'lucide-react';
|
||||
|
||||
export default function Infrastructure() {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
<div className="bg-gray-800 rounded-xl p-8 hover:bg-gray-700 transition-colors">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<Server className="w-8 h-8 text-cyan-400" />
|
||||
<h3 className="text-2xl font-bold text-white">Server Infrastructure</h3>
|
||||
</div>
|
||||
<ul className="space-y-4 text-gray-300">
|
||||
<li className="flex items-start space-x-3">
|
||||
<HardDrive className="w-5 h-5 text-cyan-400 mt-1" />
|
||||
<div>
|
||||
<span className="font-semibold">Lenovo System x3950 X6</span>
|
||||
<p className="text-gray-400">Enterprise-grade server for heavy workloads</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<HardDrive className="w-5 h-5 text-cyan-400 mt-1" />
|
||||
<div>
|
||||
<span className="font-semibold">Dell PowerEdge R620</span>
|
||||
<p className="text-gray-400">High-performance rack server</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="flex items-start space-x-3">
|
||||
<HardDrive className="w-5 h-5 text-cyan-400 mt-1" />
|
||||
<div>
|
||||
<span className="font-semibold">Custom Builds</span>
|
||||
<p className="text-gray-400">Self-made infrastructure solutions</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 rounded-xl p-8 hover:bg-gray-700 transition-colors">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1558494949-ef010cbdcc31?auto=format&fit=crop&q=80&w=2034"
|
||||
alt="Server Room"
|
||||
className="w-full h-64 object-cover rounded-lg mb-6"
|
||||
/>
|
||||
<p className="text-gray-300">
|
||||
Running a sophisticated home lab environment with enterprise-grade hardware,
|
||||
enabling experimentation with various technologies and hosting solutions.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
36
src/components/Navbar.tsx
Normal file
36
src/components/Navbar.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Terminal } from 'lucide-react';
|
||||
|
||||
export default function Navbar() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<nav className="bg-gray-800/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div className="max-w-6xl mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/" className="flex items-center space-x-2">
|
||||
<Terminal className="w-6 h-6 text-cyan-400" />
|
||||
<span className="text-xl font-bold">Kaby_Kun</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center space-x-8">
|
||||
<Link
|
||||
to="/"
|
||||
className={`hover:text-cyan-400 transition-colors ${location.pathname === '/' ? 'text-cyan-400' : 'text-gray-300'}`}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/projects"
|
||||
className={`hover:text-cyan-400 transition-colors ${location.pathname === '/projects' ? 'text-cyan-400' : 'text-gray-300'}`}
|
||||
>
|
||||
Projects
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
36
src/components/TechStack.tsx
Normal file
36
src/components/TechStack.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Server, Container, Box, Cloud, Terminal, Database } from 'lucide-react';
|
||||
|
||||
const technologies = [
|
||||
{ icon: Server, name: 'Infrastructure', desc: 'Lenovo System x3950 X6, Dell R620' },
|
||||
{ icon: Container, name: 'Containerization', desc: 'Docker, Podman' },
|
||||
{ icon: Cloud, name: 'Virtualization', desc: 'Proxmox' },
|
||||
{ icon: Terminal, name: 'DevOps', desc: 'CI/CD, Automation' },
|
||||
{ icon: Database, name: 'Database', desc: 'Management & Optimization' },
|
||||
{ icon: Box, name: 'Custom Builds', desc: 'Self-made Infrastructure' },
|
||||
];
|
||||
|
||||
export default function TechStack() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full max-w-6xl mx-auto">
|
||||
{technologies.map((tech, index) => (
|
||||
<motion.div
|
||||
key={tech.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
className="bg-gray-800 p-6 rounded-xl hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<tech.icon className="w-8 h-8 text-cyan-400" />
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-white">{tech.name}</h3>
|
||||
<p className="text-gray-400">{tech.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
15
src/index.css
Normal file
15
src/index.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-gray-900 text-gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-gradient {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-500;
|
||||
}
|
||||
}
|
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
36
src/pages/Home.tsx
Normal file
36
src/pages/Home.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import Hero from '../components/Hero';
|
||||
import TechStack from '../components/TechStack';
|
||||
import Infrastructure from '../components/Infrastructure';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
|
||||
<motion.main
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.5, delay: 0.2 }}
|
||||
className="px-6 py-16 space-y-24"
|
||||
>
|
||||
<section>
|
||||
<div className="max-w-6xl mx-auto mb-12 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">Tech Stack</h2>
|
||||
<p className="text-gray-400">Powering robust backend solutions</p>
|
||||
</div>
|
||||
<TechStack />
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="max-w-6xl mx-auto mb-12 text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">Infrastructure</h2>
|
||||
<p className="text-gray-400">Enterprise-grade hardware setup</p>
|
||||
</div>
|
||||
<Infrastructure />
|
||||
</section>
|
||||
</motion.main>
|
||||
</>
|
||||
);
|
||||
}
|
198
src/pages/Projects.tsx
Normal file
198
src/pages/Projects.tsx
Normal file
|
@ -0,0 +1,198 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Plus, X, Github, Globe } from 'lucide-react';
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
github?: string;
|
||||
website?: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export default function Projects() {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [projects, setProjects] = useState<Project[]>(() => {
|
||||
const saved = localStorage.getItem('projects');
|
||||
try {
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
} catch {
|
||||
localStorage.removeItem('projects');
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('projects', JSON.stringify(projects));
|
||||
}, [projects]);
|
||||
|
||||
const toggleAdmin = () => {
|
||||
const password = window.prompt('Enter admin password:');
|
||||
if (password === 'O8kocwmnVyW6AF') {
|
||||
setIsAdmin(true);
|
||||
} else {
|
||||
alert('Incorrect password.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddProject = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const newProject: Project = {
|
||||
id: Date.now().toString(),
|
||||
title: formData.get('title') as string,
|
||||
description: formData.get('description') as string,
|
||||
image: formData.get('image') as string,
|
||||
github: formData.get('github') as string || undefined,
|
||||
website: formData.get('website') as string || undefined,
|
||||
tags: (formData.get('tags') as string)
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag),
|
||||
};
|
||||
|
||||
setProjects(prev => [...prev, newProject]);
|
||||
setShowAddForm(false);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
const deleteProject = (id: string) => {
|
||||
setProjects(prev => prev.filter(p => p.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="max-w-6xl mx-auto px-6 py-16"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-12">
|
||||
<h1 className="text-4xl font-bold">Projects</h1>
|
||||
{isAdmin ? (
|
||||
<button
|
||||
onClick={() => setShowAddForm(true)}
|
||||
className="flex items-center space-x-2 bg-cyan-500 hover:bg-cyan-600 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Add Project</span>
|
||||
</button>
|
||||
) : (
|
||||
<span
|
||||
onClick={toggleAdmin}
|
||||
className="w-2 h-2 rounded-full bg-gray-800 hover:bg-gray-700 cursor-pointer transition-colors"
|
||||
aria-label="Admin toggle"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
<AnimatePresence>
|
||||
{projects.map(project => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className="bg-gray-800 rounded-xl overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-xl font-bold mb-2">{project.title}</h3>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => deleteProject(project.id)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
aria-label={`Delete project ${project.title}`}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-400 mb-4">{project.description}</p>
|
||||
<div className="flex flex-wrap gap-2 mb-4">
|
||||
{project.tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-gray-700 text-cyan-400 text-sm px-3 py-1 rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex space-x-4">
|
||||
{project.github && (
|
||||
<a
|
||||
href={project.github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="GitHub link"
|
||||
>
|
||||
<Github className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{project.website && (
|
||||
<a
|
||||
href={project.website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
aria-label="Website link"
|
||||
>
|
||||
<Globe className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="bg-gray-800 rounded-xl p-6 max-w-md w-full"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-2xl font-bold">Add New Project</h2>
|
||||
<button
|
||||
onClick={() => setShowAddForm(false)}
|
||||
className="text-gray-400 hover:text-red-500 transition-colors"
|
||||
aria-label="Close add project form"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddProject} className="space-y-4">
|
||||
{/* Input fields */}
|
||||
{/* ... */}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-cyan-500 hover:bg-cyan-600 text-white py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Add Project
|
||||
</button>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
animation: {
|
||||
'gradient': 'gradient 8s linear infinite',
|
||||
},
|
||||
keyframes: {
|
||||
gradient: {
|
||||
'0%, 100%': {
|
||||
'background-size': '200% 200%',
|
||||
'background-position': 'left center'
|
||||
},
|
||||
'50%': {
|
||||
'background-size': '200% 200%',
|
||||
'background-position': 'right center'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
24
tsconfig.app.json
Normal file
24
tsconfig.app.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"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
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
22
tsconfig.node.json
Normal file
22
tsconfig.node.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"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
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
optimizeDeps: {
|
||||
exclude: ['lucide-react'],
|
||||
},
|
||||
});
|
Loading…
Reference in a new issue