Skip to content

Commit 1396e71

Browse files
committed
feat: wip visualizer
1 parent 8b2eceb commit 1396e71

File tree

1 file changed

+335
-25
lines changed

1 file changed

+335
-25
lines changed

src/App.tsx

Lines changed: 335 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,344 @@
1-
import { useState } from 'react'
2-
import reactLogo from './assets/react.svg'
3-
import viteLogo from '/vite.svg'
4-
import './App.css'
1+
import { useState, useEffect, useRef, useCallback } from 'react'
2+
// import { motion, AnimatePresence } from 'framer-motion'
3+
import {
4+
Info,
5+
Laptop,
6+
Pause,
7+
Play,
8+
RotateCcw,
9+
Server,
10+
SkipForward,
11+
} from 'lucide-react'
12+
import { Button } from '@/components/ui/button'
13+
import { Slider } from '@/components/ui/slider'
14+
import { ANIMATION_STATES, TCP_IP_LAYERS } from './data'
15+
import getPacketForStep, { type PacketStep } from './utils/getPacketForStep'
16+
import getInfoText from './utils/getInfoText'
17+
import getLayoutInfoForStage from './utils/getLayerInfoForStage'
18+
import ProtocolOverview from './components/ProtocolOverview'
19+
import EncapsulatedPacket from './components/EncapsulatedPacket'
520

621
function App() {
7-
const [count, setCount] = useState(0)
22+
const [animationState, setAnimationState] = useState(ANIMATION_STATES.IDLE)
23+
const [isPlaying, setIsPlaying] = useState(false)
24+
const [speed, setSpeed] = useState(1)
25+
const [currentStep, setCurrentStep] = useState(0)
26+
const [packets, setPackets] = useState<PacketStep[]>([])
27+
const [layerInfo, setLayerInfo] = useState('')
28+
const [currentPacketStage, setCurrentPacketStage] = useState(0)
29+
const [infoText, setInfoText] = useState(
30+
'Click Play to start the TCP/IP visualization',
31+
)
32+
33+
console.log('currentPacketStage', currentPacketStage)
34+
const animationTimer = useRef<NodeJS.Timeout | null>(null)
35+
const totalSteps = 14 // Total animation steps
36+
37+
// Clear any existing timers when component unmounts
38+
useEffect(() => {
39+
return () => {
40+
if (!animationTimer.current) return
41+
clearTimeout(animationTimer.current)
42+
}
43+
}, [])
44+
45+
// Start packet animation sequence
46+
const startPacketAnimation = useCallback(
47+
(packet: PacketStep) => {
48+
const TOTAL_STAGES = 9 // Total animation stages
49+
const STAGE_DURATION = 2000 / (speed * TOTAL_STAGES)
50+
51+
// Reset current stage
52+
setCurrentPacketStage(0)
53+
54+
// Function to advance to next stage
55+
const advanceStage = (stage: number) => {
56+
if (stage >= TOTAL_STAGES) {
57+
// Animation complete, remove packet
58+
setPackets((prev) => prev.filter((p) => p.id !== packet.id))
59+
return
60+
}
61+
62+
setCurrentPacketStage(stage)
63+
64+
setLayerInfo(getLayoutInfoForStage(stage, packet.from === 'client'))
65+
66+
// Schedule next stage
67+
setTimeout(() => {
68+
advanceStage(stage + 1)
69+
}, STAGE_DURATION)
70+
}
71+
72+
advanceStage(0)
73+
},
74+
[speed],
75+
)
76+
77+
// Create packet animation based on current step
78+
const createPacketForStep = useCallback(
79+
(step: number) => {
80+
console.log('createPacketForStep', step)
81+
const newPacket = getPacketForStep(step)
82+
83+
if (!newPacket) return
84+
setPackets((prev) => [...prev, newPacket])
85+
setCurrentPacketStage(0)
86+
87+
// Start the packet animation sequence
88+
startPacketAnimation(newPacket)
89+
},
90+
[startPacketAnimation],
91+
)
92+
93+
// Handle animation steps
94+
useEffect(() => {
95+
if (!isPlaying) return
96+
97+
const stepDuration = 2000 / speed
98+
99+
const handleStep = () => {
100+
if (currentStep >= totalSteps) {
101+
setIsPlaying(false)
102+
setAnimationState(ANIMATION_STATES.COMPLETE)
103+
return
104+
}
105+
106+
// Update animation state based on current step
107+
if (currentStep < 3) {
108+
setAnimationState(ANIMATION_STATES.HANDSHAKE)
109+
} else if (currentStep < 10) {
110+
setAnimationState(ANIMATION_STATES.DATA_TRANSFER)
111+
} else {
112+
setAnimationState(ANIMATION_STATES.TERMINATION)
113+
}
114+
115+
createPacketForStep(currentStep)
116+
setInfoText(getInfoText(currentStep))
117+
setCurrentStep((prev) => prev + 1)
118+
}
119+
120+
animationTimer.current = setTimeout(handleStep, stepDuration)
121+
122+
return () => {
123+
if (!animationTimer.current) return
124+
clearTimeout(animationTimer.current)
125+
}
126+
}, [isPlaying, currentStep, speed, createPacketForStep])
127+
128+
const togglePlay = () => {
129+
if (currentStep >= totalSteps) return resetAnimation()
130+
131+
setIsPlaying(!isPlaying)
132+
}
133+
134+
const stepForward = () => {
135+
if (currentStep < totalSteps) {
136+
setIsPlaying(false)
137+
138+
// Update animation state based on next step
139+
const nextStep = currentStep
140+
if (nextStep < 3) {
141+
setAnimationState(ANIMATION_STATES.HANDSHAKE)
142+
} else if (nextStep < 10) {
143+
setAnimationState(ANIMATION_STATES.DATA_TRANSFER)
144+
} else {
145+
setAnimationState(ANIMATION_STATES.TERMINATION)
146+
}
147+
148+
createPacketForStep(nextStep)
149+
setInfoText(getInfoText(nextStep))
150+
setCurrentStep((prev) => prev + 1)
151+
}
152+
}
153+
154+
const resetAnimation = () => {
155+
setIsPlaying(false)
156+
setCurrentStep(0)
157+
setPackets([])
158+
setAnimationState(ANIMATION_STATES.IDLE)
159+
setInfoText('Click Play to start the TCP/IP visualization')
160+
setLayerInfo('')
161+
setCurrentPacketStage(0)
162+
}
8163

9164
return (
10-
<>
11-
<div>
12-
<a href="https://vite.dev" target="_blank">
13-
<img src={viteLogo} className="logo" alt="Vite logo" />
14-
</a>
15-
<a href="https://react.dev" target="_blank">
16-
<img src={reactLogo} className="logo react" alt="React logo" />
17-
</a>
18-
</div>
19-
<h1>Vite + React</h1>
20-
<div className="card">
21-
<button onClick={() => setCount((count) => count + 1)}>
22-
count is {count}
23-
</button>
24-
<p>
25-
Edit <code>src/App.tsx</code> and save to test HMR
165+
<main className="flex min-h-screen flex-col items-center justify-between p-4 md:p-8">
166+
<div className="w-full max-w-6xl mx-auto space-y-6">
167+
<h1 className="text-3xl md:text-4xl font-bold text-center">
168+
TCP/IP Protocol Visualizer
169+
</h1>
170+
<p className="text-center text-muted-foreground max-w-2xl mx-auto">
171+
An interactive visualization of how the TCP/IP protocol works,
172+
including the three-way handshake, data transmission, and connection
173+
termination.
26174
</p>
175+
<div className="space-y-6">
176+
<ProtocolOverview />
177+
178+
<div className="relative w-full h-[550px] border rounded-lg bg-background/50 overflow-hidden">
179+
{/* Client and Server */}
180+
<div className="absolute top-0 left-0 w-full h-full flex justify-between p-6">
181+
<div className="flex flex-col items-center">
182+
<Laptop className="w-16 h-16 text-primary" />
183+
<span className="mt-2 font-medium">Client</span>
184+
185+
{/* Client TCP/IP Stack */}
186+
<div className="mt-4 space-y-6">
187+
{TCP_IP_LAYERS.map((layer) => (
188+
<div
189+
key={`client-${layer.name}`}
190+
className="w-40 h-14 flex items-center justify-center rounded border-2 text-xs font-medium"
191+
style={{
192+
borderColor: layer.color,
193+
backgroundColor: `${layer.color}10`,
194+
}}
195+
>
196+
{layer.name}
197+
</div>
198+
))}
199+
</div>
200+
</div>
201+
202+
<div className="flex flex-col items-center">
203+
<Server className="w-16 h-16 text-primary" />
204+
<span className="mt-2 font-medium">Server</span>
205+
206+
{/* Server TCP/IP Stack */}
207+
<div className="mt-4 space-y-6">
208+
{TCP_IP_LAYERS.map((layer) => (
209+
<div
210+
key={`server-${layer.name}`}
211+
className="w-40 h-14 flex items-center justify-center rounded border-2 text-xs font-medium"
212+
style={{
213+
borderColor: layer.color,
214+
backgroundColor: `${layer.color}10`,
215+
}}
216+
>
217+
{layer.name}
218+
</div>
219+
))}
220+
</div>
221+
</div>
222+
</div>
223+
224+
{/* Physical Layer Connection */}
225+
<div className="absolute bottom-24 left-0 w-full flex justify-center">
226+
<div className="w-3/4 border-b-2 border-dashed border-gray-400 relative">
227+
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 text-sm text-gray-500 whitespace-nowrap">
228+
Transmission at Physical Layer
229+
</div>
230+
</div>
231+
</div>
232+
233+
{/* Layer Info */}
234+
{layerInfo && (
235+
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-background/80 backdrop-blur-sm p-2 rounded-lg border shadow-sm">
236+
<p className="text-sm font-medium">{layerInfo}</p>
237+
</div>
238+
)}
239+
240+
{/* Packets */}
241+
{packets.map((packet) => (
242+
<EncapsulatedPacket
243+
key={packet.id}
244+
packet={packet}
245+
stage={currentPacketStage}
246+
/>
247+
))}
248+
249+
{/* Connection Status */}
250+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-background/80 backdrop-blur-sm p-2 rounded-lg border shadow-sm">
251+
<div className="flex items-center space-x-2">
252+
<div
253+
className={`w-3 h-3 rounded-full ${
254+
animationState === ANIMATION_STATES.IDLE
255+
? 'bg-yellow-500'
256+
: animationState === ANIMATION_STATES.COMPLETE
257+
? 'bg-green-500'
258+
: 'bg-blue-500 animate-pulse'
259+
}`}
260+
/>
261+
<span className="text-sm font-medium">
262+
{animationState === ANIMATION_STATES.IDLE && 'Ready'}
263+
{animationState === ANIMATION_STATES.HANDSHAKE &&
264+
'Establishing Connection'}
265+
{animationState === ANIMATION_STATES.DATA_TRANSFER &&
266+
'Transferring Data'}
267+
{animationState === ANIMATION_STATES.TERMINATION &&
268+
'Terminating Connection'}
269+
{animationState === ANIMATION_STATES.COMPLETE &&
270+
'Connection Closed'}
271+
</span>
272+
</div>
273+
</div>
274+
</div>
275+
276+
{/* Info Panel */}
277+
<div className="bg-muted p-4 rounded-lg">
278+
<div className="flex items-start space-x-2">
279+
<Info className="w-5 h-5 mt-0.5 flex-shrink-0" />
280+
<p className="text-sm">{infoText}</p>
281+
</div>
282+
</div>
283+
284+
{/* Controls */}
285+
<div className="flex flex-col sm:flex-row items-center gap-4">
286+
<div className="flex items-center space-x-2">
287+
<Button
288+
variant="outline"
289+
size="icon"
290+
onClick={togglePlay}
291+
aria-label={isPlaying ? 'Pause' : 'Play'}
292+
>
293+
{isPlaying ? (
294+
<Pause className="h-4 w-4" />
295+
) : (
296+
<Play className="h-4 w-4" />
297+
)}
298+
</Button>
299+
300+
<Button
301+
variant="outline"
302+
size="icon"
303+
onClick={stepForward}
304+
disabled={currentStep >= totalSteps}
305+
aria-label="Step Forward"
306+
>
307+
<SkipForward className="h-4 w-4" />
308+
</Button>
309+
310+
<Button
311+
variant="outline"
312+
size="icon"
313+
onClick={resetAnimation}
314+
aria-label="Reset"
315+
>
316+
<RotateCcw className="h-4 w-4" />
317+
</Button>
318+
</div>
319+
320+
<div className="flex items-center space-x-4 flex-1">
321+
<span className="text-sm">Speed:</span>
322+
<Slider
323+
value={[speed]}
324+
min={0.5}
325+
max={3}
326+
step={0.5}
327+
onValueChange={([newSpeed]) => setSpeed(newSpeed)}
328+
className="w-full max-w-xs"
329+
/>
330+
<span className="text-sm w-8">{speed}x</span>
331+
</div>
332+
333+
<div className="flex items-center space-x-2">
334+
<span className="text-sm whitespace-nowrap">
335+
Step: {currentStep}/{totalSteps}
336+
</span>
337+
</div>
338+
</div>
339+
</div>
27340
</div>
28-
<p className="read-the-docs">
29-
Click on the Vite and React logos to learn more
30-
</p>
31-
</>
341+
</main>
32342
)
33343
}
34344

0 commit comments

Comments
 (0)