tdd-workshop/slides.html

909 lines
23 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TDD Workshop — 80 Minutes</title>
<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=JetBrains+Mono:wght@400;700;800&family=Archivo+Black&display=swap" rel="stylesheet">
<style>
:root {
--red: #FF3B30;
--green: #34C759;
--refactor: #007AFF;
--bg-dark: #0A0A0A;
--bg-light: #F5F5F0;
--text-dark: #1A1A1A;
--text-light: #FAFAFA;
--accent: #FFD60A;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'JetBrains Mono', monospace;
background: var(--bg-dark);
color: var(--text-light);
overflow: hidden;
cursor: none;
}
/* Custom Cursor */
#cursor {
position: fixed;
width: 20px;
height: 20px;
border: 2px solid var(--accent);
border-radius: 50%;
pointer-events: none;
z-index: 10000;
transition: transform 0.15s ease, border-color 0.2s;
mix-blend-mode: difference;
}
#cursor.click {
transform: scale(0.7);
border-color: var(--green);
}
/* Slide Container */
.slides {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 4rem;
opacity: 0;
transform: translateX(100%);
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
overflow-x: hidden;
overflow-y: auto;
}
.slide.active {
opacity: 1;
transform: translateX(0);
z-index: 10;
}
.slide.prev {
transform: translateX(-100%);
}
/* Slide Backgrounds */
.slide-title { background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1a2e 100%); }
.slide-red { background: linear-gradient(135deg, #2D0A0A 0%, var(--bg-dark) 100%); }
.slide-green { background: linear-gradient(135deg, #0A2D0F 0%, var(--bg-dark) 100%); }
.slide-refactor { background: linear-gradient(135deg, #0A1A2D 0%, var(--bg-dark) 100%); }
.slide-neutral { background: var(--bg-dark); }
/* Typography */
h1 {
font-family: 'Archivo Black', sans-serif;
font-size: clamp(3rem, 8vw, 7rem);
line-height: 0.95;
letter-spacing: -0.03em;
text-transform: uppercase;
margin-bottom: 2rem;
position: relative;
}
h1 .highlight {
display: inline-block;
padding: 0.1em 0.3em;
background: var(--accent);
color: var(--bg-dark);
transform: skewX(-5deg);
}
h2 {
font-size: clamp(2rem, 5vw, 4rem);
font-weight: 800;
margin-bottom: 1.5rem;
letter-spacing: -0.02em;
line-height: 1.6;
padding: 0.5em 0.2em;
display: inline-block;
vertical-align: middle;
}
h3 {
font-size: clamp(1.5rem, 3vw, 2.5rem);
font-weight: 700;
margin-bottom: 1rem;
opacity: 0.9;
}
p, li {
font-size: clamp(1rem, 2vw, 1.5rem);
line-height: 1.6;
opacity: 0.85;
max-width: 50ch;
}
.subtitle {
font-size: clamp(1.2rem, 2.5vw, 2rem);
opacity: 0.7;
font-weight: 400;
}
/* Cycle Visualization */
.cycle {
display: flex;
gap: 2rem;
margin: 3rem 0;
align-items: center;
}
.cycle-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
animation: fadeInUp 0.6s ease forwards;
opacity: 0;
}
.cycle-step:nth-child(1) { animation-delay: 0.2s; }
.cycle-step:nth-child(2) { animation-delay: 0.4s; }
.cycle-step:nth-child(3) { animation-delay: 0.6s; }
.cycle-step:nth-child(4) { animation-delay: 0.8s; }
.cycle-icon {
width: 140px;
height: 140px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3.5rem;
font-weight: 800;
border: 4px solid;
position: relative;
overflow: visible;
flex-shrink: 0;
}
.cycle-icon::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.2);
transform: skewX(-20deg);
animation: shine 3s infinite;
border-radius: 50%;
}
.red-step .cycle-icon {
background: var(--red);
border-color: var(--red);
color: white;
overflow: hidden;
}
.green-step .cycle-icon {
background: var(--green);
border-color: var(--green);
color: white;
overflow: hidden;
}
.refactor-step .cycle-icon {
background: var(--refactor);
border-color: var(--refactor);
color: white;
overflow: hidden;
}
.repeat-step .cycle-icon {
background: transparent;
border-color: var(--accent);
color: var(--accent);
overflow: hidden;
}
.cycle-label {
font-size: 1.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.cycle-desc {
font-size: 1.2rem;
opacity: 0.7;
text-align: center;
max-width: 20ch;
line-height: 1.4;
}
.arrow {
font-size: 2rem;
opacity: 0.4;
animation: pulse 2s infinite;
}
/* Timeline */
.timeline {
display: grid;
gap: 1.5rem;
margin: 2rem 0;
width: 100%;
max-width: 900px;
}
.timeline-item {
display: grid;
grid-template-columns: 100px 1fr;
gap: 2rem;
padding: 1.5rem;
background: rgba(255, 255, 255, 0.05);
border-left: 4px solid var(--accent);
animation: slideInLeft 0.5s ease forwards;
opacity: 0;
}
.timeline-item:nth-child(1) { animation-delay: 0.1s; }
.timeline-item:nth-child(2) { animation-delay: 0.2s; }
.timeline-item:nth-child(3) { animation-delay: 0.3s; }
.timeline-item:nth-child(4) { animation-delay: 0.4s; }
.timeline-item:nth-child(5) { animation-delay: 0.5s; }
.timeline-time {
font-size: 1.5rem;
font-weight: 800;
color: var(--accent);
}
.timeline-content h3 {
font-size: 1.3rem;
margin-bottom: 0.5rem;
}
.timeline-content p {
font-size: 1rem;
opacity: 0.7;
}
/* Ping Pong Diagram */
.ping-pong {
display: flex;
gap: 3rem;
margin: 3rem 0;
align-items: center;
}
.dev-role {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
animation: fadeInUp 0.6s ease forwards;
opacity: 0;
}
.dev-role:nth-child(1) { animation-delay: 0.2s; }
.dev-role:nth-child(2) { animation-delay: 0.5s; }
.dev-role:nth-child(3) { animation-delay: 0.8s; }
.dev-avatar {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
font-weight: 800;
border: 4px solid;
}
.dev-role:nth-child(1) .dev-avatar {
background: var(--red);
border-color: var(--red);
color: white;
}
.dev-role:nth-child(2) .dev-avatar {
background: var(--green);
border-color: var(--green);
color: white;
}
.dev-role:nth-child(3) .dev-avatar {
background: var(--refactor);
border-color: var(--refactor);
color: white;
}
.dev-name {
font-size: 1.5rem;
font-weight: 700;
text-transform: uppercase;
}
.dev-task {
font-size: 1rem;
opacity: 0.7;
text-align: center;
max-width: 20ch;
}
.flow-arrow {
font-size: 3rem;
opacity: 0.5;
animation: slideRight 1.5s infinite;
}
/* Lists */
ul {
list-style: none;
display: grid;
gap: 1rem;
margin: 2rem 0;
}
li {
padding-left: 2rem;
position: relative;
animation: fadeInUp 0.5s ease forwards;
opacity: 0;
}
li:nth-child(1) { animation-delay: 0.1s; }
li:nth-child(2) { animation-delay: 0.2s; }
li:nth-child(3) { animation-delay: 0.3s; }
li:nth-child(4) { animation-delay: 0.4s; }
li:nth-child(5) { animation-delay: 0.5s; }
li::before {
content: '▸';
position: absolute;
left: 0;
color: var(--accent);
font-weight: 800;
}
/* Progress Bar */
.progress {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
z-index: 1000;
}
.progress-bar {
height: 100%;
background: var(--accent);
transition: width 0.3s ease;
}
/* Slide Counter */
.counter {
position: fixed;
bottom: 2rem;
right: 2rem;
font-size: 1.5rem;
font-weight: 700;
opacity: 0.5;
z-index: 1000;
font-variant-numeric: tabular-nums;
}
/* Navigation Hint */
.nav-hint {
position: fixed;
bottom: 2rem;
left: 2rem;
font-size: 0.9rem;
opacity: 0.4;
z-index: 1000;
animation: fadeIn 1s ease 2s forwards;
opacity: 0;
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeIn {
to { opacity: 0.4; }
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
@keyframes slideRight {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(10px); }
}
@keyframes shine {
to { left: 100%; }
}
/* Code Block */
.code-block {
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 2rem;
margin: 2rem 0;
font-family: 'JetBrains Mono', monospace;
font-size: 1.1rem;
line-height: 1.8;
max-width: 600px;
animation: fadeInUp 0.6s ease 0.3s forwards;
opacity: 0;
}
.code-comment {
color: #6A9955;
}
.code-keyword {
color: #569CD6;
}
.code-string {
color: #CE9178;
}
/* Big Number */
.big-number {
font-size: clamp(8rem, 20vw, 15rem);
font-weight: 800;
line-height: 1;
color: var(--accent);
opacity: 0.2;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
}
.content-over {
position: relative;
z-index: 1;
max-width: 90%;
padding: 1rem 0;
}
/* Phase indicator circles */
.phase-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
border-radius: 50%;
margin-right: 0.3em;
vertical-align: middle;
position: relative;
top: -0.05em;
}
.phase-red {
background: var(--red);
box-shadow: 0 0 20px rgba(255, 59, 48, 0.5);
}
.phase-green {
background: var(--green);
box-shadow: 0 0 20px rgba(52, 199, 89, 0.5);
}
.phase-blue {
background: var(--refactor);
box-shadow: 0 0 20px rgba(0, 122, 255, 0.5);
}
@media (max-width: 768px) {
.cycle {
flex-direction: column;
gap: 1rem;
}
.ping-pong {
flex-direction: column;
gap: 1.5rem;
}
.arrow, .flow-arrow {
transform: rotate(90deg);
}
.timeline-item {
grid-template-columns: 1fr;
gap: 0.5rem;
}
}
</style>
</head>
<body>
<div id="cursor"></div>
<div class="slides">
<!-- Slide 1: Title -->
<div class="slide active slide-title">
<div class="content-over">
<h1>
<span class="highlight">TDD</span><br>
WORKSHOP
</h1>
<p class="subtitle">80 minutes · 3-Way Ping-Pong · Hands-on Practice</p>
</div>
</div>
<!-- Slide 2: The Cycle -->
<div class="slide slide-neutral">
<div class="content-over">
<h2>The TDD Rhythm</h2>
<div class="cycle">
<div class="cycle-step red-step">
<div class="cycle-icon">1</div>
<div class="cycle-label">Red</div>
<div class="cycle-desc">Write failing test</div>
</div>
<div class="arrow"></div>
<div class="cycle-step green-step">
<div class="cycle-icon">2</div>
<div class="cycle-label">Green</div>
<div class="cycle-desc">Make it pass</div>
</div>
<div class="arrow"></div>
<div class="cycle-step refactor-step">
<div class="cycle-icon">3</div>
<div class="cycle-label">Refactor</div>
<div class="cycle-desc">Clean up code</div>
</div>
<div class="arrow"></div>
<div class="cycle-step repeat-step">
<div class="cycle-icon"></div>
<div class="cycle-label">Repeat</div>
<div class="cycle-desc">Next test</div>
</div>
</div>
</div>
</div>
<!-- Slide 3: Schedule -->
<div class="slide slide-neutral">
<div class="big-number">80</div>
<div class="content-over">
<h2>Today's Schedule</h2>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-time">0:00</div>
<div class="timeline-content">
<h3>Welcome & Setup</h3>
<p>5 minutes · Verify environment</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-time">0:05</div>
<div class="timeline-content">
<h3>Live Demo: FizzBuzz</h3>
<p>15 minutes · Watch TDD in action</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-time">0:20</div>
<div class="timeline-content">
<h3>Exercise Intro</h3>
<p>5 minutes · 3-Way Ping-Pong explained</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-time">0:25</div>
<div class="timeline-content">
<h3>Hands-on Practice</h3>
<p>40 minutes · Mob programming</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-time">1:05</div>
<div class="timeline-content">
<h3>Retrospective</h3>
<p>15 minutes · Reflect & discuss</p>
</div>
</div>
</div>
</div>
</div>
<!-- Slide 4: 3-Way Ping-Pong -->
<div class="slide slide-neutral">
<div class="content-over">
<h2>3-Way Ping-Pong</h2>
<div class="ping-pong">
<div class="dev-role">
<div class="dev-avatar">A</div>
<div class="dev-name">Red</div>
<div class="dev-task">Uncomment test<br>Watch it fail</div>
</div>
<div class="flow-arrow"></div>
<div class="dev-role">
<div class="dev-avatar">B</div>
<div class="dev-name">Green</div>
<div class="dev-task">Write minimal code<br>Make it pass</div>
</div>
<div class="flow-arrow"></div>
<div class="dev-role">
<div class="dev-avatar">C</div>
<div class="dev-name">Refactor</div>
<div class="dev-task">Clean up<br>Next test</div>
</div>
</div>
<p style="margin-top: 3rem; text-align: center; opacity: 0.7;">
Rotate roles with each cycle · One shared screen
</p>
</div>
</div>
<!-- Slide 5: RED Phase -->
<div class="slide slide-red">
<div class="content-over">
<h2 style="color: var(--red);"><span class="phase-indicator phase-red"></span>RED Phase</h2>
<h3>Write a failing test</h3>
<ul>
<li>Uncomment ONE test</li>
<li>Run <code>dart test</code></li>
<li>Watch it fail (proves it can fail)</li>
<li>Pass the keyboard</li>
</ul>
<div class="code-block">
<span class="code-comment">// Uncomment this test</span><br>
<span class="code-keyword">test</span>(<span class="code-string">'returns 1 for input 1'</span>, () {<br>
&nbsp;&nbsp;<span class="code-keyword">expect</span>(fizzbuzz(<span class="code-string">1</span>), <span class="code-string">'1'</span>);<br>
});
</div>
</div>
</div>
<!-- Slide 6: GREEN Phase -->
<div class="slide slide-green">
<div class="content-over">
<h2 style="color: var(--green);"><span class="phase-indicator phase-green"></span>GREEN Phase</h2>
<h3>Make it pass (minimal code)</h3>
<ul>
<li>Write the simplest code that works</li>
<li>Don't worry about perfection</li>
<li>Run tests to confirm GREEN</li>
<li>Pass the keyboard</li>
</ul>
<div class="code-block">
<span class="code-keyword">String</span> fizzbuzz(<span class="code-keyword">int</span> n) {<br>
&nbsp;&nbsp;<span class="code-keyword">return</span> <span class="code-string">'1'</span>; <span class="code-comment">// Hardcode is OK!</span><br>
}
</div>
</div>
</div>
<!-- Slide 7: REFACTOR Phase -->
<div class="slide slide-refactor">
<div class="content-over">
<h2 style="color: var(--refactor);"><span class="phase-indicator phase-blue"></span>REFACTOR Phase</h2>
<h3>Clean up the code</h3>
<ul>
<li>Remove duplication</li>
<li>Improve names</li>
<li>Tests stay GREEN</li>
<li>Uncomment next test (back to RED)</li>
<li>Pass the keyboard</li>
</ul>
</div>
</div>
<!-- Slide 8: Kata Choices -->
<div class="slide slide-neutral">
<div class="content-over">
<h2>Choose Your Kata</h2>
<div style="display: grid; gap: 2rem; margin-top: 2rem;">
<div style="padding: 2rem; background: rgba(255, 255, 255, 0.05); border-left: 4px solid var(--accent);">
<h3>Password Validator</h3>
<p>Rules-based validation · Refactoring challenge · Open-Closed Principle</p>
</div>
<div style="padding: 2rem; background: rgba(255, 255, 255, 0.05); border-left: 4px solid var(--green);">
<h3>Shopping Cart</h3>
<p>Stateful domain object · Data structures · Value Objects</p>
</div>
</div>
</div>
</div>
<!-- Slide 9: Key Principles -->
<div class="slide slide-neutral">
<div class="content-over">
<h2>TDD Principles</h2>
<ul>
<li><strong>Tests first</strong> — Even for "obvious" code</li>
<li><strong>Small steps</strong> — One test at a time</li>
<li><strong>See RED</strong> — Proves the test can fail</li>
<li><strong>Minimal GREEN</strong> — Hardcoding is OK initially</li>
<li><strong>Refactor fearlessly</strong> — Tests protect you</li>
</ul>
</div>
</div>
<!-- Slide 10: Questions -->
<div class="slide slide-title">
<div class="content-over">
<h1>
Ready to<br>
<span class="highlight">Practice?</span>
</h1>
<p class="subtitle">Let's build muscle memory for TDD</p>
</div>
</div>
</div>
<!-- Progress Bar -->
<div class="progress">
<div class="progress-bar"></div>
</div>
<!-- Slide Counter -->
<div class="counter">
<span id="current">1</span> / <span id="total">10</span>
</div>
<!-- Navigation Hint -->
<div class="nav-hint">
← → arrows or click to navigate
</div>
<script>
// Custom cursor
const cursor = document.getElementById('cursor');
document.addEventListener('mousemove', (e) => {
cursor.style.left = e.clientX + 'px';
cursor.style.top = e.clientY + 'px';
});
document.addEventListener('mousedown', () => {
cursor.classList.add('click');
});
document.addEventListener('mouseup', () => {
cursor.classList.remove('click');
});
// Slide navigation
const slides = document.querySelectorAll('.slide');
const progressBar = document.querySelector('.progress-bar');
const currentCounter = document.getElementById('current');
const totalCounter = document.getElementById('total');
let currentSlide = 0;
const totalSlides = slides.length;
totalCounter.textContent = totalSlides;
function updateSlide() {
slides.forEach((slide, index) => {
slide.classList.remove('active', 'prev');
if (index === currentSlide) {
slide.classList.add('active');
} else if (index < currentSlide) {
slide.classList.add('prev');
}
});
// Update progress bar
const progress = ((currentSlide + 1) / totalSlides) * 100;
progressBar.style.width = progress + '%';
// Update counter
currentCounter.textContent = currentSlide + 1;
}
function nextSlide() {
if (currentSlide < totalSlides - 1) {
currentSlide++;
updateSlide();
}
}
function prevSlide() {
if (currentSlide > 0) {
currentSlide--;
updateSlide();
}
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
nextSlide();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
prevSlide();
} else if (e.key === 'Home') {
currentSlide = 0;
updateSlide();
} else if (e.key === 'End') {
currentSlide = totalSlides - 1;
updateSlide();
}
});
// Click navigation
document.addEventListener('click', (e) => {
if (e.clientX > window.innerWidth / 2) {
nextSlide();
} else {
prevSlide();
}
});
// Touch navigation
let touchStartX = 0;
document.addEventListener('touchstart', (e) => {
touchStartX = e.touches[0].clientX;
});
document.addEventListener('touchend', (e) => {
const touchEndX = e.changedTouches[0].clientX;
const diff = touchStartX - touchEndX;
if (Math.abs(diff) > 50) {
if (diff > 0) {
nextSlide();
} else {
prevSlide();
}
}
});
// Initialize
updateSlide();
</script>
</body>
</html>