Skip to content

Commit b0b5e9f

Browse files
Y-RyuZUclaude
andcommitted
refactor: reorganize project structure with section-based architecture
- Create sections/ directory for main page sections (Hero, About, Projects, Contact) - Add features/ directory for feature-specific components - Implement centralized navigation and layout components - Add comprehensive type definitions for projects, skills, and common interfaces - Create constants for static data (contacts, projects, skills) - Add custom hooks for data management - Implement consistent styling with lib/styles - Enable breakable glass cards for ProjectCard and ContactSection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9433d5f commit b0b5e9f

33 files changed

Lines changed: 2684 additions & 0 deletions
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use client';
2+
3+
import { useState, useEffect, useMemo } from 'react';
4+
import { useMinecraftTime } from '@/contexts/MinecraftTimeContext';
5+
import { DEFAULT_THEME, type BackgroundTheme, type TimeOfDay } from '@/lib/constants/background-themes';
6+
import { cn } from '@/lib/utils';
7+
import Image from 'next/image';
8+
9+
interface MinecraftBackgroundProps {
10+
theme?: BackgroundTheme;
11+
opacity?: number;
12+
blur?: number;
13+
parallax?: boolean;
14+
className?: string;
15+
}
16+
17+
/**
18+
* Dynamic background component that changes based on time of day
19+
* Supports 8 different time periods with smooth transitions
20+
*/
21+
export default function MinecraftBackground({
22+
theme = DEFAULT_THEME,
23+
opacity = 0.6,
24+
blur = 0,
25+
parallax = false,
26+
className
27+
}: MinecraftBackgroundProps) {
28+
const { currentTimeOfDay, timeProgress } = useMinecraftTime();
29+
const [mounted, setMounted] = useState(false);
30+
const [loadedImages, setLoadedImages] = useState<Set<TimeOfDay>>(new Set());
31+
32+
useEffect(() => {
33+
setMounted(true);
34+
}, []);
35+
36+
// Preload adjacent time period images for smooth transitions
37+
useEffect(() => {
38+
if (!mounted) return;
39+
40+
const timeOrder: TimeOfDay[] = [
41+
'dawn', 'morning', 'noon', 'afternoon',
42+
'dusk', 'twilight', 'night', 'midnight'
43+
];
44+
45+
const currentIndex = timeOrder.indexOf(currentTimeOfDay);
46+
const prevIndex = (currentIndex - 1 + timeOrder.length) % timeOrder.length;
47+
const nextIndex = (currentIndex + 1) % timeOrder.length;
48+
49+
const imagesToPreload = [
50+
currentTimeOfDay,
51+
timeOrder[prevIndex],
52+
timeOrder[nextIndex]
53+
];
54+
55+
imagesToPreload.forEach((timeOfDay) => {
56+
if (!loadedImages.has(timeOfDay)) {
57+
const img = new window.Image();
58+
img.src = theme.backgrounds[timeOfDay];
59+
img.onload = () => {
60+
setLoadedImages(prev => new Set([...prev, timeOfDay]));
61+
};
62+
}
63+
});
64+
}, [currentTimeOfDay, theme.backgrounds, loadedImages, mounted]);
65+
66+
// Determine if it's dark or light based on time of day
67+
const isDarkTime = useMemo(() => {
68+
// Avoid hydration mismatch - default to light theme until mounted
69+
if (!mounted) return false;
70+
return ['dusk', 'twilight', 'night', 'midnight'].includes(currentTimeOfDay);
71+
}, [currentTimeOfDay, mounted]);
72+
73+
return (
74+
<div
75+
className={cn(
76+
'fixed inset-0 -z-10 overflow-hidden',
77+
className
78+
)}
79+
style={{
80+
opacity,
81+
filter: blur > 0 ? `blur(${blur}px)` : undefined,
82+
transform: parallax ? 'translateZ(-1px) scale(1.5)' : undefined,
83+
}}
84+
>
85+
{/* Render all background images with opacity transitions */}
86+
{mounted ? (
87+
// After hydration: show dynamic backgrounds
88+
Object.entries(theme.backgrounds).map(([timeOfDay, src]) => {
89+
const isActive = timeOfDay === currentTimeOfDay;
90+
const isLoaded = loadedImages.has(timeOfDay as TimeOfDay);
91+
92+
return (
93+
<div
94+
key={timeOfDay}
95+
className={cn(
96+
'absolute inset-0 transition-opacity duration-1000 ease-in-out',
97+
isActive ? 'opacity-100' : 'opacity-0'
98+
)}
99+
>
100+
{isLoaded || isActive ? (
101+
<Image
102+
src={src}
103+
alt={`${timeOfDay} background`}
104+
fill
105+
sizes="100vw"
106+
priority={isActive}
107+
quality={90}
108+
className="object-cover"
109+
/>
110+
) : null}
111+
</div>
112+
);
113+
})
114+
) : (
115+
// Before hydration: show default noon background
116+
<div className="absolute inset-0">
117+
<Image
118+
src={theme.backgrounds.noon}
119+
alt="default background"
120+
fill
121+
sizes="100vw"
122+
priority
123+
quality={90}
124+
className="object-cover"
125+
/>
126+
</div>
127+
)}
128+
129+
{/* Overlay gradient for better text readability */}
130+
<div
131+
className={cn(
132+
'absolute inset-0',
133+
'bg-gradient-to-b',
134+
isDarkTime
135+
? 'from-black/20 via-black/10 to-black/20'
136+
: 'from-white/10 via-transparent to-white/10'
137+
)}
138+
/>
139+
140+
{/* Time transition overlay - subtle gradient based on progress */}
141+
{mounted && (
142+
<div
143+
className="absolute inset-0 transition-opacity duration-500 ease-in-out pointer-events-none"
144+
style={{
145+
opacity: Math.sin(timeProgress * Math.PI) * 0.05,
146+
background: `linear-gradient(to bottom,
147+
transparent 0%,
148+
rgba(255, 200, 100, ${timeProgress * 0.05}) 50%,
149+
transparent 100%)`
150+
}}
151+
/>
152+
)}
153+
</div>
154+
);
155+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client';
2+
3+
import { useSmoothScroll, useScrollSpy } from '@/hooks';
4+
import { cn } from '@/lib/utils';
5+
6+
const sections = [
7+
{ id: 'about', label: 'About' },
8+
{ id: 'skills', label: 'Skills' },
9+
{ id: 'projects', label: 'Projects' },
10+
{ id: 'contact', label: 'Contact' },
11+
];
12+
13+
export default function ScrollNav() {
14+
const { scrollToSection } = useSmoothScroll();
15+
const activeSection = useScrollSpy(sections.map(s => s.id));
16+
17+
return (
18+
<nav className="fixed right-8 top-1/2 -translate-y-1/2 z-40 hidden lg:block">
19+
<ul className="space-y-3">
20+
{sections.map((section) => (
21+
<li key={section.id}>
22+
<button
23+
onClick={() => scrollToSection(section.id)}
24+
className={cn(
25+
"group flex items-center justify-end gap-2 transition-all",
26+
activeSection === section.id && "scale-110"
27+
)}
28+
aria-label={`Navigate to ${section.label}`}
29+
>
30+
<span className={cn(
31+
"text-xs font-medium opacity-0 -translate-x-2 transition-all group-hover:opacity-100 group-hover:translate-x-0",
32+
activeSection === section.id && "opacity-100 translate-x-0 text-blue-600 dark:text-blue-400"
33+
)}>
34+
{section.label}
35+
</span>
36+
<span
37+
className={cn(
38+
"block w-2 h-2 rounded-full transition-all",
39+
activeSection === section.id
40+
? "w-8 bg-blue-600 dark:bg-blue-400"
41+
: "bg-gray-400 dark:bg-gray-600 group-hover:bg-gray-600 dark:group-hover:bg-gray-400"
42+
)}
43+
/>
44+
</button>
45+
</li>
46+
))}
47+
</ul>
48+
</nav>
49+
);
50+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
import Image from 'next/image';
5+
import { GlassCard, GlassCardContent, GlassCardFooter, GlassCardHeader } from '@/components/ui/glass-card';
6+
import type { Project } from '@/lib/types/project';
7+
import { ExternalLink, Users, Activity } from 'lucide-react';
8+
import { useMinecraftAssets } from '@/hooks';
9+
10+
interface ProjectCardProps {
11+
project: Project;
12+
}
13+
14+
export default function ProjectCard({ project }: ProjectCardProps) {
15+
const { getCategoryItem } = useMinecraftAssets();
16+
const achievementItem = getCategoryItem(project.category);
17+
18+
return (
19+
<GlassCard className="h-full flex flex-col transition-all hover:scale-[1.02] pixelated" breakable={true}>
20+
{/* Achievement-style header */}
21+
<div className="h-32 relative overflow-hidden bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-900 border-b-2 border-yellow-600/20 dark:border-yellow-500/20">
22+
<div className="absolute inset-0 bg-gradient-to-br from-transparent via-transparent to-yellow-50/30 dark:to-yellow-900/20" />
23+
<div className="relative h-full flex items-center justify-center">
24+
<div className="mc-item-frame p-4 rounded">
25+
<Image
26+
src={achievementItem}
27+
alt={project.title}
28+
width={48}
29+
height={48}
30+
className="mc-pixel"
31+
/>
32+
</div>
33+
</div>
34+
{/* Achievement sparkle effect */}
35+
<div className="absolute top-2 right-2">
36+
<div className="w-2 h-2 bg-yellow-400/50 rounded-full animate-pulse" />
37+
</div>
38+
</div>
39+
40+
<GlassCardHeader className="pb-3">
41+
<h3 className="text-lg font-medium text-gray-900 dark:text-white line-clamp-2">
42+
{project.title}
43+
</h3>
44+
</GlassCardHeader>
45+
46+
<GlassCardContent className="flex-1">
47+
<p className="text-sm text-gray-800 dark:text-gray-400 mb-4 line-clamp-2">
48+
{project.shortDescription}
49+
</p>
50+
51+
{/* Minimal metrics */}
52+
{project.metrics && (
53+
<div className="flex items-center gap-4 mb-4 text-xs text-gray-700 dark:text-gray-500">
54+
{project.metrics.users && (
55+
<div className="flex items-center gap-1">
56+
<Users className="h-3 w-3" />
57+
<span>{project.metrics.users}+</span>
58+
</div>
59+
)}
60+
{project.metrics.community && (
61+
<div className="flex items-center gap-1">
62+
<Activity className="h-3 w-3" />
63+
<span>{project.metrics.community.toLocaleString()}</span>
64+
</div>
65+
)}
66+
</div>
67+
)}
68+
69+
{/* Tech stack - simple text */}
70+
<div className="flex flex-wrap gap-1.5">
71+
{project.techStack.slice(0, 3).map((tech) => (
72+
<span
73+
key={tech.name}
74+
className="px-2 py-0.5 text-xs text-gray-800 dark:text-gray-400 border border-gray-200 dark:border-gray-700 rounded"
75+
>
76+
{tech.name}
77+
</span>
78+
))}
79+
{project.techStack.length > 3 && (
80+
<span className="px-2 py-0.5 text-xs text-gray-700 dark:text-gray-500">
81+
+{project.techStack.length - 3}
82+
</span>
83+
)}
84+
</div>
85+
</GlassCardContent>
86+
87+
<GlassCardFooter className="pt-3 border-t border-gray-100 dark:border-gray-800">
88+
{project.links?.website ? (
89+
<Link
90+
href={project.links.website}
91+
target="_blank"
92+
rel="noopener noreferrer"
93+
className="inline-flex items-center gap-1 text-sm text-gray-800 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
94+
>
95+
View Project
96+
<ExternalLink className="h-3 w-3" />
97+
</Link>
98+
) : project.links?.github ? (
99+
<Link
100+
href={project.links.github}
101+
target="_blank"
102+
rel="noopener noreferrer"
103+
className="inline-flex items-center gap-1 text-sm text-gray-800 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white transition-colors"
104+
>
105+
View on GitHub
106+
<ExternalLink className="h-3 w-3" />
107+
</Link>
108+
) : (
109+
<span className="text-sm text-gray-600 dark:text-gray-600">
110+
Coming Soon
111+
</span>
112+
)}
113+
</GlassCardFooter>
114+
</GlassCard>
115+
);
116+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use client';
2+
3+
import ProjectCard from './ProjectCard';
4+
import { styles } from '@/lib/styles';
5+
import { useProjects, useFeaturedProjects } from '@/hooks';
6+
7+
interface ProjectsGridProps {
8+
featured?: boolean;
9+
}
10+
11+
export default function ProjectsGrid({ featured = false }: ProjectsGridProps) {
12+
// Use custom hooks for project management - must call unconditionally
13+
const featuredProjects = useFeaturedProjects(3);
14+
const allProjects = useProjects();
15+
16+
// Select the appropriate projects based on the featured prop
17+
const { projects } = featured ? featuredProjects : allProjects;
18+
19+
return (
20+
<section id="projects" className={styles.section.base}>
21+
<div className={styles.container}>
22+
<div className="text-center mb-12">
23+
<h2 className={styles.heading.h2}>
24+
{featured ? 'Featured Projects' : 'All Projects'}
25+
</h2>
26+
<p className={styles.text.secondary}>
27+
Recent work and side projects
28+
</p>
29+
</div>
30+
31+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8">
32+
{projects.map((project) => (
33+
<ProjectCard key={project.id} project={project} />
34+
))}
35+
</div>
36+
</div>
37+
</section>
38+
);
39+
}

0 commit comments

Comments
 (0)