Compare commits
1 Commits
965d2a41dd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52bf3b96b1 |
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
+923
-716
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user