Compare commits

..

1 Commits

Author SHA1 Message Date
robre 52bf3b96b1 Refactor code structure for improved readability and maintainability
Deploy Portfolio Selfmade / deploy (push) Successful in 31s
2026-05-10 21:30:31 +02:00
8 changed files with 1216 additions and 800 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-dotnettools.csdevkit",
"bradlc.vscode-tailwindcss"
]
}
+30
View File
@@ -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
}
}
+20 -14
View File
@@ -5,24 +5,30 @@ 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 (
<div className="bg-black text-white min-h-screen overflow-x-hidden"> <ThemeProvider>
{/* Background gradient decorations */} <div className="bg-black text-white min-h-screen overflow-x-hidden">
<div className="fixed inset-0 -z-10"> {/* Background gradient decorations */}
<div className="absolute top-0 left-0 w-96 h-96 bg-green-500/10 rounded-full blur-3xl opacity-50"></div> <div className="fixed inset-0 -z-10">
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl opacity-50"></div> <div className="absolute top-0 left-0 w-96 h-96 bg-green-500/10 rounded-full blur-3xl opacity-50"></div>
</div> <div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl opacity-50"></div>
</div>
<Navigation /> <Navigation />
<HeroSection /> <HeroSection />
<SkillsSection /> <SkillsSection />
<ProjectsSection /> <ProjectsSection />
<ExperienceSection /> <ExperienceSection />
<ContactSection /> <ContactSection />
<Footer /> <Footer />
</div>
<ThemeSelector />
</div>
</ThemeProvider>
); );
} }
@@ -1,9 +1,12 @@
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: {
@@ -33,14 +36,6 @@ export const HeroSection: React.FC = () => {
}, },
}; };
const borderVariants = {
hidden: { pathLength: 0 },
visible: {
pathLength: 1,
transition: { duration: 2, delay: 0.6 },
},
};
return ( return (
<section <section
id="home" id="home"
@@ -52,14 +47,9 @@ 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}
initial="hidden"
animate="visible"
className="max-w-7xl mx-auto w-full"
>
{/* Grid Layout: Text left, Image right on desktop, stacked on mobile */} {/* Grid Layout: Text left, Image right on desktop, stacked on mobile */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 lg:items-start">
{/* Text Content */} {/* Text Content */}
<div className="text-center lg:text-left"> <div className="text-center lg:text-left">
{/* Badge */} {/* Badge */}
@@ -73,7 +63,10 @@ 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">
@@ -82,39 +75,34 @@ export const HeroSection: React.FC = () => {
</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 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 <motion.div variants={itemVariants} className="grid grid-cols-3 gap-3 sm:gap-6 mb-12">
variants={itemVariants}
className="grid grid-cols-3 gap-3 sm:gap-6 mb-12"
>
<motion.div <motion.div
whileHover={{ scale: 1.05, y: -4 }} whileHover={{ scale: 1.05, y: -4 }}
transition={{ type: 'spring', stiffness: 300 }} transition={{ type: "spring", stiffness: 300 }}
className="border border-green-500/30 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm" 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">4+</div> <div className="text-2xl sm: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-xs sm:text-sm">Jahre Erfahrung</div>
</motion.div> </motion.div>
<motion.div <motion.div
whileHover={{ scale: 1.05, y: -4 }} whileHover={{ scale: 1.05, y: -4 }}
transition={{ type: 'spring', stiffness: 300 }} transition={{ type: "spring", stiffness: 300 }}
className="border border-green-500/30 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm" 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">30+</div> <div className="text-2xl sm:text-3xl font-bold text-green-500">30+</div>
<div className="text-gray-400 text-xs sm:text-sm">Projekte umgesetzt</div> <div className="text-gray-400 text-xs sm:text-sm">Projekte umgesetzt</div>
</motion.div> </motion.div>
<motion.div <motion.div
whileHover={{ scale: 1.05, y: -4 }} whileHover={{ scale: 1.05, y: -4 }}
transition={{ type: 'spring', stiffness: 300 }} transition={{ type: "spring", stiffness: 300 }}
className="border border-green-500/30 rounded-lg p-4 sm:p-5 bg-green-500/5 backdrop-blur-sm" 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-2xl sm:text-3xl font-bold text-green-500">99%</div>
<div className="text-gray-400 text-xs sm:text-sm">Performance-Fokus</div> <div className="text-gray-400 text-xs sm:text-sm">Performance-Fokus</div>
@@ -122,7 +110,10 @@ export const HeroSection: React.FC = () => {
</motion.div> </motion.div>
{/* CTA Buttons */} {/* CTA Buttons */}
<motion.div variants={itemVariants} className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start 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>
@@ -133,38 +124,28 @@ export const HeroSection: React.FC = () => {
</div> </div>
{/* Profile Image - Right side on desktop, centered on mobile */} {/* Profile Image - Right side on desktop, centered on mobile */}
<motion.div <motion.div variants={imageVariants} className="flex justify-center lg:justify-end">
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"> <div className="relative w-56 h-56 sm:w-72 sm:h-72 lg:w-96 lg:h-96">
{/* Animated green borders on sides */} {/* Animated circular green border */}
<svg <svg
className="absolute inset-0 w-full h-full pointer-events-none" className="absolute inset-0 w-full h-full pointer-events-none -scale-x-100"
viewBox="0 0 224 224" viewBox="0 0 400 400"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<motion.line <motion.circle
x1="10" cx="200"
y1="0" cy="200"
x2="10" r="200"
y2="224" fill="none"
stroke="#22c55e" stroke="#22c55e"
strokeWidth="3" strokeWidth="1.5"
variants={borderVariants} strokeLinecap="round"
initial="hidden" initial={{ strokeDasharray: 1200, strokeDashoffset: 1200, opacity: 1 }}
animate="visible" animate={{
/> strokeDashoffset: [1200, 0, -1200],
<motion.line opacity: [1, 1, 0],
x1="214" }}
y1="0" transition={{ duration: 4, delay: 0.8, ease: "easeInOut" }}
x2="214"
y2="224"
stroke="#22c55e"
strokeWidth="3"
variants={borderVariants}
initial="hidden"
animate="visible"
/> />
</svg> </svg>
@@ -172,21 +153,22 @@ export const HeroSection: React.FC = () => {
<img <img
src="/Bewerbungsfoto.png" src="/Bewerbungsfoto.png"
alt="Robert Bretz" alt="Robert Bretz"
className="w-full h-full object-cover rounded-3xl shadow-2xl shadow-green-500/30" className="w-full h-full object-cover rounded-full shadow-2xl shadow-green-500/30"
/> />
</div> </div>
</motion.div> </motion.div>
</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.5, repeat: Infinity, ease: 'easeInOut' }} transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
className="flex justify-center mt-16" 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>
); );
@@ -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>
);
};
+82
View File
@@ -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;
};
+923 -716
View File
File diff suppressed because it is too large Load Diff