Files
kennethreitz.org/templates/mindmap.html
T

1265 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" class="antialiased">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kenneth Reitz - Digital Mind Map</title>
<!-- Load Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Crimson+Text:ital,wght@0,400;0,600;1,400;1,600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<!-- Load Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#4e3979',
50: '#f8f7fd',
100: '#f0eef9',
200: '#e3e0f4',
300: '#d0c8ec',
400: '#b5a7e0',
500: '#9b86d3',
600: '#8265c4',
700: '#6f52b0',
800: '#5c4394',
900: '#4e3979',
950: '#3a2b5c',
}
},
fontFamily: {
'sans': ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'],
'serif': ['Crimson Text', 'Charter', 'Georgia', 'Times New Roman', 'serif'],
'mono': ['JetBrains Mono', 'Fira Code', 'SF Mono', 'Consolas', 'monospace'],
}
}
}
}
</script>
</head>
<body class="bg-gray-900 font-serif text-gray-100 selection:bg-primary-700 selection:text-white">
<!-- Header -->
<header class="border-b border-gray-700 bg-gray-900/90 backdrop-blur-xl sticky top-0 z-50 shadow-lg">
<div class="max-w-6xl mx-auto px-6 py-8">
<div class="flex flex-col space-y-4">
<!-- Main brand -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-5xl font-bold tracking-tight">
<a href="/" class="text-gray-100 hover:text-primary-400 transition-colors duration-200 no-underline font-serif bg-gradient-to-r from-primary-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">
Kenneth Reitz
</a>
</h1>
<p class="text-xl text-gray-400 font-serif italic mt-2 tracking-wide">
Creator of <span class="text-primary-400 font-semibold">Requests</span> • Python Advocate • Making Complex Things Simple
</p>
</div>
<!-- Mobile-friendly navigation -->
<nav class="hidden md:flex space-x-6">
<a href="/directory" class="text-sm font-medium text-gray-300 hover:text-primary-400 transition-all duration-300 px-4 py-2 rounded-lg hover:bg-primary-900/30 border border-transparent hover:border-primary-500/30">
📁 Explore Files
</a>
<a href="https://github.com/psf/requests" target="_blank" class="text-sm font-medium text-gray-300 hover:text-primary-400 transition-all duration-300 px-4 py-2 rounded-lg hover:bg-primary-900/30 border border-transparent hover:border-primary-500/30">
🌐 Requests
</a>
</nav>
</div>
<!-- Mobile navigation -->
<div class="flex md:hidden">
<!-- Mobile nav links -->
<nav class="flex space-x-4">
<a href="/directory" class="text-sm font-medium text-gray-300 hover:text-primary-400 transition-all duration-300 px-4 py-2 rounded-lg hover:bg-primary-900/30">
📁 Files
</a>
<a href="https://github.com/psf/requests" target="_blank" class="text-sm font-medium text-gray-300 hover:text-primary-400 transition-all duration-300 px-4 py-2 rounded-lg hover:bg-primary-900/30">
🌐 Requests
</a>
</nav>
</div>
</div>
</div>
</header>
<style>
/* Kenneth Reitz Brand Colors & Variables */
:root {
--kr-primary: #6366f1;
--kr-primary-dark: #4338ca;
--kr-secondary: #f59e0b;
--kr-accent: #10b981;
--kr-purple: #8b5cf6;
--kr-pink: #ec4899;
--kr-bg: #0f0f0f;
--kr-surface: #1a1a1a;
--kr-card: #262626;
--kr-text: #f8fafc;
--kr-text-muted: #94a3b8;
--kr-border: #374151;
--kr-glow: rgba(99, 102, 241, 0.3);
}
body {
background: linear-gradient(135deg, var(--kr-bg) 0%, #111827 50%, var(--kr-bg) 100%);
min-height: 100vh;
}
/* Hero Section */
.hero-section {
padding: 80px 0 60px;
text-align: center;
position: relative;
background: radial-gradient(circle at 50% 20%, rgba(99, 102, 241, 0.1) 0%, transparent 50%);
}
.hero-title {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: 800;
background: linear-gradient(135deg, var(--kr-primary) 0%, var(--kr-purple) 50%, var(--kr-pink) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1rem;
line-height: 1.1;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 1.5rem;
color: var(--kr-text-muted);
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.hero-stats {
display: flex;
justify-content: center;
gap: 3rem;
margin-bottom: 3rem;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 2.5rem;
font-weight: 700;
color: var(--kr-primary);
display: block;
transition: all 0.3s ease;
}
#live-downloads {
background: linear-gradient(135deg, var(--kr-primary), var(--kr-purple));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
position: relative;
}
#live-downloads::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.1));
border-radius: 8px;
animation: pulse-bg 3s ease-in-out infinite;
z-index: -1;
}
@keyframes pulse-bg {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
margin-left: 8px;
animation: live-pulse 2s ease-in-out infinite;
}
@keyframes live-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
}
.stat-label {
color: var(--kr-text-muted);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Featured Projects */
.featured-section {
padding: 60px 0;
background: rgba(26, 26, 26, 0.8);
backdrop-filter: blur(10px);
}
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 3rem;
}
.project-card {
background: var(--kr-card);
border: 1px solid var(--kr-border);
border-radius: 16px;
padding: 2rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.project-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--kr-primary), var(--kr-purple));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.project-card:hover::before {
transform: scaleX(1);
}
.project-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(99, 102, 241, 0.2);
border-color: var(--kr-primary);
}
.project-icon {
width: 60px;
height: 60px;
background: linear-gradient(135deg, var(--kr-primary), var(--kr-purple));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 1.5rem;
}
.project-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--kr-text);
margin-bottom: 0.5rem;
}
.project-desc {
color: var(--kr-text-muted);
line-height: 1.6;
margin-bottom: 1.5rem;
}
.project-stats {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: var(--kr-text-muted);
}
/* Mind Map Section */
.mindmap-section {
padding: 60px 0;
}
.obsidian-mindmap {
background: linear-gradient(135deg, var(--kr-surface) 0%, #1f1f1f 100%);
border: 1px solid var(--kr-border);
border-radius: 20px;
position: relative;
overflow: hidden;
height: 70vh;
min-height: 600px;
box-shadow:
0 25px 50px rgba(0, 0, 0, 0.5),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.graph-container {
width: 100%;
height: 100%;
position: relative;
cursor: grab;
}
.graph-container:active {
cursor: grabbing;
}
.mindmap-svg {
width: 100%;
height: 100%;
background: radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.05) 0%, transparent 70%);
}
/* Enhanced Node Styles */
.node {
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.node circle {
stroke-width: 2;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.4));
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.node.directory circle {
fill: var(--kr-primary);
stroke: rgba(99, 102, 241, 0.8);
r: 10;
}
.node.file circle {
fill: var(--kr-accent);
stroke: rgba(16, 185, 129, 0.8);
r: 7;
}
.node.root circle {
fill: var(--kr-secondary);
stroke: rgba(245, 158, 11, 0.9);
r: 16;
stroke-width: 3;
}
.node:hover circle {
transform: scale(1.3);
filter: drop-shadow(0 8px 20px rgba(0, 0, 0, 0.6)) brightness(1.2);
}
.node.active circle {
stroke-width: 4;
filter: drop-shadow(0 0 20px currentColor) drop-shadow(0 6px 15px rgba(0, 0, 0, 0.4));
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.node text {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 11px;
font-weight: 600;
fill: #000000;
text-anchor: middle;
dominant-baseline: middle;
pointer-events: none;
text-shadow: 0 2px 4px rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.node.directory text {
font-weight: 700;
font-size: 12px;
}
.node.root text {
font-size: 15px;
font-weight: 800;
fill: #000000;
}
.node:hover text {
font-size: 13px;
fill: #000000;
font-weight: 700;
}
/* Enhanced Links */
.link {
fill: none;
stroke: rgba(0, 0, 0, 0.4);
stroke-width: 2;
stroke-opacity: 0.7;
transition: all 0.3s ease;
}
.link.active {
stroke: rgba(0, 0, 0, 0.9);
stroke-width: 3;
stroke-opacity: 1;
filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0.5));
}
/* Enhanced Tooltip */
.obsidian-tooltip {
position: absolute;
background: rgba(26, 26, 26, 0.95);
border: 1px solid var(--kr-border);
border-radius: 12px;
padding: 16px 20px;
color: var(--kr-text);
font-size: 13px;
font-family: 'Inter', sans-serif;
max-width: 320px;
z-index: 1000;
opacity: 0;
pointer-events: none;
backdrop-filter: blur(12px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateY(8px);
}
.obsidian-tooltip.visible {
opacity: 1;
transform: translateY(0);
}
.tooltip-title {
font-weight: 700;
color: var(--kr-primary);
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.tooltip-path {
font-size: 11px;
color: var(--kr-text-muted);
font-family: 'JetBrains Mono', monospace;
margin-bottom: 8px;
background: rgba(0, 0, 0, 0.3);
padding: 4px 8px;
border-radius: 4px;
}
.tooltip-meta {
font-size: 11px;
color: var(--kr-text-muted);
}
/* Control Panels */
.control-panel {
position: absolute;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 100;
}
.control-button {
width: 48px;
height: 48px;
background: rgba(26, 26, 26, 0.9);
border: 1px solid var(--kr-border);
border-radius: 12px;
color: var(--kr-text);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 18px;
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(12px);
}
.control-button:hover {
background: var(--kr-primary);
color: white;
transform: translateY(-2px) scale(1.05);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
}
.search-panel {
position: absolute;
top: 20px;
left: 20px;
width: 320px;
background: rgba(26, 26, 26, 0.95);
border: 1px solid var(--kr-border);
border-radius: 16px;
padding: 20px;
backdrop-filter: blur(12px);
z-index: 100;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
}
.search-input {
width: 100%;
background: rgba(15, 15, 15, 0.8);
border: 1px solid var(--kr-border);
border-radius: 10px;
padding: 12px 16px;
color: var(--kr-text);
font-size: 14px;
transition: all 0.3s ease;
font-family: 'Inter', sans-serif;
}
.search-input:focus {
outline: none;
border-color: var(--kr-primary);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
background: rgba(15, 15, 15, 0.9);
}
.search-input::placeholder {
color: var(--kr-text-muted);
}
.info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(26, 26, 26, 0.95);
border: 1px solid var(--kr-border);
border-radius: 16px;
padding: 20px;
backdrop-filter: blur(12px);
z-index: 100;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
font-family: 'Inter', sans-serif;
font-size: 12px;
color: var(--kr-text-muted);
min-width: 220px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
padding: 6px 0;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
}
.info-item:last-child {
margin-bottom: 0;
border-bottom: none;
}
.info-label {
color: var(--kr-text-muted);
font-weight: 500;
}
.info-value {
color: var(--kr-primary);
font-weight: 700;
}
/* Loading Animation */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--kr-text);
font-family: 'Inter', sans-serif;
}
.loading-spinner {
width: 56px;
height: 56px;
border: 4px solid rgba(99, 102, 241, 0.2);
border-top: 4px solid var(--kr-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 18px;
font-weight: 600;
color: var(--kr-text-muted);
}
/* Philosophy Quote */
.philosophy-section {
text-align: center;
padding: 60px 0;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%);
}
.philosophy-quote {
font-size: 1.8rem;
font-style: italic;
color: var(--kr-text);
margin-bottom: 1rem;
max-width: 800px;
margin-left: auto;
margin-right: auto;
line-height: 1.4;
}
.philosophy-author {
color: var(--kr-primary);
font-weight: 600;
font-size: 1.1rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.hero-stats {
gap: 2rem;
}
.stat-number {
font-size: 2rem;
}
.featured-grid {
grid-template-columns: 1fr;
}
.search-panel {
width: calc(100% - 40px);
position: relative;
top: 0;
left: 0;
margin-bottom: 20px;
}
.obsidian-mindmap {
height: 60vh;
min-height: 500px;
}
.info-panel {
position: relative;
bottom: 0;
left: 0;
margin-top: 20px;
width: 100%;
}
.control-panel {
top: 10px;
right: 10px;
flex-direction: row;
}
.control-button {
width: 40px;
height: 40px;
font-size: 16px;
}
}
</style>
<!-- Hero Section -->
<section class="hero-section">
<div class="max-w-6xl mx-auto px-6">
<h1 class="hero-title">Kenneth Reitz</h1>
<p class="hero-subtitle">
Creator of <strong>Requests</strong> • Python Advocate • Making Complex Things Simple & Beautiful
</p>
<div class="hero-stats">
<div class="stat-item">
<span class="stat-number" id="live-downloads">0</span>
<span class="stat-label">Live Downloads Today <span class="live-indicator"></span></span>
</div>
<div class="stat-item">
<span class="stat-number">16M</span>
<span class="stat-label">Daily Average</span>
</div>
<div class="stat-item">
<span class="stat-number">51K+</span>
<span class="stat-label">GitHub Stars</span>
</div>
<div class="stat-item">
<span class="stat-number"></span>
<span class="stat-label">Impact</span>
</div>
</div>
</div>
</section>
<!-- Featured Projects -->
<section class="featured-section">
<div class="max-w-6xl mx-auto px-6">
<h2 class="text-4xl font-bold text-center mb-4" style="color: var(--kr-text);">Legendary Contributions</h2>
<p class="text-center text-xl" style="color: var(--kr-text-muted);">Libraries that changed how the world codes</p>
<div class="featured-grid">
<div class="project-card">
<div class="project-icon">🌐</div>
<h3 class="project-title">Requests</h3>
<p class="project-desc">HTTP for Humans. The most elegant Python library ever created, downloaded by every Python developer on Earth.</p>
<div class="project-stats">
<span>⭐ 51k stars</span>
<span>📦 16M daily downloads</span>
<span>🔥 Industry standard</span>
</div>
</div>
<div class="project-card">
<div class="project-icon">📦</div>
<h3 class="project-title">pipenv</h3>
<p class="project-desc">Python packaging and dependency management for humans. Bringing sanity to Python environments.</p>
<div class="project-stats">
<span>⭐ 24k stars</span>
<span>🛠️ Dev tool</span>
<span>🎯 Problem solver</span>
</div>
</div>
<div class="project-card">
<div class="project-icon"></div>
<h3 class="project-title">maya</h3>
<p class="project-desc">Datetimes for Humans. Making datetime manipulation intuitive and Pythonic with natural language parsing.</p>
<div class="project-stats">
<span>⭐ 3.4k stars</span>
<span>🕐 Time magic</span>
<span>🐍 Pythonic</span>
</div>
</div>
<div class="project-card">
<div class="project-icon">🎨</div>
<h3 class="project-title">Philosophy</h3>
<p class="project-desc">"For Humans" design philosophy. Creating APIs that feel natural and make developers happy to write code.</p>
<div class="project-stats">
<span>💡 Human-centered</span>
<span>✨ Beautiful APIs</span>
<span>🌟 Developer joy</span>
</div>
</div>
</div>
</div>
</section>
<!-- Philosophy Section -->
<section class="philosophy-section">
<div class="max-w-4xl mx-auto px-6">
<blockquote class="philosophy-quote">
"Beautiful is better than ugly. Simple is better than complex. Complex is better than complicated."
</blockquote>
<div class="philosophy-author">— The Zen of Python (Tim Peters)</div>
</div>
</section>
<!-- Mind Map Section -->
<section class="mindmap-section">
<div class="max-w-7xl mx-auto px-6">
<h2 class="text-4xl font-bold text-center mb-4" style="color: var(--kr-text);">Explore the Digital Universe</h2>
<p class="text-center text-xl mb-8" style="color: var(--kr-text-muted);">Interactive knowledge graph of thoughts, projects, and insights</p>
<div class="obsidian-mindmap">
<!-- Loading State -->
<div id="loading" class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">Mapping neural pathways...</div>
</div>
<!-- Main Graph Container -->
<div id="graph-container" class="graph-container" style="display: none;">
<svg class="mindmap-svg" id="mindmap-svg"></svg>
<!-- Search Panel -->
<div class="search-panel">
<input
type="text"
id="search-input"
class="search-input"
placeholder="Search the knowledge graph..."
>
</div>
<!-- Control Panel -->
<div class="control-panel">
<div class="control-button" id="zoom-in" title="Zoom In">+</div>
<div class="control-button" id="zoom-out" title="Zoom Out"></div>
<div class="control-button" id="reset-view" title="Reset View"></div>
<div class="control-button" id="toggle-physics" title="Toggle Physics"></div>
</div>
<!-- Info Panel -->
<div class="info-panel">
<div class="info-item">
<span class="info-label">Nodes</span>
<span class="info-value" id="node-count">0</span>
</div>
<div class="info-item">
<span class="info-label">Connections</span>
<span class="info-value" id="link-count">0</span>
</div>
<div class="info-item">
<span class="info-label">Directories</span>
<span class="info-value" id="dir-count">0</span>
</div>
<div class="info-item">
<span class="info-label">Files</span>
<span class="info-value" id="file-count">0</span>
</div>
</div>
<!-- Tooltip -->
<div class="obsidian-tooltip" id="tooltip"></div>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-gray-800/60 mt-20 bg-gradient-to-t from-gray-900 to-gray-900/60 backdrop-blur-sm">
<div class="max-w-6xl mx-auto px-6 py-16">
<div class="text-center">
<!-- Impact Statement -->
<div class="mb-8">
<p class="text-lg text-gray-300 font-medium mb-2">
Empowering developers worldwide with elegant, human-centered code
</p>
<p class="text-sm text-gray-500">
16M downloads daily • Touching every Python project on Earth
</p>
</div>
<!-- Social Links -->
<div class="flex justify-center space-x-8 mb-8">
<a href="https://github.com/kennethreitz" target="_blank" class="text-gray-400 hover:text-primary-400 transition-all duration-300 flex items-center space-x-2 group">
<svg class="h-6 w-6 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd"/>
</svg>
<span class="hidden sm:inline font-medium">GitHub</span>
</a>
<a href="https://twitter.com/kennethreitz" target="_blank" class="text-gray-400 hover:text-primary-400 transition-all duration-300 flex items-center space-x-2 group">
<svg class="h-6 w-6 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.29 18.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0020 3.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.073 4.073 0 01.8 7.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 010 16.407a11.616 11.616 0 006.29 1.84"/>
</svg>
<span class="hidden sm:inline font-medium">Twitter</span>
</a>
<a href="https://github.com/psf/requests" target="_blank" class="text-gray-400 hover:text-primary-400 transition-all duration-300 flex items-center space-x-2 group">
<svg class="h-6 w-6 group-hover:scale-110 transition-transform" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2L3 7v11h14V7l-7-5z"/>
</svg>
<span class="hidden sm:inline font-medium">Requests</span>
</a>
</div>
<!-- Copyright -->
<div class="border-t border-gray-800/40 pt-8">
<p class="text-sm text-gray-500">
&copy; 2024 Kenneth Reitz. Crafted with passion for beautiful, human-centered design.
</p>
<p class="text-xs text-gray-600 mt-2">
"Beautiful is better than ugly. Simple is better than complex."
</p>
</div>
</div>
</div>
</footer>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
class ObsidianMindmap {
constructor() {
this.data = null;
this.nodes = [];
this.links = [];
this.simulation = null;
this.svg = null;
this.g = null;
this.zoom = null;
this.physicsEnabled = true;
this.searchTerm = '';
this.width = 0;
this.height = 0;
this.init();
}
async init() {
try {
const response = await fetch('/api/mindmap');
this.data = await response.json();
this.setupSVG();
this.processData();
this.createSimulation();
this.render();
this.setupEventListeners();
this.updateStats();
document.getElementById('loading').style.display = 'none';
document.getElementById('graph-container').style.display = 'block';
} catch (error) {
console.error('Failed to load mindmap data:', error);
}
}
setupSVG() {
const container = document.getElementById('graph-container');
this.width = container.clientWidth;
this.height = container.clientHeight;
this.svg = d3.select('#mindmap-svg')
.attr('width', this.width)
.attr('height', this.height);
this.zoom = d3.zoom()
.scaleExtent([0.1, 4])
.on('zoom', (event) => {
this.g.attr('transform', event.transform);
});
this.svg.call(this.zoom);
this.g = this.svg.append('g');
}
processData(node = this.data, parent = null, level = 0) {
if (!node) return;
const nodeData = {
id: node.path || 'root',
name: node.name || 'Root',
type: node.type || 'root',
path: node.path || '',
level: level,
parent: parent,
children: node.children || []
};
this.nodes.push(nodeData);
if (parent) {
this.links.push({
source: parent.id,
target: nodeData.id
});
}
if (node.children) {
node.children.forEach(child => {
this.processData(child, nodeData, level + 1);
});
}
}
createSimulation() {
this.simulation = d3.forceSimulation(this.nodes)
.force('link', d3.forceLink(this.links)
.id(d => d.id)
.distance(d => {
const sourceLevel = this.nodes.find(n => n.id === d.source.id)?.level || 0;
const targetLevel = this.nodes.find(n => n.id === d.target.id)?.level || 0;
return 100 + (Math.max(sourceLevel, targetLevel) * 30);
})
.strength(0.7))
.force('charge', d3.forceManyBody()
.strength(d => {
if (d.type === 'root') return -2000;
if (d.type === 'directory') return -800;
return -400;
}))
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
.force('collision', d3.forceCollide()
.radius(d => {
if (d.type === 'root') return 30;
if (d.type === 'directory') return 25;
return 20;
}))
.force('x', d3.forceX(this.width / 2).strength(0.1))
.force('y', d3.forceY(this.height / 2).strength(0.1));
this.simulation.on('tick', () => this.ticked());
}
render() {
// Render links first (so they appear behind nodes)
this.linkElements = this.g.selectAll('.link')
.data(this.links)
.enter()
.append('line')
.attr('class', 'link')
.attr('stroke-width', d => {
const sourceNode = this.nodes.find(n => n.id === d.source.id);
const targetNode = this.nodes.find(n => n.id === d.target.id);
if (sourceNode?.type === 'root' || targetNode?.type === 'root') return 4;
if (sourceNode?.type === 'directory' || targetNode?.type === 'directory') return 3;
return 2;
});
// Render nodes
this.nodeElements = this.g.selectAll('.node')
.data(this.nodes)
.enter()
.append('g')
.attr('class', d => `node ${d.type}`)
.call(this.drag());
// Add circles to nodes
this.nodeElements.append('circle');
// Add text labels
this.nodeElements.append('text')
.attr('dy', d => d.type === 'root' ? 30 : 25)
.text(d => {
if (d.name.length > 20) {
return d.name.substring(0, 17) + '...';
}
return d.name;
});
// Add event listeners
this.nodeElements
.on('mouseover', (event, d) => this.showTooltip(event, d))
.on('mouseout', () => this.hideTooltip())
.on('click', (event, d) => this.handleNodeClick(event, d))
.on('dblclick', (event, d) => this.handleNodeDoubleClick(event, d));
}
ticked() {
if (this.linkElements) {
this.linkElements
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
}
if (this.nodeElements) {
this.nodeElements
.attr('transform', d => `translate(${d.x}, ${d.y})`);
}
}
drag() {
return d3.drag()
.on('start', (event, d) => {
if (!event.active && this.physicsEnabled) {
this.simulation.alphaTarget(0.3).restart();
}
d.fx = d.x;
d.fy = d.y;
})
.on('drag', (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on('end', (event, d) => {
if (!event.active && this.physicsEnabled) {
this.simulation.alphaTarget(0);
}
d.fx = null;
d.fy = null;
});
}
showTooltip(event, d) {
const tooltip = document.getElementById('tooltip');
const icon = d.type === 'directory' ? '📁' : d.type === 'root' ? '🌟' : '📄';
tooltip.innerHTML = `
<div class="tooltip-title">
<span>${icon}</span>
<span>${d.name}</span>
</div>
<div class="tooltip-path">${d.path || 'Root'}</div>
<div class="tooltip-meta">
Type: ${d.type} • Level: ${d.level}
${d.children ? ` • Children: ${d.children.length}` : ''}
</div>
`;
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY - 15) + 'px';
tooltip.classList.add('visible');
}
hideTooltip() {
document.getElementById('tooltip').classList.remove('visible');
}
handleNodeClick(event, d) {
// Highlight connected nodes
this.nodeElements.classed('active', false);
this.linkElements.classed('active', false);
d3.select(event.currentTarget).classed('active', true);
// Highlight connected links
this.linkElements
.classed('active', link =>
link.source.id === d.id || link.target.id === d.id
);
}
handleNodeDoubleClick(event, d) {
if (d.path && d.path !== '') {
window.open(`/${d.path}`, '_blank');
}
}
setupEventListeners() {
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', () => {
this.svg.transition().duration(300).call(
this.zoom.scaleBy, 1.5
);
});
document.getElementById('zoom-out').addEventListener('click', () => {
this.svg.transition().duration(300).call(
this.zoom.scaleBy, 1 / 1.5
);
});
document.getElementById('reset-view').addEventListener('click', () => {
this.svg.transition().duration(750).call(
this.zoom.transform,
d3.zoomIdentity.translate(this.width / 2, this.height / 2).scale(1)
);
});
document.getElementById('toggle-physics').addEventListener('click', () => {
this.physicsEnabled = !this.physicsEnabled;
if (this.physicsEnabled) {
this.simulation.alphaTarget(0.3).restart();
} else {
this.simulation.stop();
}
});
// Search functionality
const searchInput = document.getElementById('search-input');
searchInput.addEventListener('input', (event) => {
this.searchTerm = event.target.value.toLowerCase();
this.filterNodes();
});
// Window resize
window.addEventListener('resize', () => {
this.handleResize();
});
}
filterNodes() {
this.nodeElements
.style('opacity', d => {
if (!this.searchTerm) return 1;
return d.name.toLowerCase().includes(this.searchTerm) ? 1 : 0.3;
})
.select('circle')
.style('transform', d => {
if (!this.searchTerm) return null;
const scale = d.name.toLowerCase().includes(this.searchTerm) ? 1.2 : 0.8;
return `scale(${scale})`;
});
this.linkElements
.style('opacity', d => {
if (!this.searchTerm) return 0.7;
const sourceMatch = d.source.name.toLowerCase().includes(this.searchTerm);
const targetMatch = d.target.name.toLowerCase().includes(this.searchTerm);
return (sourceMatch || targetMatch) ? 1 : 0.2;
});
}
updateStats() {
const nodeCount = this.nodes.length;
const linkCount = this.links.length;
const dirCount = this.nodes.filter(n => n.type === 'directory').length;
const fileCount = this.nodes.filter(n => n.type === 'file').length;
document.getElementById('node-count').textContent = nodeCount;
document.getElementById('link-count').textContent = linkCount;
document.getElementById('dir-count').textContent = dirCount;
document.getElementById('file-count').textContent = fileCount;
}
handleResize() {
const container = document.getElementById('graph-container');
this.width = container.clientWidth;
this.height = container.clientHeight;
this.svg
.attr('width', this.width)
.attr('height', this.height);
this.simulation
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
.force('x', d3.forceX(this.width / 2).strength(0.1))
.force('y', d3.forceY(this.height / 2).strength(0.1))
.alpha(0.3)
.restart();
}
}
// Real-time download counter
function initDownloadCounter() {
const downloadsPerSecond = 16000000 / (24 * 60 * 60); // 16M per day
const startOfDay = new Date();
startOfDay.setHours(0, 0, 0, 0);
const now = new Date();
const secondsSinceStartOfDay = (now - startOfDay) / 1000;
let currentDownloads = Math.floor(secondsSinceStartOfDay * downloadsPerSecond);
const counter = document.getElementById('live-downloads');
function updateCounter() {
currentDownloads += downloadsPerSecond;
const displayNumber = Math.floor(currentDownloads);
// Format with commas
counter.textContent = displayNumber.toLocaleString();
// Add a subtle glow effect when updating
counter.style.transform = 'scale(1.05)';
counter.style.filter = 'brightness(1.2)';
setTimeout(() => {
counter.style.transform = 'scale(1)';
counter.style.filter = 'brightness(1)';
}, 200);
}
// Initial display
updateCounter();
// Update every second
setInterval(updateCounter, 1000);
}
// Initialize the mindmap when the page loads
document.addEventListener('DOMContentLoaded', () => {
new ObsidianMindmap();
initDownloadCounter();
});
</script>
</body>
</html>