style: apply Prettier formatting across codebase

Isolated reformat commit so subsequent logic changes have clean diffs.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TehRiehlDealandClaude Opus 4.8 committed 2026-06-03 09:29:52 -07:00
1 parent a85bd16218
commit aa58425f1a
4 files changed
+481 -400

No files matched your search

+5 -2
View File
@@ -1,10 +1,13 @@
*, *::before, *::after {
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
html,
body {
width: 100%;
height: 100%;
margin: 0;
+326 -294
View File
@@ -31,13 +31,9 @@
// Stats panel state
let statsPanelVisible = $state(false);
let expandedBranches = $state(new Set<string>([
'concepts',
'tools-devops',
'frontend',
'backend-real-time',
'languages'
])); // All expanded by default
let expandedBranches = $state(
new Set<string>(['concepts', 'tools-devops', 'frontend', 'backend-real-time', 'languages'])
); // All expanded by default
function openModal(node: TreeNode) {
selectedNode = node;
@@ -48,17 +44,17 @@
}
function closeModal() {
// Prevent closing if modal just opened (within 300ms)
// This fixes touch events "bleeding through" on mobile
if (Date.now() - modalOpenedAt < 300) {
return;
}
// Prevent closing if modal just opened (within 300ms)
// This fixes touch events "bleeding through" on mobile
if (Date.now() - modalOpenedAt < 300) {
return;
}
modalVisible = false;
setTimeout(() => {
selectedNode = null;
}, 200);
}
modalVisible = false;
setTimeout(() => {
selectedNode = null;
}, 200);
}
function getCategoryColorHex(category: TreeNode['category']): string {
const colorMap: Record<TreeNode['category'], string> = {
@@ -66,7 +62,7 @@
experience: '#4ecdc4',
projects: '#ff6b6b',
skills: '#f9a825',
education: '#dda0dd',
education: '#dda0dd'
};
return colorMap[category];
}
@@ -175,7 +171,7 @@
await app.init({
background: '#05050a',
resizeTo: container,
antialias: true,
antialias: true
});
container.appendChild(app.canvas);
@@ -215,23 +211,26 @@
const b = Math.floor(20 * (1 - ratio));
const color = (r << 16) + (g << 8) + b;
spaceGradient
.circle(centerX, centerY, radius)
.fill({ color: color, alpha: 0.8 });
spaceGradient.circle(centerX, centerY, radius).fill({ color: color, alpha: 0.8 });
}
deepBackgroundContainer.addChild(spaceGradient);
// ========== NEBULA CLOUDS ==========
const nebulaColors = [
{ color: 0x4ecdc4, name: 'teal' }, // Experience
{ color: 0xff6b6b, name: 'coral' }, // Projects
{ color: 0xf9a825, name: 'amber' }, // Skills
{ color: 0xdda0dd, name: 'plum' }, // Education
{ color: 0x6b5b95, name: 'purple' }, // Extra accent
{ color: 0x4ecdc4, name: 'teal' }, // Experience
{ color: 0xff6b6b, name: 'coral' }, // Projects
{ color: 0xf9a825, name: 'amber' }, // Skills
{ color: 0xdda0dd, name: 'plum' }, // Education
{ color: 0x6b5b95, name: 'purple' } // Extra accent
];
// Create large nebula cloud regions
const nebulaRegions: { graphics: Graphics; baseX: number; baseY: number; pulseSpeed: number }[] = [];
const nebulaRegions: {
graphics: Graphics;
baseX: number;
baseY: number;
pulseSpeed: number;
}[] = [];
nebulaColors.forEach((nebula, index) => {
// Position nebulas in different quadrants, offset from center
@@ -250,9 +249,7 @@
const size = outerSize * (0.4 + layerRatio * 0.6);
const alpha = 0.02 * (1 - layerRatio);
nebulaCloud
.circle(0, 0, size)
.fill({ color: nebula.color, alpha: alpha });
nebulaCloud.circle(0, 0, size).fill({ color: nebula.color, alpha: alpha });
}
// Add some irregular shapes for more organic look
@@ -266,9 +263,7 @@
const size = blobSize * (0.5 + layerRatio * 0.5);
const alpha = 0.03 * (1 - layerRatio);
nebulaCloud
.circle(offsetX, offsetY, size)
.fill({ color: nebula.color, alpha: alpha });
nebulaCloud.circle(offsetX, offsetY, size).fill({ color: nebula.color, alpha: alpha });
}
}
@@ -280,7 +275,7 @@
graphics: nebulaCloud,
baseX: baseX,
baseY: baseY,
pulseSpeed: 0.5 + Math.random() * 0.5,
pulseSpeed: 0.5 + Math.random() * 0.5
});
// Animate nebula pulsing
@@ -290,7 +285,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 2,
delay: Math.random() * 2
});
gsap.to(nebulaCloud.scale, {
@@ -300,7 +295,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 2,
delay: Math.random() * 2
});
});
@@ -312,9 +307,7 @@
const size = Math.random() * 1 + 0.5;
const alpha = Math.random() * 0.4 + 0.1;
star
.circle(0, 0, size)
.fill({ color: 0xffffff, alpha: alpha });
star.circle(0, 0, size).fill({ color: 0xffffff, alpha: alpha });
star.x = Math.random() * screenWidth;
star.y = Math.random() * screenHeight;
@@ -327,7 +320,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 3,
delay: Math.random() * 3
});
}
@@ -342,12 +335,8 @@
const tint = starTints[Math.floor(Math.random() * starTints.length)];
// Star with subtle glow
star
.circle(0, 0, size + 2)
.fill({ color: tint, alpha: alpha * 0.2 });
star
.circle(0, 0, size)
.fill({ color: tint, alpha: alpha });
star.circle(0, 0, size + 2).fill({ color: tint, alpha: alpha * 0.2 });
star.circle(0, 0, size).fill({ color: tint, alpha: alpha });
star.x = Math.random() * screenWidth;
star.y = Math.random() * screenHeight;
@@ -360,7 +349,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 2,
delay: Math.random() * 2
});
}
@@ -372,15 +361,9 @@
const tint = starTints[Math.floor(Math.random() * starTints.length)];
// Multi-layer glow for bright stars
star
.circle(0, 0, size + 6)
.fill({ color: tint, alpha: 0.05 });
star
.circle(0, 0, size + 3)
.fill({ color: tint, alpha: 0.1 });
star
.circle(0, 0, size)
.fill({ color: 0xffffff, alpha: 0.9 });
star.circle(0, 0, size + 6).fill({ color: tint, alpha: 0.05 });
star.circle(0, 0, size + 3).fill({ color: tint, alpha: 0.1 });
star.circle(0, 0, size).fill({ color: 0xffffff, alpha: 0.9 });
star.x = Math.random() * screenWidth;
star.y = Math.random() * screenHeight;
@@ -394,7 +377,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 2,
delay: Math.random() * 2
});
}
@@ -409,15 +392,9 @@
const color = nebulaColors[colorIndex].color;
// Soft glowing particle
dust
.circle(0, 0, size + 4)
.fill({ color: color, alpha: 0.05 });
dust
.circle(0, 0, size + 2)
.fill({ color: color, alpha: 0.08 });
dust
.circle(0, 0, size)
.fill({ color: color, alpha: 0.15 });
dust.circle(0, 0, size + 4).fill({ color: color, alpha: 0.05 });
dust.circle(0, 0, size + 2).fill({ color: color, alpha: 0.08 });
dust.circle(0, 0, size).fill({ color: color, alpha: 0.15 });
dust.x = Math.random() * screenWidth;
dust.y = Math.random() * screenHeight;
@@ -426,7 +403,7 @@
dustParticles.push({
graphics: dust,
vx: (Math.random() - 0.5) * 0.2,
vy: (Math.random() - 0.5) * 0.2,
vy: (Math.random() - 0.5) * 0.2
});
// Fade in and out
@@ -436,7 +413,7 @@
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
delay: Math.random() * 2,
delay: Math.random() * 2
});
}
@@ -463,9 +440,7 @@
const radius = vignetteSize * (0.4 + ratio * 0.6);
const alpha = ratio * ratio * 0.4;
vignette
.circle(centerX, centerY, radius)
.fill({ color: 0x000000, alpha: alpha });
vignette.circle(centerX, centerY, radius).fill({ color: 0x000000, alpha: alpha });
}
app.stage.addChild(vignette);
@@ -540,77 +515,89 @@
});
// ========== TOUCH EVENTS FOR PANNING & ZOOMING ==========
app.canvas.addEventListener('touchstart', (e: TouchEvent) => {
if (e.touches.length === 1) {
// Single touch - start panning
isDragging = true;
hasMoved = false;
isTouchZooming = false;
dragStartX = e.touches[0].clientX;
dragStartY = e.touches[0].clientY;
containerStartX = treeContainer.x;
containerStartY = treeContainer.y;
} else if (e.touches.length === 2) {
// Two touches - start pinch zoom
isDragging = false;
isTouchZooming = true;
hasMoved = true; // Pinch zoom counts as movement
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastTouchDistance = Math.sqrt(dx * dx + dy * dy);
}
}, { passive: true });
app.canvas.addEventListener(
'touchstart',
(e: TouchEvent) => {
if (e.touches.length === 1) {
// Single touch - start panning
isDragging = true;
hasMoved = false;
isTouchZooming = false;
dragStartX = e.touches[0].clientX;
dragStartY = e.touches[0].clientY;
containerStartX = treeContainer.x;
containerStartY = treeContainer.y;
} else if (e.touches.length === 2) {
// Two touches - start pinch zoom
isDragging = false;
isTouchZooming = true;
hasMoved = true; // Pinch zoom counts as movement
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
lastTouchDistance = Math.sqrt(dx * dx + dy * dy);
}
},
{ passive: true }
);
app.canvas.addEventListener('touchmove', (e: TouchEvent) => {
if (e.touches.length === 1 && isDragging && !isTouchZooming) {
// Single touch - panning
const dx = e.touches[0].clientX - dragStartX;
const dy = e.touches[0].clientY - dragStartY;
app.canvas.addEventListener(
'touchmove',
(e: TouchEvent) => {
if (e.touches.length === 1 && isDragging && !isTouchZooming) {
// Single touch - panning
const dx = e.touches[0].clientX - dragStartX;
const dy = e.touches[0].clientY - dragStartY;
// Only count as moved if we've moved more than a small threshold
if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
// Only count as moved if we've moved more than a small threshold
if (Math.abs(dx) > 10 || Math.abs(dy) > 10) {
hasMoved = true;
treeContainer.x = containerStartX + dx;
treeContainer.y = containerStartY + dy;
updateParallax();
}
} else if (e.touches.length === 2 && isTouchZooming) {
// Two touches - pinch zoom
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (lastTouchDistance > 0) {
const scale = distance / lastTouchDistance;
let newScale = treeContainer.scale.x * scale;
newScale = Math.max(minZoom, Math.min(maxZoom, newScale));
// Zoom centered on origin node (same as mouse wheel zoom)
treeContainer.scale.set(newScale);
updateParallax();
}
lastTouchDistance = distance;
hasMoved = true;
treeContainer.x = containerStartX + dx;
treeContainer.y = containerStartY + dy;
updateParallax();
}
} else if (e.touches.length === 2 && isTouchZooming) {
// Two touches - pinch zoom
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const distance = Math.sqrt(dx * dx + dy * dy);
},
{ passive: true }
);
if (lastTouchDistance > 0) {
const scale = distance / lastTouchDistance;
let newScale = treeContainer.scale.x * scale;
newScale = Math.max(minZoom, Math.min(maxZoom, newScale));
// Zoom centered on origin node (same as mouse wheel zoom)
treeContainer.scale.set(newScale);
updateParallax();
app.canvas.addEventListener(
'touchend',
(e: TouchEvent) => {
if (e.touches.length === 0) {
isDragging = false;
isTouchZooming = false;
lastTouchDistance = 0;
} else if (e.touches.length === 1) {
// Went from 2 touches to 1 - reset pan start
isTouchZooming = false;
isDragging = true;
hasMoved = true; // Consider this movement since we were zooming
dragStartX = e.touches[0].clientX;
dragStartY = e.touches[0].clientY;
containerStartX = treeContainer.x;
containerStartY = treeContainer.y;
}
lastTouchDistance = distance;
hasMoved = true;
}
}, { passive: true });
app.canvas.addEventListener('touchend', (e: TouchEvent) => {
if (e.touches.length === 0) {
isDragging = false;
isTouchZooming = false;
lastTouchDistance = 0;
} else if (e.touches.length === 1) {
// Went from 2 touches to 1 - reset pan start
isTouchZooming = false;
isDragging = true;
hasMoved = true; // Consider this movement since we were zooming
dragStartX = e.touches[0].clientX;
dragStartY = e.touches[0].clientY;
containerStartX = treeContainer.x;
containerStartY = treeContainer.y;
}
}, { passive: true });
},
{ passive: true }
);
// ========== SCROLL EVENT FOR ZOOMING ==========
app.canvas.addEventListener('wheel', (e: WheelEvent) => {
@@ -634,8 +621,8 @@
function getNodeRadius(size: TreeNode['size']): number {
const mobileFactor = isMobile ? 0.8 : 1;
switch (size) {
case 'x-large':
return 75 * mobileFactor;
case 'x-large':
return 75 * mobileFactor;
case 'large':
return 30 * mobileFactor;
case 'medium':
@@ -658,7 +645,7 @@
const treeDeltaY = treeContainer.y - initialTreeY;
// Parallax position factors (subtle intensity)
const starFactor = 0.10;
const starFactor = 0.1;
const nebulaFactor = 0.07;
const gradientFactor = 0.05;
@@ -747,7 +734,11 @@
}
// ========== DRAW NODES ==========
const nodeObjects: { graphics: Graphics | Container; node: TreeNode; glowGraphics: Graphics }[] = [];
const nodeObjects: {
graphics: Graphics | Container;
node: TreeNode;
glowGraphics: Graphics;
}[] = [];
const profileTexture = await Assets.load('/headshot.jpg');
@@ -756,12 +747,8 @@
const color = categoryColors[node.category];
const glowGraphics = new Graphics();
glowGraphics
.circle(0, 0, radius + 8)
.fill({ color: color, alpha: 0.1 });
glowGraphics
.circle(0, 0, radius + 4)
.fill({ color: color, alpha: 0.15 });
glowGraphics.circle(0, 0, radius + 8).fill({ color: color, alpha: 0.1 });
glowGraphics.circle(0, 0, radius + 4).fill({ color: color, alpha: 0.15 });
glowGraphics.x = node.x;
glowGraphics.y = node.y;
glowGraphics.alpha = 0;
@@ -773,9 +760,7 @@
profileContainer.y = node.y;
const maskGraphics = new Graphics();
maskGraphics
.circle(0, 0, radius)
.fill({ color: 0xffffff });
maskGraphics.circle(0, 0, radius).fill({ color: 0xffffff });
profileContainer.addChild(maskGraphics);
const profileSprite = new Sprite(profileTexture);
@@ -786,12 +771,8 @@
profileContainer.addChild(profileSprite);
const borderGraphics = new Graphics();
borderGraphics
.circle(0, 0, radius + 2)
.stroke({ width: 3, color: color });
borderGraphics
.circle(0, 0, radius + 6)
.stroke({ width: 2, color: color, alpha: 0.5 });
borderGraphics.circle(0, 0, radius + 2).stroke({ width: 3, color: color });
borderGraphics.circle(0, 0, radius + 6).stroke({ width: 2, color: color, alpha: 0.5 });
profileContainer.addChild(borderGraphics);
profileContainer.alpha = 0;
@@ -825,7 +806,6 @@
treeContainer.addChild(profileContainer);
nodeObjects.push({ graphics: profileContainer, node, glowGraphics });
} else {
const nodeContainer = new Container();
nodeContainer.x = node.x;
@@ -838,9 +818,7 @@
.fill({ color: 0x12121a })
.stroke({ width: 2, color: color });
nodeGraphics
.circle(0, 0, radius * 0.75)
.stroke({ width: 1, color: color, alpha: 0.2 });
nodeGraphics.circle(0, 0, radius * 0.75).stroke({ width: 1, color: color, alpha: 0.2 });
nodeContainer.addChild(nodeGraphics);
@@ -933,7 +911,7 @@
// STEP 1: After title fades, show origin node first
// On mobile, start origin animation BEFORE title fully fades (overlap them)
const originNode = sortedNodes.find(obj => obj.node.id === 'origin');
const originNode = sortedNodes.find((obj) => obj.node.id === 'origin');
const originDelay = isMobile ? titleDuration - 0.5 : titleDuration;
const originDuration = isMobile ? 0.3 : 0.6;
@@ -947,7 +925,7 @@
alpha: 1,
duration: originDuration,
delay: originDelay,
ease: 'power2.out',
ease: 'power2.out'
});
gsap.to(originNode.graphics.scale, {
x: 1,
@@ -960,13 +938,13 @@
if (originNode.graphics instanceof Container) {
originNode.graphics.eventMode = 'static';
}
},
}
});
gsap.to(originNode.glowGraphics, {
alpha: 1,
duration: originDuration,
delay: originDelay,
ease: 'power2.out',
ease: 'power2.out'
});
}
@@ -983,17 +961,17 @@
});
// STEP 3: Animate in the rest of the nodes (excluding origin)
const otherNodes = sortedNodes.filter(obj => obj.node.id !== 'origin');
const otherNodes = sortedNodes.filter((obj) => obj.node.id !== 'origin');
const nodeStartDelay = isMobile ? titleDuration - 0.2 : titleDuration + 0.5;
otherNodes.forEach((obj, index) => {
const delay = nodeStartDelay + (index * nodeStagger);
const delay = nodeStartDelay + index * nodeStagger;
gsap.to(obj.graphics, {
alpha: 1,
duration: 0.5,
delay: delay,
ease: 'power2.out',
ease: 'power2.out'
});
gsap.to(obj.graphics.scale, {
x: 1,
@@ -1006,13 +984,13 @@
if (obj.graphics instanceof Container) {
obj.graphics.eventMode = 'static';
}
},
}
});
gsap.to(obj.glowGraphics, {
alpha: 1,
duration: 0.5,
delay: delay,
ease: 'power2.out',
ease: 'power2.out'
});
// Start ambient pulse after node appears
@@ -1023,7 +1001,7 @@
delay: delay + 0.5,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
repeat: -1
});
});
@@ -1036,7 +1014,7 @@
delay: titleDuration + 1,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
repeat: -1
});
}
@@ -1044,14 +1022,14 @@
const lineStartDelay = isMobile ? titleDuration - 0.3 : titleDuration + 0.4;
sortedLines.forEach((line, index) => {
const delay = lineStartDelay + (index * nodeStagger);
const delay = lineStartDelay + index * nodeStagger;
// Core line appears first
gsap.to(line.graphics, {
alpha: 1,
duration: 0.4,
delay: delay,
ease: 'power2.out',
ease: 'power2.out'
});
// Base glow appears with line
@@ -1059,7 +1037,7 @@
alpha: 1,
duration: 0.6,
delay: delay + 0.1,
ease: 'power2.out',
ease: 'power2.out'
});
// Plasma beam fades in
@@ -1080,7 +1058,7 @@
duration: pulseDuration,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
repeat: -1
});
// Glow layer also pulses
@@ -1091,16 +1069,19 @@
duration: pulseDuration * 1.2,
ease: 'sine.inOut',
yoyo: true,
repeat: -1,
repeat: -1
});
},
}
});
});
// Clean up title card after animation (matches titleDuration)
setTimeout(() => {
showWelcomeTitle = false;
}, isMobile ? 1500 : 2500);
setTimeout(
() => {
showWelcomeTitle = false;
},
isMobile ? 1500 : 2500
);
// Fade in stats panel toggle button with the tree
const toggleButton = document.querySelector('.stats-panel-toggle');
@@ -1108,8 +1089,8 @@
gsap.to(toggleButton, {
opacity: 1,
duration: 0.5,
delay: nodeStartDelay + (otherNodes.length * nodeStagger * 0.5), // Fade in midway through node animation
ease: 'power2.out',
delay: nodeStartDelay + otherNodes.length * nodeStagger * 0.5, // Fade in midway through node animation
ease: 'power2.out'
});
}
@@ -1211,7 +1192,7 @@
// Cleanup on component unmount
$effect(() => {
return () => {
cleanupFunctions.forEach(fn => fn());
cleanupFunctions.forEach((fn) => fn());
};
});
@@ -1315,124 +1296,163 @@
<span class="category-badge">{selectedNode.category}</span>
<div id="modal-description">
<!-- Origin-specific content -->
{#if selectedNode.category === 'origin'}
{#if selectedNode.jobTitle}
<div class="origin-title">{selectedNode.jobTitle}</div>
{/if}
{#if selectedNode.location}
<div class="origin-location">📍 {selectedNode.location}</div>
{/if}
{#if selectedNode.aboutMe}
<p class="about-me">{selectedNode.aboutMe}</p>
{/if}
{#if selectedNode.email || selectedNode.linkedIn || selectedNode.github || selectedNode.website}
<div class="social-links">
{#if selectedNode.email}
<a href="mailto:{selectedNode.email}" title="Email" aria-label="Send email"
>✉️ Email</a
>
{/if}
{#if selectedNode.linkedIn}
<a
href={selectedNode.linkedIn}
target="_blank"
rel="noopener noreferrer"
title="LinkedIn"
aria-label="LinkedIn profile (opens in new tab)">💼 LinkedIn</a
>
{/if}
{#if selectedNode.github}
<a
href={selectedNode.github}
target="_blank"
rel="noopener noreferrer"
title="GitHub"
aria-label="GitHub profile (opens in new tab)">💻 GitHub</a
>
{/if}
{#if selectedNode.website}
<a
href={selectedNode.website}
target="_blank"
rel="noopener noreferrer"
title="Website"
aria-label="Personal website (opens in new tab)">🌐 Website</a
>
{/if}
</div>
{#if selectedNode.resumeUrl}
<a href={selectedNode.resumeUrl} download class="resume-button">
📄 Download Resume
</a>
{/if}
{/if}
{/if}
<!-- Origin-specific content -->
{#if selectedNode.category === 'origin'}
{#if selectedNode.jobTitle}
<div class="origin-title">{selectedNode.jobTitle}</div>
{/if}
{#if selectedNode.location}
<div class="origin-location">📍 {selectedNode.location}</div>
{/if}
{#if selectedNode.aboutMe}
<p class="about-me">{selectedNode.aboutMe}</p>
{/if}
{#if selectedNode.email || selectedNode.linkedIn || selectedNode.github || selectedNode.website}
<div class="social-links">
{#if selectedNode.email}
<a href="mailto:{selectedNode.email}" title="Email" aria-label="Send email">✉️ Email</a>
{/if}
{#if selectedNode.linkedIn}
<a href={selectedNode.linkedIn} target="_blank" rel="noopener noreferrer" title="LinkedIn" aria-label="LinkedIn profile (opens in new tab)">💼 LinkedIn</a>
{/if}
{#if selectedNode.github}
<a href={selectedNode.github} target="_blank" rel="noopener noreferrer" title="GitHub" aria-label="GitHub profile (opens in new tab)">💻 GitHub</a>
{/if}
{#if selectedNode.website}
<a href={selectedNode.website} target="_blank" rel="noopener noreferrer" title="Website" aria-label="Personal website (opens in new tab)">🌐 Website</a>
<!-- Experience-specific content -->
{#if selectedNode.company}
<div class="meta-info">
<span class="company">{selectedNode.company}</span>
{#if selectedNode.startDate}
<span class="dates"
>{selectedNode.startDate} — {selectedNode.endDate || 'Present'}</span
>
{/if}
</div>
{#if selectedNode.resumeUrl}
<a
href={selectedNode.resumeUrl}
download
class="resume-button"
>
📄 Download Resume
</a>
{/if}
<!-- Education-specific content -->
{#if selectedNode.institution}
<div class="meta-info">
<span class="company">{selectedNode.institution}</span>
{#if selectedNode.degree}
<span class="dates">{selectedNode.degree} in {selectedNode.field || 'N/A'}</span>
{/if}
{#if selectedNode.graduationYear}
<span class="dates">Graduated {selectedNode.graduationYear}</span>
{/if}
</div>
{/if}
{/if}
<!-- Experience-specific content -->
{#if selectedNode.company}
<div class="meta-info">
<span class="company">{selectedNode.company}</span>
{#if selectedNode.startDate}
<span class="dates">{selectedNode.startDate} — {selectedNode.endDate || 'Present'}</span>
{/if}
</div>
{/if}
<!-- Skill-specific content -->
{#if selectedNode.proficiency}
<div class="meta-info">
<span class="proficiency-badge {selectedNode.proficiency}"
>{selectedNode.proficiency}</span
>
{#if selectedNode.yearsOfExperience}
<span class="dates">{selectedNode.yearsOfExperience} years experience</span>
{/if}
</div>
{/if}
<!-- Education-specific content -->
{#if selectedNode.institution}
<div class="meta-info">
<span class="company">{selectedNode.institution}</span>
{#if selectedNode.degree}
<span class="dates">{selectedNode.degree} in {selectedNode.field || 'N/A'}</span>
{/if}
{#if selectedNode.graduationYear}
<span class="dates">Graduated {selectedNode.graduationYear}</span>
{/if}
</div>
{/if}
{#if selectedNode.category !== 'origin'}
<p>{selectedNode.description}</p>
{/if}
<!-- Skill-specific content -->
{#if selectedNode.proficiency}
<div class="meta-info">
<span class="proficiency-badge {selectedNode.proficiency}">{selectedNode.proficiency}</span>
{#if selectedNode.yearsOfExperience}
<span class="dates">{selectedNode.yearsOfExperience} years experience</span>
{/if}
</div>
{/if}
<!-- Highlights (for experience) -->
{#if selectedNode.highlights && selectedNode.highlights.length > 0}
<ul class="highlights">
{#each selectedNode.highlights as highlight}
<li>{highlight}</li>
{/each}
</ul>
{/if}
{#if selectedNode.category !== 'origin'}
<p>{selectedNode.description}</p>
{/if}
<!-- Tech stack (for projects) -->
{#if selectedNode.techStack && selectedNode.techStack.length > 0}
<div class="tech-stack">
{#each selectedNode.techStack as tech}
<span class="tech-tag">{tech}</span>
{/each}
</div>
{/if}
<!-- Highlights (for experience) -->
{#if selectedNode.highlights && selectedNode.highlights.length > 0}
<ul class="highlights">
{#each selectedNode.highlights as highlight}
<li>{highlight}</li>
{/each}
</ul>
{/if}
<!-- Project status badge -->
{#if selectedNode.status}
<div class="project-status">
<span class="status-badge {selectedNode.status}">{selectedNode.status}</span>
</div>
{/if}
<!-- Tech stack (for projects) -->
{#if selectedNode.techStack && selectedNode.techStack.length > 0}
<div class="tech-stack">
{#each selectedNode.techStack as tech}
<span class="tech-tag">{tech}</span>
{/each}
</div>
{/if}
<!-- Project status badge -->
{#if selectedNode.status}
<div class="project-status">
<span class="status-badge {selectedNode.status}">{selectedNode.status}</span>
</div>
{/if}
<!-- Links (for projects) -->
{#if selectedNode.projectUrl || selectedNode.repoUrl || selectedNode.pypiUrl || selectedNode.npmUrl}
<div class="links">
{#if selectedNode.projectUrl}
<a href={selectedNode.projectUrl} target="_blank" rel="noopener noreferrer" aria-label="Live site (opens in new tab)">🔗 Live Site</a>
{/if}
{#if selectedNode.repoUrl}
<a href={selectedNode.repoUrl} target="_blank" rel="noopener noreferrer" aria-label="Code repository (opens in new tab)">💻 Repository</a>
{/if}
{#if selectedNode.pypiUrl}
<a href={selectedNode.pypiUrl} target="_blank" rel="noopener noreferrer" aria-label="Python package on PyPI (opens in new tab)">📦 PyPI</a>
{/if}
{#if selectedNode.npmUrl}
<a href={selectedNode.npmUrl} target="_blank" rel="noopener noreferrer" aria-label="npm package (opens in new tab)">📦 npm</a>
{/if}
</div>
{/if}
<!-- Links (for projects) -->
{#if selectedNode.projectUrl || selectedNode.repoUrl || selectedNode.pypiUrl || selectedNode.npmUrl}
<div class="links">
{#if selectedNode.projectUrl}
<a
href={selectedNode.projectUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Live site (opens in new tab)">🔗 Live Site</a
>
{/if}
{#if selectedNode.repoUrl}
<a
href={selectedNode.repoUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Code repository (opens in new tab)">💻 Repository</a
>
{/if}
{#if selectedNode.pypiUrl}
<a
href={selectedNode.pypiUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="Python package on PyPI (opens in new tab)">📦 PyPI</a
>
{/if}
{#if selectedNode.npmUrl}
<a
href={selectedNode.npmUrl}
target="_blank"
rel="noopener noreferrer"
aria-label="npm package (opens in new tab)">📦 npm</a
>
{/if}
</div>
{/if}
</div>
</div>
</div>
@@ -1706,7 +1726,11 @@
.category-badge {
display: inline-block;
background: linear-gradient(135deg, var(--node-color), color-mix(in srgb, var(--node-color) 70%, black));
background: linear-gradient(
135deg,
var(--node-color),
color-mix(in srgb, var(--node-color) 70%, black)
);
color: #0a0a0f;
padding: 0.25rem 0.75rem;
border-radius: 4px;
@@ -1928,7 +1952,11 @@
width: 100%;
margin-top: 1rem;
padding: 0.75rem 1rem;
background: linear-gradient(135deg, var(--node-color), color-mix(in srgb, var(--node-color) 70%, black));
background: linear-gradient(
135deg,
var(--node-color),
color-mix(in srgb, var(--node-color) 70%, black)
);
color: #0a0a0f;
text-align: center;
text-decoration: none;
@@ -2012,8 +2040,12 @@
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.welcome-title {
+134 -100
View File
@@ -4,17 +4,17 @@ export interface TreeNode {
description: string;
x: number;
y: number;
parentId: string | null; // null means it's the root or connects to center
parentId: string | null; // null means it's the root or connects to center
category: 'origin' | 'experience' | 'projects' | 'skills' | 'education';
size: 'x-large' | 'large' | 'medium' | 'small';
icon?: string;
icon?: string;
// ========== EXPERIENCE-SPECIFIC FIELDS ==========
// ========== EXPERIENCE-SPECIFIC FIELDS ==========
company?: string;
jobTitle?: string;
startDate?: string;
endDate?: string; // Use "Present" for current role
highlights?: string[]; // Key achievements or responsibilities
endDate?: string; // Use "Present" for current role
highlights?: string[]; // Key achievements or responsibilities
// ========== CONTRACT-SPECIFIC FIELDS ==========
contractName?: string;
@@ -26,10 +26,10 @@ export interface TreeNode {
techStack?: string[];
projectUrl?: string;
repoUrl?: string;
npmUrl?: string; // For npm packages
pypiUrl?: string; // For PyPI packages
downloads?: string; // "1,000+ downloads"
stars?: number; // GitHub stars
npmUrl?: string; // For npm packages
pypiUrl?: string; // For PyPI packages
downloads?: string; // "1,000+ downloads"
stars?: number; // GitHub stars
status?: 'active' | 'maintained' | 'archived' | 'completed';
// ========== SKILL-SPECIFIC FIELDS ==========
@@ -42,7 +42,7 @@ export interface TreeNode {
field?: string;
graduationYear?: string;
// ========== ORIGIN-SPECIFIC FIELDS ==========
// ========== ORIGIN-SPECIFIC FIELDS ==========
fullName?: string;
aboutMe?: string;
location?: string;
@@ -62,7 +62,8 @@ export const treeNodes: TreeNode[] = [
{
id: 'origin',
label: 'Kevin Riehl',
description: 'Welcome to my skill tree! Click on nodes to explore my experience, projects, and skills.',
description:
'Welcome to my skill tree! Click on nodes to explore my experience, projects, and skills.',
x: 0,
y: 0,
parentId: null,
@@ -71,13 +72,14 @@ export const treeNodes: TreeNode[] = [
icon: '',
fullName: 'Kevin Riehl',
jobTitle: 'Software Development - Lead Associate',
aboutMe: 'Intermediate Software Engineer with 5+ years of experience architecting full-stack applications using React, Node.js (Hapi), and SQL. Expertise includes modernizing legacy codebases, implementing rigorous testing standards, and leading frontend architecture improvements. Passionate about leveraging modern tools like ShadCN and Docker to streamline development workflows.',
aboutMe:
'Intermediate Software Engineer with 5+ years of experience architecting full-stack applications using React, Node.js (Hapi), and SQL. Expertise includes modernizing legacy codebases, implementing rigorous testing standards, and leading frontend architecture improvements. Passionate about leveraging modern tools like ShadCN and Docker to streamline development workflows.',
location: 'Kent, WA',
email: 'kevinriehl@gmail.com',
linkedIn: 'https://www.linkedin.com/in/kevinriehl',
github: 'https://github.com/TehRiehlDeal',
website: 'https://tehriehldeal.com',
resumeUrl: '/kevin-riehl-resume.pdf',
resumeUrl: '/kevin-riehl-resume.pdf'
},
// ========== EXPERIENCE (Upper Right - Contract-Based) ==========
@@ -90,14 +92,15 @@ export const treeNodes: TreeNode[] = [
parentId: 'origin',
category: 'experience',
size: 'large',
icon: '/icons/experience.svg',
icon: '/icons/experience.svg'
},
// Contract Parent Node
{
id: 'exp-contract',
label: 'DHA - Web and Mobile Technology',
description: 'Continuous work supporting the same government client through multiple contracting companies as the contract was re-competed and awarded.',
description:
'Continuous work supporting the same government client through multiple contracting companies as the contract was re-competed and awarded.',
x: 320,
y: -220,
parentId: 'exp-branch',
@@ -107,7 +110,7 @@ export const treeNodes: TreeNode[] = [
contractName: 'DHA WMT',
client: 'Defense Health Agency',
contractStart: 'October 2020',
contractEnd: 'Present',
contractEnd: 'Present'
},
// Job 1 - First company (starting point of the arc)
@@ -127,8 +130,8 @@ export const treeNodes: TreeNode[] = [
endDate: 'April 2021',
highlights: [
'Refactored legacy PHP endpoints into a robust Hapi.js architecture, aligning the project with modern JavaScript standards and improving API response times',
'Executed a critical database migration from MySQL to MSSQL, ensuring data integrity during the transfer and optimizing schema definitions for the new environment',
],
'Executed a critical database migration from MySQL to MSSQL, ensuring data integrity during the transfer and optimizing schema definitions for the new environment'
]
},
// Job 2 - Arc continues upward
@@ -149,8 +152,8 @@ export const treeNodes: TreeNode[] = [
highlights: [
'Architected and built a comprehensive data analytics search engine using React and Hapi.js (TypeScript), enabling users to filter complex event datasets by location, type, and date for real-time analysis',
'Facilitated successful production releases by managing SQL database migration scripts and executing rigorous regression testing to ensure critical features remained stable during updates',
'Supported engineering team growth by conducting code reviews and leading the onboarding process for new developers, ensuring smooth knowledge transfer and adherence to coding standards',
],
'Supported engineering team growth by conducting code reviews and leading the onboarding process for new developers, ensuring smooth knowledge transfer and adherence to coding standards'
]
},
// Job 3 - Peak of the arc
@@ -171,8 +174,8 @@ export const treeNodes: TreeNode[] = [
highlights: [
"Championed the adoption of ShadCN by conducting a live technical demonstration on the team's starter template, proving the framework's ability to accelerate development velocity and ensure UI consistency across projects",
'Developed a Web Bluetooth interface for a biometric monitoring proof-of-concept, enabling real-time data ingestion and aggregation from multiple wearable sensors (accelerometers and smartwatches) for sleep analysis',
"Supported the engineering team's migration to containerized workflows by configuring, testing, and troubleshooting custom Docker environments, ensuring consistent application behavior across development and production",
],
"Supported the engineering team's migration to containerized workflows by configuring, testing, and troubleshooting custom Docker environments, ensuring consistent application behavior across development and production"
]
},
// Job 4 - Continuing upward and right
@@ -193,8 +196,8 @@ export const treeNodes: TreeNode[] = [
highlights: [
'Architected a secure telehealth video conferencing application using React and WebRTC, designing the core signaling class to manage complex peer-to-peer connections (SDP offers/answers) and media stream constraints',
'Implemented Google MediaPipe to develop a real-time gesture recognition feature, enabling accessibility for speech-impaired users by translating hand signs (e.g., thumbs up) into animated in-chat reactions',
'Engineered adaptive network logic that monitors packet loss in real-time and dynamically adjusts video stream quality, ensuring connection stability and optimal user experience during bandwidth fluctuations',
],
'Engineered adaptive network logic that monitors packet loss in real-time and dynamically adjusts video stream quality, ensuring connection stability and optimal user experience during bandwidth fluctuations'
]
},
// Job 5 - Current (highest point, prominent)
@@ -212,9 +215,7 @@ export const treeNodes: TreeNode[] = [
jobTitle: 'Lead Associate',
startDate: 'January 2026',
endDate: 'Present',
highlights: [
'The journey has just begun',
],
highlights: ['The journey has just begun']
},
// ========== PROJECTS (Upper Left Quadrant) ==========
@@ -227,7 +228,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'origin',
category: 'projects',
size: 'large',
icon: '/icons/projects.svg',
icon: '/icons/projects.svg'
},
// Open Source Libraries
@@ -240,7 +241,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'proj-branch',
category: 'projects',
size: 'medium',
icon: '/icons/opensource.svg',
icon: '/icons/opensource.svg'
},
// Personal Tools
@@ -253,7 +254,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'proj-branch',
category: 'projects',
size: 'medium',
icon: '/icons/tools.svg',
icon: '/icons/tools.svg'
},
// Games
@@ -266,14 +267,15 @@ export const treeNodes: TreeNode[] = [
parentId: 'proj-branch',
category: 'projects',
size: 'medium',
icon: '/icons/games.svg',
icon: '/icons/games.svg'
},
// ========== OPEN SOURCE CHILDREN ==========
{
id: 'proj-tvdbapi',
label: 'tvdbAPI',
description: 'Python library for accessing The TVDB API. Fetch show info, episodes, actors, and images with a simple interface. Published on PyPI.',
description:
'Python library for accessing The TVDB API. Fetch show info, episodes, actors, and images with a simple interface. Published on PyPI.',
x: -420,
y: -310,
parentId: 'proj-opensource',
@@ -288,7 +290,8 @@ export const treeNodes: TreeNode[] = [
{
id: 'proj-tmdbapi',
label: 'tmdbAPI',
description: 'Python library for The Movie Database API. Search for movies, TV shows, actors, and collections with comprehensive query options.',
description:
'Python library for The Movie Database API. Search for movies, TV shows, actors, and collections with comprehensive query options.',
x: -500,
y: -250,
parentId: 'proj-opensource',
@@ -303,7 +306,8 @@ export const treeNodes: TreeNode[] = [
{
id: 'proj-image-resize',
label: 'Image Resize Tool',
description: 'GUI-based batch image processor with intelligent whitespace trimming and automated resizing workflows. Features folder-based processing for brand-specific transformations and maintains aspect ratios.',
description:
'GUI-based batch image processor with intelligent whitespace trimming and automated resizing workflows. Features folder-based processing for brand-specific transformations and maintains aspect ratios.',
x: -460,
y: -180,
parentId: 'proj-opensource',
@@ -319,7 +323,8 @@ export const treeNodes: TreeNode[] = [
{
id: 'proj-renameshow',
label: 'Rename Show',
description: 'Desktop application for batch renaming TV show files using metadata from TVDB and TMDB. Features undo support, multi-episode detection, and automatic show searching.',
description:
'Desktop application for batch renaming TV show files using metadata from TVDB and TMDB. Features undo support, multi-episode detection, and automatic show searching.',
x: -475,
y: -135,
parentId: 'proj-tools',
@@ -327,12 +332,13 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/desktop.svg',
techStack: ['Python', 'Tkinter', 'tvdbAPI', 'tmdbAPI', 'PIL'],
repoUrl: 'https://github.com/TehRiehlDeal/python-file-rename',
repoUrl: 'https://github.com/TehRiehlDeal/python-file-rename'
},
{
id: 'proj-portfolio',
label: 'Skill Tree Portfolio',
description: 'This interactive portfolio you\'re exploring right now! A Path of Exile inspired skill tree built with modern web technologies.',
description:
"This interactive portfolio you're exploring right now! A Path of Exile inspired skill tree built with modern web technologies.",
x: -480,
y: -60,
parentId: 'proj-tools',
@@ -341,12 +347,13 @@ export const treeNodes: TreeNode[] = [
icon: '/icons/svelte.svg',
techStack: ['SvelteKit', 'TypeScript', 'PixiJS', 'GSAP', 'Tailwind CSS'],
projectUrl: 'https://tehriehldeal.com',
repoUrl: 'https://github.com/yourusername/skill-tree-portfolio',
repoUrl: 'https://github.com/yourusername/skill-tree-portfolio'
},
{
id: 'proj-character-builder',
label: 'Pathfinder 2E Character Builder',
description: 'Web-based character creation tool for Pathfinder 2E tabletop RPG. Features modern component-based architecture with comprehensive testing and code quality tooling.',
description:
'Web-based character creation tool for Pathfinder 2E tabletop RPG. Features modern component-based architecture with comprehensive testing and code quality tooling.',
x: -490,
y: 15,
parentId: 'proj-tools',
@@ -360,14 +367,26 @@ export const treeNodes: TreeNode[] = [
{
id: 'proj-budget',
label: 'TehRiehlBudget',
description: 'Self-hosted personal finance app tracking spending, balances, and net worth across checking, savings, credit, loans, and investments. Features field-level AES-256-GCM encryption, receipt uploads, audit logging, CSV export, and a local Ollama-powered AI advisor.',
description:
'Self-hosted personal finance app tracking spending, balances, and net worth across checking, savings, credit, loans, and investments. Features field-level AES-256-GCM encryption, receipt uploads, audit logging, CSV export, and a local Ollama-powered AI advisor.',
x: -440,
y: 75,
parentId: 'proj-tools',
category: 'projects',
size: 'small',
icon: '/icons/react.svg',
techStack: ['React', 'TypeScript', 'NestJS', 'Prisma', 'PostgreSQL', 'TailwindCSS', 'ShadCN', 'Zustand', 'Supabase', 'Ollama'],
techStack: [
'React',
'TypeScript',
'NestJS',
'Prisma',
'PostgreSQL',
'TailwindCSS',
'ShadCN',
'Zustand',
'Supabase',
'Ollama'
],
projectUrl: 'https://budget.tehriehldeal.com/',
repoUrl: 'https://git.tehriehldeal.com/TehRiehlDeal/TehRiehlBudget',
status: 'active'
@@ -377,7 +396,8 @@ export const treeNodes: TreeNode[] = [
{
id: 'proj-movieloop',
label: 'MovieLoop',
description: 'Daily challenge game where players chain movies through shared actors — a "six degrees of Kevin Bacon" reimagined for the web. Features leaderboards, daily and freeplay modes, undo support, and PWA install with push notifications.',
description:
'Daily challenge game where players chain movies through shared actors — a "six degrees of Kevin Bacon" reimagined for the web. Features leaderboards, daily and freeplay modes, undo support, and PWA install with push notifications.',
x: -260,
y: -390,
parentId: 'proj-games',
@@ -400,7 +420,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'origin',
category: 'skills',
size: 'large',
icon: '/icons/skills.svg',
icon: '/icons/skills.svg'
},
// Concepts - upper right
@@ -413,7 +433,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'skills-branch',
category: 'skills',
size: 'medium',
icon: '/icons/concepts.svg',
icon: '/icons/concepts.svg'
},
// Tools & DevOps - moved up slightly
@@ -426,7 +446,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'skills-branch',
category: 'skills',
size: 'medium',
icon: '/icons/devops.svg',
icon: '/icons/devops.svg'
},
// Frontend - lower right
@@ -439,7 +459,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'skills-branch',
category: 'skills',
size: 'medium',
icon: '/icons/frontend.svg',
icon: '/icons/frontend.svg'
},
// Backend & Real-Time - pulled closer to Skills
@@ -452,7 +472,7 @@ export const treeNodes: TreeNode[] = [
parentId: 'skills-branch',
category: 'skills',
size: 'medium',
icon: '/icons/backend.svg',
icon: '/icons/backend.svg'
},
// Languages - left side
@@ -465,33 +485,35 @@ export const treeNodes: TreeNode[] = [
parentId: 'skills-branch',
category: 'skills',
size: 'medium',
icon: '/icons/languages.svg',
icon: '/icons/languages.svg'
},
// ========== CONCEPTS CHILDREN (4) ==========
{
id: 'concept-oop',
label: 'Object Oriented Programming',
description: 'Object-Oriented Programming principles including encapsulation, inheritance, and polymorphism.',
description:
'Object-Oriented Programming principles including encapsulation, inheritance, and polymorphism.',
x: 380,
y: -120,
parentId: 'concepts',
category: 'skills',
size: 'small',
icon: '/icons/oop.svg',
proficiency: 'expert',
proficiency: 'expert'
},
{
id: 'concept-data-structure',
label: 'Data Structures & Algorithms',
description: 'Fundamental building blocks of efficient software—organizing data and solving problems with optimal time and space complexity.',
description:
'Fundamental building blocks of efficient software—organizing data and solving problems with optimal time and space complexity.',
x: 430,
y: -75,
parentId: 'concepts',
category: 'skills',
size: 'small',
icon: '/icons/datastructure.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'concept-tdd',
@@ -503,7 +525,7 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/test.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'concept-agile',
@@ -515,7 +537,7 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/agile.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
// ========== TOOLS & DEVOPS CHILDREN (5) ==========
@@ -529,19 +551,20 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/git.svg',
proficiency: 'expert',
proficiency: 'expert'
},
{
id: 'tool-docker',
label: 'Docker',
description: 'Containerization with Docker for consistent development and deployment environments.',
description:
'Containerization with Docker for consistent development and deployment environments.',
x: 560,
y: 95,
parentId: 'tools-devops',
category: 'skills',
size: 'small',
icon: '/icons/docker.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'tool-cicd',
@@ -553,19 +576,20 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/cicd.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'tool-vscod',
label: 'VSCode',
description: 'Powerful, lightweight code editor by Microsoft with built-in Git support, debugging, and a vast extension ecosystem.',
description:
'Powerful, lightweight code editor by Microsoft with built-in Git support, debugging, and a vast extension ecosystem.',
x: 520,
y: 205,
parentId: 'tools-devops',
category: 'skills',
size: 'small',
icon: '/icons/vscode-plain.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'tool-linux',
@@ -577,7 +601,7 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/linux.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
// ========== FRONTEND CHILDREN (6) ==========
@@ -591,31 +615,33 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/react.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'fe-redux',
label: 'Redux',
description: 'Predictable state management library for JavaScript applications, centralizing app state in a single store.',
description:
'Predictable state management library for JavaScript applications, centralizing app state in a single store.',
x: 450,
y: 350,
parentId: 'frontend',
category: 'skills',
size: 'small',
icon: '/icons/redux.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'fe-zustand',
label: 'Zustand',
description: 'Lightweight state management for React using a simple hooks-based API with minimal boilerplate.',
description:
'Lightweight state management for React using a simple hooks-based API with minimal boilerplate.',
x: 440,
y: 405,
parentId: 'frontend',
category: 'skills',
size: 'small',
icon: '/icons/zustand-plain.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'fe-tailwind',
@@ -627,31 +653,33 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/tailwindcss.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'fe-shadcn',
label: 'ShadCN/UI',
description: 'Beautifully designed, accessible UI components that you copy and paste into your project. Built on Radix UI and Tailwind CSS.',
description:
'Beautifully designed, accessible UI components that you copy and paste into your project. Built on Radix UI and Tailwind CSS.',
x: 340,
y: 500,
parentId: 'frontend',
category: 'skills',
size: 'small',
icon: '/icons/shadcnui.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'fe-pwa',
label: 'PWA',
description: 'Progressive Web Apps—web applications that deliver native app-like experiences with offline support, push notifications, and home screen installation.',
description:
'Progressive Web Apps—web applications that deliver native app-like experiences with offline support, push notifications, and home screen installation.',
x: 270,
y: 520,
parentId: 'frontend',
category: 'skills',
size: 'small',
icon: '/icons/pwa.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
// ========== BACKEND & REAL-TIME CHILDREN (6) ==========
@@ -665,67 +693,72 @@ export const treeNodes: TreeNode[] = [
category: 'skills',
size: 'small',
icon: '/icons/nodedotjs.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'be-hapi',
label: 'Hapi.js',
description: 'Enterprise-grade Node.js framework for building scalable APIs and services with built-in validation, authentication, and caching.',
description:
'Enterprise-grade Node.js framework for building scalable APIs and services with built-in validation, authentication, and caching.',
x: 175,
y: 370,
parentId: 'backend-real-time',
category: 'skills',
size: 'small',
icon: '/icons/hapi.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'be-nestjs',
label: 'NestJS',
description: 'Progressive Node.js framework for building efficient, scalable server-side applications with TypeScript and Angular-inspired architecture.',
description:
'Progressive Node.js framework for building efficient, scalable server-side applications with TypeScript and Angular-inspired architecture.',
x: 115,
y: 390,
parentId: 'backend-real-time',
category: 'skills',
size: 'small',
icon: '/icons/nestjs.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'be-webrtc',
label: 'WebRTC',
description: 'Real-time communication protocol enabling peer-to-peer audio, video, and data streaming directly in the browser without plugins.',
description:
'Real-time communication protocol enabling peer-to-peer audio, video, and data streaming directly in the browser without plugins.',
x: 50,
y: 385,
parentId: 'backend-real-time',
category: 'skills',
size: 'small',
icon: '/icons/webrtc.svg',
proficiency: 'intermediate',
proficiency: 'intermediate'
},
{
id: 'be-socketio',
label: 'Socket.io',
description: 'Real-time bidirectional event-based communication library for Node.js and browsers with automatic fallback support.',
description:
'Real-time bidirectional event-based communication library for Node.js and browsers with automatic fallback support.',
x: 0,
y: 355,
parentId: 'backend-real-time',
category: 'skills',
size: 'small',
icon: '/icons/socketdotio.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
{
id: 'be-restfulapi',
label: 'RESTful APIs',
description: 'Architectural style for designing networked applications using stateless HTTP methods to perform CRUD operations on resources.',
description:
'Architectural style for designing networked applications using stateless HTTP methods to perform CRUD operations on resources.',
x: -25,
y: 310,
parentId: 'backend-real-time',
category: 'skills',
size: 'small',
icon: '/icons/openapi.svg',
proficiency: 'advanced',
proficiency: 'advanced'
},
// ========== LANGUAGES CHILDREN (7) ==========
@@ -740,7 +773,7 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/javascript.svg',
proficiency: 'expert',
yearsOfExperience: 5,
yearsOfExperience: 5
},
{
id: 'lang-typescript',
@@ -753,7 +786,7 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/typescript.svg',
proficiency: 'expert',
yearsOfExperience: 5,
yearsOfExperience: 5
},
{
id: 'lang-python',
@@ -766,12 +799,13 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/python.svg',
proficiency: 'intermediate',
yearsOfExperience: 2,
yearsOfExperience: 2
},
{
id: 'lang-rust',
label: 'Rust',
description: 'Systems programming language focused on safety, speed, and concurrency—guaranteeing memory safety without a garbage collector.',
description:
'Systems programming language focused on safety, speed, and concurrency—guaranteeing memory safety without a garbage collector.',
x: -190,
y: 355,
parentId: 'languages',
@@ -779,12 +813,13 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/rust.svg',
proficiency: 'beginner',
yearsOfExperience: 1,
yearsOfExperience: 1
},
{
id: 'lang-php',
label: 'PHP',
description: 'Server-side scripting language powering much of the web—from WordPress to Laravel, built for dynamic web content.',
description:
'Server-side scripting language powering much of the web—from WordPress to Laravel, built for dynamic web content.',
x: -165,
y: 405,
parentId: 'languages',
@@ -792,7 +827,7 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/php.svg',
proficiency: 'intermediate',
yearsOfExperience: 2,
yearsOfExperience: 2
},
{
id: 'lang-sql',
@@ -805,7 +840,7 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/mysql.svg',
proficiency: 'intermediate',
yearsOfExperience: 2,
yearsOfExperience: 2
},
{
id: 'lang-bash',
@@ -818,7 +853,7 @@ export const treeNodes: TreeNode[] = [
size: 'small',
icon: '/icons/gnubash.svg',
proficiency: 'intermediate',
yearsOfExperience: 2,
yearsOfExperience: 2
},
// ========== EDUCATION (Bottom Left Quadrant) ==========
@@ -836,19 +871,18 @@ export const treeNodes: TreeNode[] = [
{
id: 'edu-degree',
label: 'University',
description: 'Bachelor\'s degree in Computer Science.',
description: "Bachelor's degree in Computer Science.",
x: -300,
y: 80,
parentId: 'edu-branch',
category: 'education',
size: 'small',
institution: 'Green River College',
degree: 'Bachelor\'s Degree of Applied Science',
degree: "Bachelor's Degree of Applied Science",
field: 'IT/Software Development',
graduationYear: '2017',
icon: '/icons/greenriver.png'
},
}
// {
// id: 'edu-cert',
// label: 'Certifications',
@@ -863,9 +897,9 @@ export const treeNodes: TreeNode[] = [
// Colors for each category (PoE-inspired)
export const categoryColors: Record<TreeNode['category'], number> = {
origin: 0xffd700, // Gold
experience: 0x4ecdc4, // Teal
projects: 0xff6b6b, // Coral red
skills: 0xf9a825, // Amber orange
education: 0xdda0dd, // Plum purple
origin: 0xffd700, // Gold
experience: 0x4ecdc4, // Teal
projects: 0xff6b6b, // Coral red
skills: 0xf9a825, // Amber orange
education: 0xdda0dd // Plum purple
};
+16 -4
View File
@@ -56,14 +56,23 @@
<link rel="icon" type="image/png" sizes="512x512" href="/icons/pwa-512x512.png" />
<!-- SEO Meta Tags -->
<meta name="description" content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization showcasing Kevin Riehl's professional experience, projects, and technical skills." />
<meta name="keywords" content="portfolio, web developer, software engineer, interactive resume, skill tree" />
<meta
name="description"
content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization showcasing Kevin Riehl's professional experience, projects, and technical skills."
/>
<meta
name="keywords"
content="portfolio, web developer, software engineer, interactive resume, skill tree"
/>
<meta name="author" content="Kevin Riehl" />
<!-- Open Graph Meta Tags for social sharing -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Kevin Riehl - The Atlas of Skills" />
<meta property="og:description" content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization." />
<meta
property="og:description"
content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization."
/>
<meta property="og:url" content="https://tehriehldeal.com" />
<meta property="og:site_name" content="Atlas of Skills" />
<!-- Add og:image when screenshots are available -->
@@ -72,7 +81,10 @@
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Kevin Riehl - The Atlas of Skills" />
<meta name="twitter:description" content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization." />
<meta
name="twitter:description"
content="Interactive portfolio featuring a Path of Exile-inspired skill tree visualization."
/>
<!-- Add twitter:image when screenshots are available -->
<!-- <meta name="twitter:image" content="https://tehriehldeal.com/screenshots/preview.png" /> -->
</svelte:head>