Skip to content

Commit 43ecf2e

Browse files
authored
feat: add Minecraft-themed animations to minecraft template (#36)
1 parent d0c45a6 commit 43ecf2e

File tree

12 files changed

+238
-29
lines changed

12 files changed

+238
-29
lines changed

src/templates/minecraft/BlogCard.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export default function BlogCard({ blog, featured }: { blog: Blog; featured?: bo
2727
<div className="pointer-events-none absolute inset-0 border-t-2 border-l-2 border-[#c6c6c6]" />
2828
{/* Shadow edge (bottom & right) */}
2929
<div className="pointer-events-none absolute inset-0 border-b-2 border-r-2 border-[#555555]" />
30+
{/* Enchantment glint on featured posts */}
31+
{featured && (
32+
<div
33+
className="pointer-events-none absolute inset-0 animate-mc-enchant opacity-30"
34+
style={{
35+
backgroundImage: 'linear-gradient(120deg, transparent 30%, rgba(138,205,50,0.35) 50%, transparent 70%)',
36+
backgroundSize: '200% 100%',
37+
}}
38+
/>
39+
)}
3040

3141
<div className="relative flex items-start gap-2">
3242
<span className="mt-0.5 shrink-0 text-[#5c7a29] text-lg leading-none"></span>

src/templates/minecraft/BlogsSection.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { Link } from 'react-router-dom'
22
import { useSiteData } from '../../hooks/useSiteData'
33
import BlogCard from './BlogCard'
4+
import { useInView } from './useInView'
45

56
const PREVIEW_COUNT = 5
67

78
export default function BlogsSection() {
89
const { blogs, loading } = useSiteData()
10+
const { ref, visible } = useInView()
911
const preview = blogs.slice(0, PREVIEW_COUNT)
1012

1113
if (loading && preview.length === 0) return null
1214
if (!loading && blogs.length === 0) return null
1315

1416
return (
1517
<section id="blogs" className="bg-[#1a1a2e] py-12" aria-labelledby="blogs-title">
16-
<div className="mx-auto max-w-4xl px-6">
18+
<div ref={ref} className="mx-auto max-w-4xl px-6">
1719
<div className="mb-6 flex items-end justify-between">
1820
<div>
1921
<p className="text-xs uppercase tracking-widest text-[#5c7a29] mb-1">📜 Scroll Archive</p>
@@ -33,7 +35,13 @@ export default function BlogsSection() {
3335
</div>
3436
<div className="grid gap-2">
3537
{(loading ? [] : preview).map((blog, i) => (
36-
<BlogCard key={blog.id} blog={blog} featured={i === 0} />
38+
<div
39+
key={blog.id}
40+
className={visible ? 'animate-mc-fade-up' : 'opacity-0'}
41+
style={{ animationDelay: `${i * 0.08}s` }}
42+
>
43+
<BlogCard blog={blog} featured={i === 0} />
44+
</div>
3745
))}
3846
</div>
3947
</div>

src/templates/minecraft/CustomProjectsSection.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { Link } from 'react-router-dom'
22
import { useSiteData } from '../../hooks/useSiteData'
33
import ProjectCard from './ProjectCard'
4+
import { useInView } from './useInView'
45

56
const PREVIEW_COUNT = 4
67

78
export default function CustomProjectsSection() {
89
const { projects, loading } = useSiteData()
10+
const { ref, visible } = useInView()
911
const preview = projects.slice(0, PREVIEW_COUNT)
1012

1113
if (loading && preview.length === 0) return null
1214
if (!loading && projects.length === 0) return null
1315

1416
return (
1517
<section id="projects" className="bg-[#1a1a2e] py-12" aria-labelledby="projects-title">
16-
<div className="mx-auto max-w-4xl px-6">
18+
<div ref={ref} className="mx-auto max-w-4xl px-6">
1719
<div className="mb-6 flex items-end justify-between">
1820
<div>
1921
<p className="text-xs uppercase tracking-widest text-[#5c7a29] mb-1">🔨 Crafting Table</p>
@@ -32,8 +34,14 @@ export default function CustomProjectsSection() {
3234
)}
3335
</div>
3436
<div className="grid gap-2">
35-
{(loading ? [] : preview).map((project) => (
36-
<ProjectCard key={project.id} project={project} />
37+
{(loading ? [] : preview).map((project, i) => (
38+
<div
39+
key={project.id}
40+
className={visible ? 'animate-mc-fade-up' : 'opacity-0'}
41+
style={{ animationDelay: `${i * 0.08}s` }}
42+
>
43+
<ProjectCard project={project} />
44+
</div>
3745
))}
3846
</div>
3947
</div>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { useEffect, useRef } from 'react'
2+
3+
/**
4+
* Renders lightweight canvas-based floating particles that mimic
5+
* Minecraft XP-orb / firefly particles drifting upwards.
6+
*/
7+
export default function FloatingParticles() {
8+
const canvasRef = useRef<HTMLCanvasElement>(null)
9+
const animRef = useRef(0)
10+
11+
useEffect(() => {
12+
const canvas = canvasRef.current
13+
if (!canvas) return
14+
const ctx = canvas.getContext('2d')
15+
if (!ctx) return
16+
17+
function resize() {
18+
canvas!.width = canvas!.offsetWidth
19+
canvas!.height = canvas!.offsetHeight
20+
}
21+
resize()
22+
window.addEventListener('resize', resize)
23+
24+
type Particle = { x: number; y: number; vx: number; vy: number; size: number; alpha: number; color: string }
25+
26+
const COLORS = ['#8acd32', '#5CC0D0', '#FFAA00', '#ffff55']
27+
const COUNT = 28
28+
29+
const particles: Particle[] = Array.from({ length: COUNT }, () => ({
30+
x: Math.random() * canvas.width,
31+
y: Math.random() * canvas.height,
32+
vx: (Math.random() - 0.5) * 0.3,
33+
vy: -(0.2 + Math.random() * 0.4),
34+
size: 2 + Math.random() * 2,
35+
alpha: 0.15 + Math.random() * 0.35,
36+
color: COLORS[Math.floor(Math.random() * COLORS.length)],
37+
}))
38+
39+
function draw() {
40+
animRef.current = requestAnimationFrame(draw)
41+
ctx!.clearRect(0, 0, canvas!.width, canvas!.height)
42+
43+
for (const p of particles) {
44+
p.x += p.vx
45+
p.y += p.vy
46+
47+
// Wrap around
48+
if (p.y < -10) {
49+
p.y = canvas!.height + 10
50+
p.x = Math.random() * canvas!.width
51+
}
52+
if (p.x < -10) p.x = canvas!.width + 10
53+
if (p.x > canvas!.width + 10) p.x = -10
54+
55+
ctx!.globalAlpha = p.alpha
56+
ctx!.fillStyle = p.color
57+
58+
// Draw pixelated square particle (Minecraft style)
59+
const s = Math.round(p.size)
60+
ctx!.fillRect(Math.round(p.x), Math.round(p.y), s, s)
61+
}
62+
63+
ctx!.globalAlpha = 1
64+
}
65+
66+
draw()
67+
68+
return () => {
69+
cancelAnimationFrame(animRef.current)
70+
window.removeEventListener('resize', resize)
71+
}
72+
}, [])
73+
74+
return (
75+
<canvas
76+
ref={canvasRef}
77+
className="absolute inset-0 w-full h-full pointer-events-none"
78+
style={{ imageRendering: 'pixelated' }}
79+
aria-hidden="true"
80+
/>
81+
)
82+
}

src/templates/minecraft/GitHubSection.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import RepoCard from './RepoCard'
2+
import { useInView } from './useInView'
23

34
type ProjectRepo = {
45
name: string
@@ -21,11 +22,12 @@ export default function GitHubSection({
2122
body,
2223
repos,
2324
}: GitHubSectionProps) {
25+
const { ref, visible } = useInView()
2426
if (!repos?.length) return null
2527

2628
return (
2729
<section id="github" className="bg-[#1a1a2e] py-12" aria-labelledby="github-title">
28-
<div className="mx-auto max-w-4xl px-6">
30+
<div ref={ref} className="mx-auto max-w-4xl px-6">
2931
<div className="mb-6">
3032
<p className="text-xs uppercase tracking-widest text-[#5c7a29] mb-1">⛏️ Mining Repos</p>
3133
<h2 id="github-title" className="text-xl font-bold text-white drop-shadow-[2px_2px_0_#3f3f00]">
@@ -37,8 +39,14 @@ export default function GitHubSection({
3739
<p className="mt-1 text-xs text-[#777]">Found {repos.length} repos in world</p>
3840
</div>
3941
<div className="grid gap-2">
40-
{repos.map((repo) => (
41-
<RepoCard key={repo.url} repo={repo} />
42+
{repos.map((repo, i) => (
43+
<div
44+
key={repo.url}
45+
className={visible ? 'animate-mc-fade-up' : 'opacity-0'}
46+
style={{ animationDelay: `${i * 0.07}s` }}
47+
>
48+
<RepoCard repo={repo} />
49+
</div>
4250
))}
4351
</div>
4452
</div>

src/templates/minecraft/HeroSection.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useState, useEffect, useRef } from 'react'
2+
import FloatingParticles from './FloatingParticles'
23

34
type Hero = {
45
eyebrow: string
@@ -156,13 +157,14 @@ export default function HeroSection({ hero, snapshot }: HeroSectionProps) {
156157
return (
157158
<section id="hero" className="relative bg-[#1a1a2e] py-16 overflow-hidden" aria-labelledby="hero-title">
158159
<BlockGrid />
160+
<FloatingParticles />
159161

160162
{/* Dirt block border at the top */}
161163
<div className="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-[#6B8E3E] via-[#8B6E4E] to-[#6B8E3E]" style={{ imageRendering: 'pixelated' }} />
162164

163165
<div className="relative mx-auto max-w-4xl px-6">
164166
{/* Main panel — styled like the Minecraft inventory UI */}
165-
<div className="relative border-4 border-[#3b3b3b] bg-[#c6c6c6] p-1">
167+
<div className="relative border-4 border-[#3b3b3b] bg-[#c6c6c6] p-1 animate-mc-fade-up">
166168
{/* Inner bevel */}
167169
<div className="border-2 border-t-[#ffffff80] border-l-[#ffffff80] border-b-[#555555] border-r-[#555555] bg-[#8b8b8b] p-6 sm:p-8">
168170
<div className="flex flex-col sm:flex-row items-start gap-6">
@@ -197,10 +199,11 @@ export default function HeroSection({ hero, snapshot }: HeroSectionProps) {
197199
<div className="mt-4">
198200
<p className="text-xs text-[#555555] uppercase tracking-wider mb-2">{snapshot.title}</p>
199201
<div className="flex flex-wrap gap-1">
200-
{snapshot.items.map((item) => (
202+
{snapshot.items.map((item, i) => (
201203
<span
202204
key={item}
203-
className="relative inline-block border-2 border-[#3b3b3b] bg-[#6b6b6b] px-2 py-1 text-xs text-[#5c7a29] font-bold"
205+
className="relative inline-block border-2 border-[#3b3b3b] bg-[#6b6b6b] px-2 py-1 text-xs text-[#5c7a29] font-bold animate-mc-place hover:animate-mc-item-bob"
206+
style={{ animationDelay: `${0.5 + i * 0.06}s` }}
204207
>
205208
<span className="pointer-events-none absolute inset-0 border-t border-l border-[#9b9b9b]" />
206209
<span className="pointer-events-none absolute inset-0 border-b border-r border-[#4b4b4b]" />

src/templates/minecraft/PhilosophySection.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
type PhilosophyCard = { title: string; body: string }
22
type Philosophy = { title: string; body: string; cards: PhilosophyCard[] }
33

4+
import { useInView } from './useInView'
5+
46
export default function PhilosophySection({ philosophy }: { philosophy: Philosophy }) {
7+
const { ref, visible } = useInView()
58
if (!philosophy?.cards?.length && !philosophy?.body) return null
69

710
return (
811
<section id="philosophy" className="bg-[#1a1a2e] py-12" aria-labelledby="philosophy-title">
9-
<div className="mx-auto max-w-4xl px-6">
12+
<div ref={ref} className="mx-auto max-w-4xl px-6">
1013
{/* Section header — enchantment table style */}
1114
<div className="mb-6">
1215
<p className="text-xs uppercase tracking-widest text-[#5c7a29] mb-1">📖 Enchantment Table</p>
@@ -20,10 +23,11 @@ export default function PhilosophySection({ philosophy }: { philosophy: Philosop
2023

2124
{/* Cards as book pages */}
2225
<div className="grid gap-3 sm:grid-cols-2">
23-
{philosophy.cards.map((card) => (
26+
{philosophy.cards.map((card, i) => (
2427
<div
2528
key={card.title}
26-
className="relative border-2 border-[#3b3b3b] bg-[#8b8b8b] p-4 transition-all hover:border-[#ffffff80] hover:-translate-y-0.5"
29+
className={`relative border-2 border-[#3b3b3b] bg-[#8b8b8b] p-4 transition-all hover:border-[#ffffff80] hover:-translate-y-0.5 ${visible ? 'animate-mc-place' : 'opacity-0'}`}
30+
style={{ animationDelay: `${i * 0.1}s` }}
2731
>
2832
<div className="pointer-events-none absolute inset-0 border-t-2 border-l-2 border-[#c6c6c6]" />
2933
<div className="pointer-events-none absolute inset-0 border-b-2 border-r-2 border-[#555555]" />

src/templates/minecraft/RepoCard.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ export default function RepoCard({ repo }: { repo: ProjectRepo }) {
2424
{/* MC-style bevel edges */}
2525
<div className="pointer-events-none absolute inset-0 border-t-2 border-l-2 border-[#c6c6c6]" />
2626
<div className="pointer-events-none absolute inset-0 border-b-2 border-r-2 border-[#555555]" />
27+
{/* Enchantment glint on featured repos */}
28+
{repo.featured && (
29+
<div
30+
className="pointer-events-none absolute inset-0 animate-mc-enchant opacity-30"
31+
style={{
32+
backgroundImage: 'linear-gradient(120deg, transparent 30%, rgba(138,205,50,0.35) 50%, transparent 70%)',
33+
backgroundSize: '200% 100%',
34+
}}
35+
/>
36+
)}
2737

2838
<div className="relative">
2939
<div className="flex items-center justify-between gap-4">

src/templates/minecraft/StatsSection.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ type ActivityData = { year: number; repos: number }
33
type CommitActivityData = { year: number; commits: number }
44
type RepoStarsData = { name: string; stars: number; language: string }
55

6+
import { useInView } from './useInView'
7+
68
type Stats = {
79
metrics: {
810
totalRepos: number
@@ -34,14 +36,14 @@ const ORE_COLORS = [
3436

3537
const BAR_WIDTH = 120
3638

37-
function PixelBar({ pct, colorClass }: { pct: number; colorClass: string }) {
39+
function PixelBar({ pct, colorClass, animate, delay }: { pct: number; colorClass: string; animate?: boolean; delay?: number }) {
3840
const filled = Math.max(2, Math.round((pct / 100) * BAR_WIDTH))
3941
return (
4042
<div className="h-3 relative" style={{ width: BAR_WIDTH }}>
4143
<div className="absolute inset-0 bg-[#3b3b3b] border border-[#2b2b2b]" />
4244
<div
43-
className={`absolute top-0 left-0 h-full ${colorClass} border-r border-[#2b2b2b]`}
44-
style={{ width: filled, imageRendering: 'pixelated' }}
45+
className={`absolute top-0 left-0 h-full ${colorClass} border-r border-[#2b2b2b] ${animate ? 'animate-mc-bar-fill' : ''}`}
46+
style={{ width: filled, imageRendering: 'pixelated', animationDelay: delay ? `${delay}s` : undefined }}
4547
/>
4648
</div>
4749
)
@@ -61,6 +63,7 @@ function MetricSlot({ label, value }: { label: string; value: number }) {
6163
}
6264

6365
export default function StatsSection({ stats }: { stats: Stats | null }) {
66+
const { ref, visible } = useInView()
6467
if (!stats) return null
6568

6669
const { metrics, languageDistribution, activityByYear, commitActivityByYear, topReposByStars } = stats
@@ -73,7 +76,7 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
7376

7477
return (
7578
<section id="stats" className="bg-[#1a1a2e] py-12" aria-labelledby="stats-title">
76-
<div className="mx-auto max-w-4xl px-6 space-y-8">
79+
<div ref={ref} className="mx-auto max-w-4xl px-6 space-y-8">
7780
{/* Header */}
7881
<div>
7982
<p className="text-xs uppercase tracking-widest text-[#5c7a29] mb-1">📊 Statistics</p>
@@ -84,10 +87,16 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
8487

8588
{/* Metrics — inventory hotbar style */}
8689
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
87-
<MetricSlot label="Repos" value={metrics.totalRepos} />
88-
<MetricSlot label="Stars" value={metrics.totalStars} />
89-
<MetricSlot label="Forks" value={metrics.totalForks} />
90-
<MetricSlot label="Followers" value={metrics.followers} />
90+
{[
91+
{ label: 'Repos', value: metrics.totalRepos },
92+
{ label: 'Stars', value: metrics.totalStars },
93+
{ label: 'Forks', value: metrics.totalForks },
94+
{ label: 'Followers', value: metrics.followers },
95+
].map((m, i) => (
96+
<div key={m.label} className={visible ? 'animate-mc-place' : 'opacity-0'} style={{ animationDelay: `${i * 0.1}s` }}>
97+
<MetricSlot label={m.label} value={m.value} />
98+
</div>
99+
))}
91100
</div>
92101

93102
{/* Charts in 2-column grid */}
@@ -103,7 +112,7 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
103112
{languageDistribution.slice(0, 8).map((lang, i) => (
104113
<div key={lang.language} className="flex items-center gap-2 text-xs">
105114
<span className="w-20 shrink-0 truncate text-[#d4d4d4] font-bold">{lang.language}</span>
106-
<PixelBar pct={lang.percentage} colorClass={ORE_COLORS[i % ORE_COLORS.length]} />
115+
<PixelBar pct={lang.percentage} colorClass={ORE_COLORS[i % ORE_COLORS.length]} animate={visible} delay={i * 0.1} />
107116
<span className="w-8 shrink-0 text-right text-[#a0a0a0]">{lang.percentage}%</span>
108117
</div>
109118
))}
@@ -123,7 +132,7 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
123132
{activityByYear.map((row) => (
124133
<div key={row.year} className="flex items-center gap-2 text-xs">
125134
<span className="w-10 shrink-0 text-[#d4d4d4] font-bold">{row.year}</span>
126-
<PixelBar pct={(row.repos / maxRepos) * 100} colorClass="bg-[#5CC0D0]" />
135+
<PixelBar pct={(row.repos / maxRepos) * 100} colorClass="bg-[#5CC0D0]" animate={visible} delay={0.2} />
127136
<span className="w-6 shrink-0 text-right text-[#a0a0a0]">{row.repos}</span>
128137
</div>
129138
))}
@@ -143,7 +152,7 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
143152
{commitActivityByYear.map((row) => (
144153
<div key={row.year} className="flex items-center gap-2 text-xs">
145154
<span className="w-10 shrink-0 text-[#d4d4d4] font-bold">{row.year}</span>
146-
<PixelBar pct={(row.commits / maxCommits) * 100} colorClass="bg-[#5c7a29]" />
155+
<PixelBar pct={(row.commits / maxCommits) * 100} colorClass="bg-[#5c7a29]" animate={visible} delay={0.2} />
147156
<span className="w-10 shrink-0 text-right text-[#a0a0a0]">{row.commits}</span>
148157
</div>
149158
))}
@@ -163,7 +172,7 @@ export default function StatsSection({ stats }: { stats: Stats | null }) {
163172
{topReposByStars.map((repo) => (
164173
<div key={repo.name} className="flex items-center gap-2 text-xs">
165174
<span className="w-24 shrink-0 truncate text-[#d4d4d4] font-bold">{repo.name}</span>
166-
<PixelBar pct={(repo.stars / maxStars) * 100} colorClass="bg-[#FFAA00]" />
175+
<PixelBar pct={(repo.stars / maxStars) * 100} colorClass="bg-[#FFAA00]" animate={visible} delay={0.2} />
167176
<span className="shrink-0 text-[#ffaa00]">{repo.stars}</span>
168177
</div>
169178
))}

0 commit comments

Comments
 (0)