Compare commits
3 Commits
430e02c8ce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52bf3b96b1 | |||
| 965d2a41dd | |||
| a7e62e84fa |
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"bradlc.vscode-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+30
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"prettier.printWidth": 120,
|
||||||
|
"editor.rulers": [120],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "always",
|
||||||
|
"source.organizeImports": "always"
|
||||||
|
},
|
||||||
|
"javascript.format.semicolons": "insert",
|
||||||
|
"typescript.format.semicolons": "insert",
|
||||||
|
"typescript.format.insertSpaceAfterCommaDelimiter": true,
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"explorer.compactFolders": false,
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"editor.linkedEditing": true,
|
||||||
|
"search.exclude": {
|
||||||
|
"**/node_modules": true,
|
||||||
|
"**/dist": true,
|
||||||
|
"**/pnpm-lock.yaml": true
|
||||||
|
},
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/node_modules/*/**": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 870 KiB |
@@ -5,9 +5,12 @@ import { ProjectsSection } from './features/projects';
|
|||||||
import { ExperienceSection } from './features/experience';
|
import { ExperienceSection } from './features/experience';
|
||||||
import { ContactSection } from './features/contact';
|
import { ContactSection } from './features/contact';
|
||||||
import Footer from './shared/components/Footer';
|
import Footer from './shared/components/Footer';
|
||||||
|
import { ThemeProvider } from './shared/context/ThemeContext';
|
||||||
|
import { ThemeSelector } from './shared/components/ThemeSelector';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<div className="bg-black text-white min-h-screen overflow-x-hidden">
|
<div className="bg-black text-white min-h-screen overflow-x-hidden">
|
||||||
{/* Background gradient decorations */}
|
{/* Background gradient decorations */}
|
||||||
<div className="fixed inset-0 -z-10">
|
<div className="fixed inset-0 -z-10">
|
||||||
@@ -22,7 +25,10 @@ function App() {
|
|||||||
<ExperienceSection />
|
<ExperienceSection />
|
||||||
<ContactSection />
|
<ContactSection />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
||||||
|
<ThemeSelector />
|
||||||
</div>
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ export const ContactSection: React.FC = () => {
|
|||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
>
|
>
|
||||||
<span className="text-green-400 text-sm font-medium">GET IN TOUCH</span>
|
<span className="text-green-400 text-sm font-medium">Kontakt aufnehmen</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
Let's Work Together
|
Lass uns zusammenarbeiten
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||||
Have a project in mind? Let's discuss how we can bring your ideas to life
|
Hast du ein Projekt? Lass uns besprechen, wie wir deine Idee umsetzen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,32 +63,32 @@ export const ContactSection: React.FC = () => {
|
|||||||
<label className="block text-white font-medium mb-3">Name</label>
|
<label className="block text-white font-medium mb-3">Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Your name"
|
placeholder="Dein Name"
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors"
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<label className="block text-white font-medium mb-3">Email</label>
|
<label className="block text-white font-medium mb-3">E-Mail</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="your.email@example.com"
|
placeholder="deine.email@example.com"
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors"
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<label className="block text-white font-medium mb-3">Message</label>
|
<label className="block text-white font-medium mb-3">Nachricht</label>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="Tell me about your project"
|
placeholder="Erzähle mir von deinem Projekt"
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors resize-none"
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none resize-none"
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<Button variant="primary" size="lg" className="w-full">
|
<Button variant="primary" size="lg" className="w-full">
|
||||||
Send Message
|
Nachricht senden
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -102,10 +102,10 @@ export const ContactSection: React.FC = () => {
|
|||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<h3 className="text-2xl font-bold text-white mb-4">Let's Connect</h3>
|
<h3 className="text-2xl font-bold text-white mb-4">Kontakt</h3>
|
||||||
<p className="text-gray-400">
|
<p className="text-gray-400">
|
||||||
I'm always open to discussing new projects, creative ideas, or opportunities
|
Ich freue mich auf neue Projekte, kreative Ideen und spannende Kooperationen.
|
||||||
to be part of your vision. Feel free to reach out!
|
Schreibe mir gerne eine Nachricht!
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export const ContactSection: React.FC = () => {
|
|||||||
<Mail className="text-green-500" size={24} />
|
<Mail className="text-green-500" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-sm">Email</p>
|
<p className="text-gray-400 text-sm">E-Mail</p>
|
||||||
<p className="text-white font-medium">robert@bretz.dev</p>
|
<p className="text-white font-medium">robert@bretz.dev</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,8 +127,8 @@ export const ContactSection: React.FC = () => {
|
|||||||
<MapPin className="text-green-500" size={24} />
|
<MapPin className="text-green-500" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-sm">Location</p>
|
<p className="text-gray-400 text-sm">Standort</p>
|
||||||
<p className="text-white font-medium">Germany</p>
|
<p className="text-white font-medium">Deutschland</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,29 +136,29 @@ export const ContactSection: React.FC = () => {
|
|||||||
|
|
||||||
{/* Social Links */}
|
{/* Social Links */}
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<p className="text-white font-medium mb-4">Connect with me</p>
|
<p className="text-white font-medium mb-4">Folge mir</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#"
|
href="#"
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(34,197,94,0.2)' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 text-green-500"
|
||||||
>
|
>
|
||||||
<FaGithub size={24} />
|
<FaGithub size={24} />
|
||||||
</motion.a>
|
</motion.a>
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#"
|
href="#"
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(34,197,94,0.2)' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 text-green-500"
|
||||||
>
|
>
|
||||||
<FaLinkedin size={24} />
|
<FaLinkedin size={24} />
|
||||||
</motion.a>
|
</motion.a>
|
||||||
<motion.a
|
<motion.a
|
||||||
href="#"
|
href="#"
|
||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1, backgroundColor: 'rgba(34,197,94,0.2)' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 text-green-500"
|
||||||
>
|
>
|
||||||
<FaTwitter size={24} />
|
<FaTwitter size={24} />
|
||||||
</motion.a>
|
</motion.a>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ export const ExperienceSection: React.FC = () => {
|
|||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
>
|
>
|
||||||
<span className="text-green-400 text-sm font-medium">My Journey</span>
|
<span className="text-green-400 text-sm font-medium">Mein Weg</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
Professional Experience
|
Berufserfahrung
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 text-lg">
|
<p className="text-gray-400 text-lg">
|
||||||
A timeline of my work experience and achievements
|
Eine Übersicht meiner beruflichen Stationen und Erfolge
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -42,15 +42,16 @@ export const TimelineItem: React.FC<TimelineItemProps> = ({
|
|||||||
className={`w-full md:w-5/12 ${isEven ? 'md:ml-0 md:text-right' : 'md:ml-auto'}`}
|
className={`w-full md:w-5/12 ${isEven ? 'md:ml-0 md:text-right' : 'md:ml-auto'}`}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02, boxShadow: '0 10px 30px rgba(34,197,94,0.15)' }}
|
||||||
className="p-6 rounded-xl border border-green-500/30 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/60 transition-all duration-300"
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
|
className="p-6 rounded-xl border border-green-500/30 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
{/* Year Badge */}
|
{/* Year Badge */}
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="px-3 py-1 rounded-full bg-green-500/20 border border-green-500/50">
|
<div className="px-3 py-1 rounded-full bg-green-500/20 border border-green-500/50">
|
||||||
<span className="text-green-400 font-bold text-sm">
|
<span className="text-green-400 font-bold text-sm">
|
||||||
{experience.startYear}
|
{experience.startYear}
|
||||||
{experience.endYear ? `-${experience.endYear}` : '-Present'}
|
{experience.endYear ? `-${experience.endYear}` : '-Heute'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,12 +73,14 @@ export const TimelineItem: React.FC<TimelineItemProps> = ({
|
|||||||
{experience.technologies && (
|
{experience.technologies && (
|
||||||
<div className="flex flex-wrap gap-2 justify-end">
|
<div className="flex flex-wrap gap-2 justify-end">
|
||||||
{experience.technologies.map((tech) => (
|
{experience.technologies.map((tech) => (
|
||||||
<span
|
<motion.span
|
||||||
key={tech}
|
key={tech}
|
||||||
|
whileHover={{ scale: 1.05, backgroundColor: 'rgba(34,197,94,0.2)' }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
className="text-xs px-2 py-1 rounded border border-green-500/30 text-green-400 bg-green-500/10"
|
className="text-xs px-2 py-1 rounded border border-green-500/30 text-green-400 bg-green-500/10"
|
||||||
>
|
>
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ export const experienceData: Experience[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
position: 'Creative Tech Consultant',
|
position: 'Consultant für kreative Technologien',
|
||||||
startYear: 2018,
|
startYear: 2018,
|
||||||
endYear: 2020,
|
endYear: 2020,
|
||||||
description:
|
description:
|
||||||
'Delivered immersive digital experiences using Unity, Blender, Godot and LaTeX documentation for creative product storytelling.',
|
'Entwicklung immersiver digitaler Erlebnisse mit Unity, Blender, Godot und LaTeX für kreative Produktstorytelling.',
|
||||||
technologies: ['Unity', 'Blender', 'Godot', 'LaTeX'],
|
technologies: ['Unity', 'Blender', 'Godot', 'LaTeX'],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import React from 'react';
|
import { motion } from "framer-motion";
|
||||||
import { motion } from 'framer-motion';
|
import { ArrowDown } from "lucide-react";
|
||||||
import { ArrowDown } from 'lucide-react';
|
import React from "react";
|
||||||
import Button from '../../../shared/components/Button';
|
import Button from "../../../shared/components/Button";
|
||||||
|
import { useScrollToSection } from "../../../shared/hooks/useScrollToSection";
|
||||||
|
|
||||||
export const HeroSection: React.FC = () => {
|
export const HeroSection: React.FC = () => {
|
||||||
|
const handleScrollToSection = useScrollToSection();
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
transition: {
|
transition: {
|
||||||
staggerChildren: 0.2,
|
staggerChildren: 0.15,
|
||||||
delayChildren: 0.3,
|
delayChildren: 0.2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -20,6 +23,15 @@ export const HeroSection: React.FC = () => {
|
|||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
transition: { duration: 0.6 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const imageVariants = {
|
||||||
|
hidden: { opacity: 0, scale: 0.8 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
transition: { duration: 0.8 },
|
transition: { duration: 0.8 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -35,16 +47,15 @@ export const HeroSection: React.FC = () => {
|
|||||||
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl"></div>
|
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<motion.div
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="max-w-7xl mx-auto w-full">
|
||||||
variants={containerVariants}
|
{/* Grid Layout: Text left, Image right on desktop, stacked on mobile */}
|
||||||
initial="hidden"
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 lg:items-start">
|
||||||
animate="visible"
|
{/* Text Content */}
|
||||||
className="max-w-5xl mx-auto text-center"
|
<div className="text-center lg:text-left">
|
||||||
>
|
|
||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
<motion.div variants={itemVariants} className="mb-8">
|
<motion.div variants={itemVariants} className="mb-8">
|
||||||
<div className="inline-block px-4 py-2 border border-green-500/50 rounded-full bg-green-500/10 backdrop-blur-sm">
|
<div className="inline-block px-4 py-2 border border-green-500/50 rounded-full bg-green-500/10 backdrop-blur-sm">
|
||||||
<span className="text-green-400 text-sm font-medium flex items-center gap-2">
|
<span className="text-green-400 text-sm font-medium flex items-center gap-2 justify-center lg:justify-start">
|
||||||
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||||
Softwareentwickler & Architektur-Enthusiast | Deutschland
|
Softwareentwickler & Architektur-Enthusiast | Deutschland
|
||||||
</span>
|
</span>
|
||||||
@@ -52,44 +63,57 @@ export const HeroSection: React.FC = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Main Heading */}
|
{/* Main Heading */}
|
||||||
<motion.h1 variants={itemVariants} className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
<motion.h1
|
||||||
|
variants={itemVariants}
|
||||||
|
className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6 leading-tight"
|
||||||
|
>
|
||||||
<span className="text-white">Robert Bretz</span>
|
<span className="text-white">Robert Bretz</span>
|
||||||
<br />
|
<br />
|
||||||
<span className="bg-gradient-to-r from-green-500 to-emerald-500 bg-clip-text text-transparent">
|
<span className="bg-gradient-to-r from-green-500 to-emerald-500 bg-clip-text text-transparent">
|
||||||
Crafting clean, performant digital products
|
Saubere, performante digitale Lösungen
|
||||||
</span>
|
</span>
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
{/* Subtitle */}
|
{/* Subtitle */}
|
||||||
<motion.p
|
<motion.p variants={itemVariants} className="text-gray-300 text-lg sm:text-xl mb-8">
|
||||||
variants={itemVariants}
|
|
||||||
className="text-gray-300 text-lg sm:text-xl max-w-2xl mx-auto mb-8"
|
|
||||||
>
|
|
||||||
Ich entwickle moderne Anwendungen mit React, TypeScript, Tailwind, .NET, NestJS und sauberer Architektur.
|
Ich entwickle moderne Anwendungen mit React, TypeScript, Tailwind, .NET, NestJS und sauberer Architektur.
|
||||||
Mein Fokus liegt auf Performance, Vertical Slice Architecture, Docker-Deployments und skalierbaren Backend-Systemen.
|
Mein Fokus liegt auf Performance, Vertical Slice Architecture, Docker-Deployments und skalierbaren
|
||||||
|
Backend-Systemen.
|
||||||
</motion.p>
|
</motion.p>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
|
<motion.div variants={itemVariants} className="grid grid-cols-3 gap-3 sm:gap-6 mb-12">
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
whileHover={{ scale: 1.05, y: -4 }}
|
||||||
className="grid grid-cols-1 sm:grid-cols-3 gap-4 sm:gap-8 mb-12 max-w-2xl mx-auto"
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
|
className="border-2 border-green-500/60 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div className="border border-green-500/30 rounded-lg p-5 bg-green-500/5 backdrop-blur-sm">
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">4+</div>
|
||||||
<div className="text-3xl font-bold text-green-500">4+</div>
|
<div className="text-gray-400 text-xs sm:text-sm">Jahre Erfahrung</div>
|
||||||
<div className="text-gray-400 text-sm">Years Engineering</div>
|
</motion.div>
|
||||||
</div>
|
<motion.div
|
||||||
<div className="border border-green-500/30 rounded-lg p-5 bg-green-500/5 backdrop-blur-sm">
|
whileHover={{ scale: 1.05, y: -4 }}
|
||||||
<div className="text-3xl font-bold text-green-500">30+</div>
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
<div className="text-gray-400 text-sm">Delivered Solutions</div>
|
className="border-2 border-green-500/60 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm"
|
||||||
</div>
|
>
|
||||||
<div className="border border-green-500/30 rounded-lg p-5 bg-green-500/5 backdrop-blur-sm">
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">30+</div>
|
||||||
<div className="text-3xl font-bold text-green-500">99%</div>
|
<div className="text-gray-400 text-xs sm:text-sm">Projekte umgesetzt</div>
|
||||||
<div className="text-gray-400 text-sm">Performance Focus</div>
|
</motion.div>
|
||||||
</div>
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05, y: -4 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
|
className="border-2 border-green-500/60 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">99%</div>
|
||||||
|
<div className="text-gray-400 text-xs sm:text-sm">Performance-Fokus</div>
|
||||||
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* CTA Buttons */}
|
{/* CTA Buttons */}
|
||||||
<motion.div variants={itemVariants} className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start"
|
||||||
|
>
|
||||||
<Button variant="primary" size="lg">
|
<Button variant="primary" size="lg">
|
||||||
Lass uns sprechen
|
Lass uns sprechen
|
||||||
</Button>
|
</Button>
|
||||||
@@ -97,16 +121,54 @@ export const HeroSection: React.FC = () => {
|
|||||||
Projekte ansehen
|
Projekte ansehen
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Image - Right side on desktop, centered on mobile */}
|
||||||
|
<motion.div variants={imageVariants} className="flex justify-center lg:justify-end">
|
||||||
|
<div className="relative w-56 h-56 sm:w-72 sm:h-72 lg:w-96 lg:h-96">
|
||||||
|
{/* Animated circular green border */}
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 w-full h-full pointer-events-none -scale-x-100"
|
||||||
|
viewBox="0 0 400 400"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<motion.circle
|
||||||
|
cx="200"
|
||||||
|
cy="200"
|
||||||
|
r="200"
|
||||||
|
fill="none"
|
||||||
|
stroke="#22c55e"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
initial={{ strokeDasharray: 1200, strokeDashoffset: 1200, opacity: 1 }}
|
||||||
|
animate={{
|
||||||
|
strokeDashoffset: [1200, 0, -1200],
|
||||||
|
opacity: [1, 1, 0],
|
||||||
|
}}
|
||||||
|
transition={{ duration: 4, delay: 0.8, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Profile Image */}
|
||||||
|
<img
|
||||||
|
src="/Bewerbungsfoto.png"
|
||||||
|
alt="Robert Bretz"
|
||||||
|
className="w-full h-full object-cover rounded-full shadow-2xl shadow-green-500/30"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicator */}
|
{/* Scroll Indicator */}
|
||||||
<motion.div
|
<motion.button
|
||||||
variants={itemVariants}
|
// onClick={() => handleScrollToSection("skills")}
|
||||||
animate={{ y: [0, 12, 0] }}
|
animate={{ y: [0, 12, 0] }}
|
||||||
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
|
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
|
||||||
className="flex justify-center"
|
className="flex justify-center mt-20 w-full cursor-pointer hover:text-green-400 transition-colors text-green-500"
|
||||||
|
aria-label="Scroll to skills section"
|
||||||
>
|
>
|
||||||
<ArrowDown className="text-green-500" size={32} />
|
<ArrowDown size={32} />
|
||||||
</motion.div>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { Menu, X } from 'lucide-react';
|
import { Menu, X } from "lucide-react";
|
||||||
import { useNavigation } from '../hooks/useNavigation';
|
import { useNavigation } from "../hooks/useNavigation";
|
||||||
import { useScrollToSection } from '../../../shared/hooks/useScrollToSection';
|
import { useScrollToSection } from "../../../shared/hooks/useScrollToSection";
|
||||||
import Button from '../../../shared/components/Button';
|
import Button from "../../../shared/components/Button";
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ id: 'home', label: 'Über mich' },
|
{ id: "home", label: "Über mich" },
|
||||||
{ id: 'skills', label: 'Fähigkeiten' },
|
{ id: "skills", label: "Fähigkeiten" },
|
||||||
{ id: 'projects', label: 'Projekte' },
|
{ id: "projects", label: "Projekte" },
|
||||||
{ id: 'experience', label: 'Erfahrung' },
|
{ id: "experience", label: "Erfahrung" },
|
||||||
{ id: 'contact', label: 'Kontakt' },
|
{ id: "contact", label: "Kontakt" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Navigation: React.FC = () => {
|
export const Navigation: React.FC = () => {
|
||||||
@@ -27,81 +27,98 @@ export const Navigation: React.FC = () => {
|
|||||||
<motion.nav
|
<motion.nav
|
||||||
initial={{ y: -120 }}
|
initial={{ y: -120 }}
|
||||||
animate={{ y: 0 }}
|
animate={{ y: 0 }}
|
||||||
transition={{ duration: 0.45, ease: 'easeOut' }}
|
transition={{ duration: 0.45, ease: "easeOut" }}
|
||||||
className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
className={`fixed top-0 w-full z-50 ${
|
||||||
isScrolled
|
isScrolled
|
||||||
? 'bg-black/90 backdrop-blur-xl border-b border-green-500/20 shadow-lg shadow-emerald-500/5'
|
? "bg-black/90 backdrop-blur-xl border-b border-green-500/20 shadow-lg shadow-emerald-500/5"
|
||||||
: 'bg-transparent'
|
: "bg-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-16">
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo */}
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
className="text-2xl font-bold text-green-500 cursor-pointer"
|
className="text-2xl font-bold text-green-500 cursor-pointer"
|
||||||
onClick={() => handleNavClick('home')}
|
onClick={() => handleNavClick("home")}
|
||||||
>
|
>
|
||||||
</> Robert Bretz
|
</> Robert Bretz
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
<div className="hidden md:flex gap-8">
|
<div className="hidden md:flex gap-8">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handleNavClick(item.id)}
|
onClick={() => handleNavClick(item.id)}
|
||||||
className={`text-sm font-medium transition-colors duration-300 pb-2 border-b-2 ${
|
whileHover={activeSection !== item.id ? { color: '#ffffff' } : {}}
|
||||||
|
className={`text-sm font-medium pb-2 border-b-2 ${
|
||||||
activeSection === item.id
|
activeSection === item.id
|
||||||
? 'text-green-500 border-green-500'
|
? "text-green-500 border-green-500"
|
||||||
: 'text-gray-300 border-transparent hover:text-white'
|
: "text-gray-300 border-transparent"
|
||||||
}`}
|
}`}
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop CTA + Mobile Toggle */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="hidden md:block">
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="hidden md:block"
|
||||||
|
>
|
||||||
<Button variant="primary" size="sm">
|
<Button variant="primary" size="sm">
|
||||||
Anfrage
|
Anfrage
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<button
|
{/* Mobile Menu Toggle */}
|
||||||
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Open mobile menu"
|
aria-label="Open mobile menu"
|
||||||
className="md:hidden p-2 rounded-full border border-green-500/20 bg-green-500/10 text-green-400 hover:text-white hover:border-green-500 transition-all"
|
className="md:hidden p-2 rounded-full border border-green-500/20 bg-green-500/10 text-green-400"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
|
whileHover={{ scale: 1.1, borderColor: "#22c55e", color: "#ffffff" }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
>
|
>
|
||||||
{isOpen ? <X size={22} /> : <Menu size={22} />}
|
{isOpen ? <X size={22} /> : <Menu size={22} />}
|
||||||
</button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, height: 0 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, height: "auto" }}
|
||||||
exit={{ opacity: 0, y: -20 }}
|
exit={{ opacity: 0, height: 0 }}
|
||||||
className="md:hidden bg-black/95 border-t border-green-500/20"
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
||||||
|
className="md:hidden bg-black/95 border-t border-green-500/20 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="px-6 py-6 space-y-4">
|
<div className="px-6 py-6 space-y-2">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item, index) => (
|
||||||
<button
|
<motion.button
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => handleNavClick(item.id)}
|
onClick={() => handleNavClick(item.id)}
|
||||||
className={`w-full text-left text-lg font-medium px-4 py-3 rounded-xl transition-colors duration-300 ${
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
whileHover={activeSection !== item.id ? { scale: 1.02, x: 4, backgroundColor: 'rgba(255,255,255,0.05)' } : { scale: 1.02, x: 4 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
className={`w-full text-left text-lg font-medium px-4 py-3 rounded-xl ${
|
||||||
activeSection === item.id
|
activeSection === item.id
|
||||||
? 'bg-green-500/15 text-green-400'
|
? "bg-green-500/15 text-green-400"
|
||||||
: 'text-gray-300 hover:bg-white/5 hover:text-white'
|
: "text-gray-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -18,35 +18,43 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({ project, index }) => {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.45, delay: index * 0.08, ease: 'easeOut' }}
|
transition={{ duration: 0.45, delay: index * 0.08, ease: 'easeOut' }}
|
||||||
whileHover={{ y: -8, scale: 1.01 }}
|
whileHover={{ y: -8, scale: 1.01 }}
|
||||||
className="group rounded-3xl overflow-hidden border border-green-500/25 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/55 transition-all duration-300 shadow-[0_20px_60px_-40px_rgba(34,197,94,0.8)]"
|
className="rounded-3xl overflow-hidden border border-green-500/25 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
<div className="relative h-64 overflow-hidden bg-gradient-to-br from-gray-900 to-black">
|
<div className="relative h-64 overflow-hidden bg-gradient-to-br from-gray-900 to-black">
|
||||||
<img
|
<motion.img
|
||||||
src={project.image}
|
src={project.image}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500 ease-out"
|
className="w-full h-full object-cover"
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/85 to-transparent"></div>
|
<div className="absolute inset-0 bg-gradient-to-t from-black/85 to-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="inline-flex items-center gap-2 mb-4 px-3 py-1 rounded-full bg-green-500/15 border border-green-500/25">
|
<motion.div
|
||||||
|
className="inline-flex items-center gap-2 mb-4 px-3 py-1 rounded-full bg-green-500/15 border border-green-500/25"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
|
>
|
||||||
<span className="text-green-400 text-xs font-semibold">{project.category}</span>
|
<span className="text-green-400 text-xs font-semibold">{project.category}</span>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
<h3 className="text-2xl font-bold text-white mb-3">{project.title}</h3>
|
<h3 className="text-2xl font-bold text-white mb-3">{project.title}</h3>
|
||||||
<p className="text-gray-400 text-sm mb-5 line-clamp-3">{project.description}</p>
|
<p className="text-gray-400 text-sm mb-5 line-clamp-3">{project.description}</p>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 mb-6">
|
<div className="flex flex-wrap gap-2 mb-6">
|
||||||
{project.tags.slice(0, 3).map((tag) => (
|
{project.tags.slice(0, 3).map((tag) => (
|
||||||
<span
|
<motion.span
|
||||||
key={tag}
|
key={tag}
|
||||||
|
whileHover={{ scale: 1.05, backgroundColor: 'rgba(34,197,94,0.2)' }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
className="text-xs px-2 py-1 rounded-full border border-green-500/25 text-green-300 bg-white/5"
|
className="text-xs px-2 py-1 rounded-full border border-green-500/25 text-green-300 bg-white/5"
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</motion.span>
|
||||||
))}
|
))}
|
||||||
{project.tags.length > 3 && (
|
{project.tags.length > 3 && (
|
||||||
<span className="text-xs px-2 py-1 rounded-full text-gray-400 bg-gray-900/40">
|
<span className="text-xs px-2 py-1 rounded-full text-gray-400 bg-gray-900/40">
|
||||||
@@ -61,9 +69,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({ project, index }) => {
|
|||||||
href={project.link}
|
href={project.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05, backgroundColor: 'rgba(34,197,94,0.3)' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-2xl bg-green-500/20 hover:bg-green-500/30 text-green-300 transition-colors text-sm font-medium"
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-2xl bg-green-500/20 text-green-300 text-sm font-medium"
|
||||||
>
|
>
|
||||||
<ExternalLink size={16} />
|
<ExternalLink size={16} />
|
||||||
Live
|
Live
|
||||||
@@ -74,9 +82,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({ project, index }) => {
|
|||||||
href={project.github}
|
href={project.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05, borderColor: 'rgba(34,197,94,0.5)', color: '#ffffff' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-2xl border border-green-500/25 hover:border-green-500/50 text-gray-300 hover:text-white transition-colors text-sm font-medium"
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-2xl border border-green-500/25 text-gray-300 text-sm font-medium"
|
||||||
>
|
>
|
||||||
<FaGithub size={16} />
|
<FaGithub size={16} />
|
||||||
Code
|
Code
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const ProjectsSection: React.FC = () => {
|
|||||||
|
|
||||||
const filteredProjects = projectsData.filter(
|
const filteredProjects = projectsData.filter(
|
||||||
(project: Project) =>
|
(project: Project) =>
|
||||||
selectedCategory === 'All' || project.category === selectedCategory
|
selectedCategory === 'Alle' || project.category === selectedCategory
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,10 +53,19 @@ export const ProjectsSection: React.FC = () => {
|
|||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`px-6 py-2 rounded-full font-medium transition-all duration-300 ${
|
animate={{
|
||||||
|
backgroundColor:
|
||||||
|
selectedCategory === category ? 'rgba(34,197,94,1)' : 'transparent',
|
||||||
|
borderColor:
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-green-500 text-black'
|
? 'rgba(34,197,94,1)'
|
||||||
: 'border border-green-500/30 text-gray-300 hover:border-green-500/60'
|
: 'rgba(34,197,94,0.3)',
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={`px-6 py-2 rounded-full font-medium ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'text-black'
|
||||||
|
: 'border border-green-500/30 text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
|||||||
@@ -32,13 +32,17 @@ export const SkillCard: React.FC<SkillCardProps> = ({ skill, index }) => {
|
|||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.45, delay: index * 0.08, ease: 'easeOut' }}
|
transition={{ duration: 0.45, delay: index * 0.08, ease: 'easeOut' }}
|
||||||
whileHover={{ scale: 1.025, y: -6 }}
|
whileHover={{ scale: 1.025, y: -6, boxShadow: '0 20px 60px -40px rgba(34,197,94,0.75)' }}
|
||||||
className="p-6 rounded-3xl border border-green-500/25 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/60 transition-all duration-300 shadow-[0_20px_60px_-40px_rgba(34,197,94,0.75)]"
|
className="p-6 rounded-3xl border border-green-500/25 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between mb-4">
|
<div className="flex items-start justify-between mb-4">
|
||||||
<div className="p-3 rounded-2xl bg-green-500/15">
|
<motion.div
|
||||||
|
className="p-3 rounded-2xl bg-green-500/15"
|
||||||
|
whileHover={{ scale: 1.1, rotate: 5 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300 }}
|
||||||
|
>
|
||||||
{IconComponent && <IconComponent className="text-green-500" size={24} />}
|
{IconComponent && <IconComponent className="text-green-500" size={24} />}
|
||||||
</div>
|
</motion.div>
|
||||||
<span className={`text-xs font-semibold px-3 py-1 rounded-full ${levelBg[skill.level]}`}>
|
<span className={`text-xs font-semibold px-3 py-1 rounded-full ${levelBg[skill.level]}`}>
|
||||||
<span className={levelColors[skill.level]}>{skill.level}</span>
|
<span className={levelColors[skill.level]}>{skill.level}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -48,8 +52,8 @@ export const SkillCard: React.FC<SkillCardProps> = ({ skill, index }) => {
|
|||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-gray-400">Experience</span>
|
<span className="text-gray-400">Erfahrung</span>
|
||||||
<span className="text-green-400 font-medium">{skill.years}+ yrs</span>
|
<span className="text-green-400 font-medium">{skill.years}+ Jahre</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const SkillsSection: React.FC = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const filteredSkills = skillsData.filter(
|
const filteredSkills = skillsData.filter(
|
||||||
(skill) => selectedCategory === 'All' || skill.category === selectedCategory
|
(skill) => selectedCategory === 'Alle' || skill.category === selectedCategory
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -66,10 +66,19 @@ export const SkillsSection: React.FC = () => {
|
|||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className={`px-6 py-2 rounded-full font-medium transition-all duration-300 ${
|
animate={{
|
||||||
|
backgroundColor:
|
||||||
|
selectedCategory === category ? 'rgba(34,197,94,1)' : 'transparent',
|
||||||
|
borderColor:
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-green-500 text-black'
|
? 'rgba(34,197,94,1)'
|
||||||
: 'border border-green-500/30 text-gray-300 hover:border-green-500/60'
|
: 'rgba(34,197,94,0.3)',
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className={`px-6 py-2 rounded-full font-medium ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'text-black'
|
||||||
|
: 'border border-green-500/30 text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
Box,
|
Box,
|
||||||
FileText,
|
FileText,
|
||||||
Gamepad,
|
Gamepad2,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Smartphone,
|
||||||
|
Cpu,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { Skill } from '../../../shared/types';
|
import type { Skill } from '../../../shared/types';
|
||||||
|
|
||||||
@@ -34,11 +36,11 @@ export const iconMap = {
|
|||||||
'Linux': Terminal,
|
'Linux': Terminal,
|
||||||
'Clean Architecture': Layers,
|
'Clean Architecture': Layers,
|
||||||
'Vertical Slice Architecture': Layers,
|
'Vertical Slice Architecture': Layers,
|
||||||
'Design Patterns': Layout,
|
'Design Patterns': Cpu,
|
||||||
'Figma': PenTool,
|
'Figma': PenTool,
|
||||||
'Unity': Gamepad,
|
'Unity': Gamepad2,
|
||||||
'Blender': Package,
|
'Blender': Package,
|
||||||
'Godot': Package,
|
'Godot': Gamepad2,
|
||||||
'LaTeX': FileText,
|
'LaTeX': FileText,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +56,7 @@ export const skillsData: Array<Omit<Skill, 'icon'> & { iconName: keyof typeof ic
|
|||||||
name: 'Vue.js',
|
name: 'Vue.js',
|
||||||
iconName: 'Vue.js',
|
iconName: 'Vue.js',
|
||||||
level: 'Advanced',
|
level: 'Advanced',
|
||||||
years: 3,
|
years: 0.5,
|
||||||
category: 'Frontend',
|
category: 'Frontend',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
className = '',
|
className = '',
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses = 'font-semibold rounded-full transition-all duration-300';
|
const baseClasses = 'font-semibold rounded-full';
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: 'px-4 py-2 text-sm',
|
sm: 'px-4 py-2 text-sm',
|
||||||
@@ -25,15 +25,13 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
primary:
|
primary: 'bg-green-500 text-black',
|
||||||
'bg-green-500 hover:bg-green-600 text-black shadow-lg hover:shadow-green-500/50',
|
secondary: 'border-2 border-green-500 text-green-500',
|
||||||
secondary:
|
|
||||||
'border-2 border-green-500 text-green-500 hover:bg-green-500/10',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<motion.button
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05, boxShadow: '0 10px 25px rgba(34,197,94,0.2)' }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
className={`${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
||||||
|
|||||||
@@ -27,13 +27,22 @@ const Footer: React.FC = () => {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<h4 className="text-white font-semibold mb-4">Quick Links</h4>
|
<h4 className="text-white font-semibold mb-4">Schnellzugriff</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{['About', 'Skills', 'Projects', 'Contact'].map((link) => (
|
{[
|
||||||
<li key={link}>
|
{ label: 'Über mich', href: '#home' },
|
||||||
<a href={`#${link.toLowerCase()}`} className="text-gray-400 hover:text-green-400 transition-colors">
|
{ label: 'Fähigkeiten', href: '#skills' },
|
||||||
{link}
|
{ label: 'Projekte', href: '#projects' },
|
||||||
</a>
|
{ label: 'Kontakt', href: '#contact' },
|
||||||
|
].map((link) => (
|
||||||
|
<li key={link.label}>
|
||||||
|
<motion.a
|
||||||
|
href={link.href}
|
||||||
|
whileHover={{ color: '#22c55e' }}
|
||||||
|
className="text-gray-400"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</motion.a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -46,9 +55,9 @@ const Footer: React.FC = () => {
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
>
|
>
|
||||||
<h4 className="text-white font-semibold mb-4">Services</h4>
|
<h4 className="text-white font-semibold mb-4">Leistungen</h4>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{['Frontend Development', 'UI/UX Design', 'Full Stack Dev'].map((service) => (
|
{['Frontend-Entwicklung', 'UI/UX-Design', 'Fullstack-Entwicklung'].map((service) => (
|
||||||
<li key={service}>
|
<li key={service}>
|
||||||
<span className="text-gray-400">{service}</span>
|
<span className="text-gray-400">{service}</span>
|
||||||
</li>
|
</li>
|
||||||
@@ -83,7 +92,7 @@ const Footer: React.FC = () => {
|
|||||||
© {currentYear} Robert Bretz. All rights reserved.
|
© {currentYear} Robert Bretz. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-sm mt-4 md:mt-0">
|
<p className="text-gray-500 text-sm mt-4 md:mt-0">
|
||||||
Built with 💚 using React & Tailwind CSS
|
Erstellt mit 💚 in React & Tailwind CSS
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Settings } from 'lucide-react';
|
||||||
|
import { useTheme } from '../../shared/context/ThemeContext';
|
||||||
|
import { themeColors, type ThemeColor } from '../../shared/config/theme';
|
||||||
|
|
||||||
|
export const ThemeSelector: React.FC = () => {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const colors = Object.keys(themeColors) as ThemeColor[];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-8 right-8 z-50">
|
||||||
|
{/* Settings Button */}
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="w-14 h-14 bg-gradient-to-br from-green-500/20 to-emerald-500/10 backdrop-blur-md border border-green-500/30 rounded-full flex items-center justify-center text-green-500 shadow-lg hover:shadow-green-500/20"
|
||||||
|
>
|
||||||
|
<Settings size={24} />
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
{/* Color Selector */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||||
|
animate={isOpen ? { opacity: 1, scale: 1, y: 0 } : { opacity: 0, scale: 0.8, y: 20 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className={`absolute bottom-20 right-0 ${isOpen ? 'pointer-events-auto' : 'pointer-events-none'}`}
|
||||||
|
>
|
||||||
|
<div className="bg-black/95 backdrop-blur-xl border border-green-500/20 rounded-2xl p-4 shadow-2xl">
|
||||||
|
<p className="text-xs font-semibold text-gray-400 mb-3 px-2">Design-Farbe</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{colors.map((color) => {
|
||||||
|
const colorData = themeColors[color];
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={color}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={() => {
|
||||||
|
setTheme(color);
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-10 h-10 rounded-full transition-all border-2 ${
|
||||||
|
theme === color
|
||||||
|
? 'border-white shadow-lg'
|
||||||
|
: 'border-transparent shadow-md'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: colorData.primary }}
|
||||||
|
title={color.charAt(0).toUpperCase() + color.slice(1)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
export type ThemeColor = 'green' | 'blue' | 'purple' | 'pink' | 'red' | 'orange';
|
||||||
|
|
||||||
|
export const themeColors: Record<ThemeColor, {
|
||||||
|
primary: string;
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
rgb: string;
|
||||||
|
tailwind: {
|
||||||
|
primary: string;
|
||||||
|
light: string;
|
||||||
|
dark: string;
|
||||||
|
};
|
||||||
|
}> = {
|
||||||
|
green: {
|
||||||
|
primary: '#22c55e',
|
||||||
|
light: '#4ade80',
|
||||||
|
dark: '#16a34a',
|
||||||
|
rgb: '34, 197, 94',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'green-500',
|
||||||
|
light: 'green-400',
|
||||||
|
dark: 'green-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
primary: '#3b82f6',
|
||||||
|
light: '#60a5fa',
|
||||||
|
dark: '#1d4ed8',
|
||||||
|
rgb: '59, 130, 246',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'blue-500',
|
||||||
|
light: 'blue-400',
|
||||||
|
dark: 'blue-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
primary: '#a855f7',
|
||||||
|
light: '#c084fc',
|
||||||
|
dark: '#7e22ce',
|
||||||
|
rgb: '168, 85, 247',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'purple-500',
|
||||||
|
light: 'purple-400',
|
||||||
|
dark: 'purple-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pink: {
|
||||||
|
primary: '#ec4899',
|
||||||
|
light: '#f472b6',
|
||||||
|
dark: '#be185d',
|
||||||
|
rgb: '236, 72, 153',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'pink-500',
|
||||||
|
light: 'pink-400',
|
||||||
|
dark: 'pink-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
primary: '#ef4444',
|
||||||
|
light: '#f87171',
|
||||||
|
dark: '#b91c1c',
|
||||||
|
rgb: '239, 68, 68',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'red-500',
|
||||||
|
light: 'red-400',
|
||||||
|
dark: 'red-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
primary: '#f97316',
|
||||||
|
light: '#fb923c',
|
||||||
|
dark: '#c2410c',
|
||||||
|
rgb: '249, 115, 22',
|
||||||
|
tailwind: {
|
||||||
|
primary: 'orange-500',
|
||||||
|
light: 'orange-400',
|
||||||
|
dark: 'orange-600',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTheme: ThemeColor = 'green';
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { ThemeColor, defaultTheme } from '../config/theme';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: ThemeColor;
|
||||||
|
setTheme: (theme: ThemeColor) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setThemeState] = useState<ThemeColor>(defaultTheme);
|
||||||
|
|
||||||
|
// Load theme from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('portfolio-theme') as ThemeColor | null;
|
||||||
|
if (savedTheme) {
|
||||||
|
setThemeState(savedTheme);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setTheme = (newTheme: ThemeColor) => {
|
||||||
|
setThemeState(newTheme);
|
||||||
|
localStorage.setItem('portfolio-theme', newTheme);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useTheme must be used within ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
Generated
+874
-667
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user