1
- import { useState , useEffect , useRef , useCallback , useMemo } from 'react'
2
- import {
3
- Info ,
4
- Laptop ,
5
- Pause ,
6
- Play ,
7
- RotateCcw ,
8
- Server ,
9
- SkipForward ,
10
- } from 'lucide-react'
11
- import { Button } from '@/components/ui/button'
12
- import { Slider } from '@/components/ui/slider'
1
+ import { useState , useCallback , useMemo } from 'react'
13
2
import { ANIMATION_STATES , TCP_IP_LAYERS } from './data'
14
3
import getPacketForStep , { type PacketStep } from './utils/getPacketForStep'
15
4
import getInfoText , { TOTAL_STEPS } from './utils/getInfoText'
16
5
import getLayoutInfoForStage from './utils/getLayerInfoForStage'
17
6
import ProtocolOverview , { type Tab } from './components/ProtocolOverview'
18
7
import EncapsulatedPacket from './components/EncapsulatedPacket'
19
8
import ConnectionStatus from './components/ConnectionStatus'
20
-
21
- const BASE_DURATION = 3000
9
+ import InfoPanel from './components/InfoPanel'
10
+ import ControlsPanel from './components/ControlsPanel'
11
+ import { Laptop , Server } from 'lucide-react'
22
12
23
13
function App ( ) {
24
14
const [ activeTab , setActiveTab ] = useState < Tab > ( 'overview' )
25
15
const [ animationState , setAnimationState ] = useState ( ANIMATION_STATES . IDLE )
26
16
const [ isPlaying , setIsPlaying ] = useState ( false )
27
17
const [ speed , setSpeed ] = useState ( 0.5 )
28
18
const [ currentStep , setCurrentStep ] = useState ( 0 )
29
- const [ packets , setPackets ] = useState < PacketStep [ ] > ( [ ] )
19
+ const [ currentPacket , setCurrentPacket ] = useState < PacketStep | null > ( null )
30
20
const [ layerInfo , setLayerInfo ] = useState ( '' )
31
- const [ currentPacketStage , setCurrentPacketStage ] = useState ( 0 )
32
21
const [ infoText , setInfoText ] = useState (
33
22
'Click Play to start the TCP/IP visualization' ,
34
23
)
35
-
36
- const animationTimer = useRef < NodeJS . Timeout | null > ( null )
37
- // Clear any existing timers on unmount
38
- useEffect ( ( ) => {
39
- return ( ) => {
40
- if ( ! animationTimer . current ) return
41
- clearTimeout ( animationTimer . current )
42
- }
24
+ const [ pendingStep , setPendingStep ] = useState ( false )
25
+
26
+ // Create a new packet for the current step
27
+ const createPacketForStep = useCallback ( ( step : number ) => {
28
+ const newPacket = getPacketForStep ( step )
29
+ if ( ! newPacket ) return null
30
+ setCurrentPacket ( newPacket )
31
+ setLayerInfo ( getLayoutInfoForStage ( 0 , newPacket . from === 'client' ) )
32
+ return newPacket
43
33
} , [ ] )
44
34
45
- // Start packet animation sequence
46
- const startPacketAnimation = useCallback (
47
- ( packet : PacketStep ) => {
48
- const TOTAL_STAGES = 9
49
- const STAGE_DURATION = BASE_DURATION / ( speed * TOTAL_STAGES )
50
-
51
- // Reset current stage
52
- setCurrentPacketStage ( 0 )
53
-
54
- // Recursively advance through stages
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
- const createPacketForStep = useCallback (
78
- ( step : number ) => {
79
- const newPacket = getPacketForStep ( step )
80
-
81
- if ( ! newPacket ) return
82
-
83
- setPackets ( ( prev ) => [ ...prev , newPacket ] )
84
- setCurrentPacketStage ( 0 )
85
- startPacketAnimation ( newPacket )
86
- } ,
87
- [ startPacketAnimation ] ,
88
- )
89
-
90
- // Handle animation steps
91
- useEffect ( ( ) => {
92
- if ( ! isPlaying ) return
93
-
94
- const stepDuration = BASE_DURATION / speed
95
-
96
- const handleStep = ( ) => {
97
- if ( currentStep >= TOTAL_STEPS ) {
98
- setIsPlaying ( false )
99
- setAnimationState ( ANIMATION_STATES . COMPLETE )
100
- return
101
- }
102
-
103
- if ( currentStep < 3 ) {
104
- setAnimationState ( ANIMATION_STATES . HANDSHAKE )
105
- } else if ( currentStep < 10 ) {
106
- setAnimationState ( ANIMATION_STATES . DATA_TRANSFER )
107
- } else {
108
- setAnimationState ( ANIMATION_STATES . TERMINATION )
109
- }
110
-
111
- createPacketForStep ( currentStep )
112
- setInfoText ( getInfoText ( currentStep ) )
113
- setCurrentStep ( ( prev ) => prev + 1 )
35
+ // Start or continue animation
36
+ const startAnimation = useCallback ( ( ) => {
37
+ if ( currentStep >= TOTAL_STEPS ) {
38
+ setIsPlaying ( false )
39
+ setAnimationState ( ANIMATION_STATES . COMPLETE )
40
+ setCurrentPacket ( null )
41
+ return
114
42
}
115
-
116
- animationTimer . current = setTimeout (
117
- handleStep ,
118
- currentPacketStage === 0 ? stepDuration / 2 : stepDuration ,
119
- )
120
-
121
- return ( ) => {
122
- if ( ! animationTimer . current ) return
123
- clearTimeout ( animationTimer . current )
43
+ if ( currentStep < 3 ) {
44
+ setAnimationState ( ANIMATION_STATES . HANDSHAKE )
45
+ } else if ( currentStep < 10 ) {
46
+ setAnimationState ( ANIMATION_STATES . DATA_TRANSFER )
47
+ } else {
48
+ setAnimationState ( ANIMATION_STATES . TERMINATION )
124
49
}
125
- } , [ isPlaying , currentStep , speed , currentPacketStage , createPacketForStep ] )
126
-
127
- const activePacketType = useMemo (
128
- ( ) => ( packets . length > 0 ? packets [ packets . length - 1 ] . type . name : null ) ,
129
- [ packets ] ,
130
- )
50
+ setInfoText ( getInfoText ( currentStep ) )
51
+ createPacketForStep ( currentStep )
52
+ } , [ currentStep , createPacketForStep ] )
131
53
132
- const togglePlay = ( ) => {
54
+ const handleTogglePlay = ( ) => {
133
55
if ( ! isPlaying ) setActiveTab ( 'packets' )
134
-
135
- if ( currentStep >= TOTAL_STEPS ) return resetAnimation ( )
136
-
137
- setIsPlaying ( ! isPlaying )
138
- }
139
-
140
- const stepForward = ( ) => {
141
- if ( currentStep < TOTAL_STEPS ) {
142
- setIsPlaying ( false )
143
-
144
- // Update animation state based on next step
145
- if ( currentStep < 3 ) {
146
- setAnimationState ( ANIMATION_STATES . HANDSHAKE )
147
- } else if ( currentStep < 10 ) {
148
- setAnimationState ( ANIMATION_STATES . DATA_TRANSFER )
149
- } else {
150
- setAnimationState ( ANIMATION_STATES . TERMINATION )
56
+ if ( currentStep >= TOTAL_STEPS ) return handleResetAnimation ( )
57
+ setIsPlaying ( ( prev ) => {
58
+ const next = ! prev
59
+ if ( next && ! currentPacket ) {
60
+ startAnimation ( )
151
61
}
62
+ return next
63
+ } )
64
+ }
152
65
153
- createPacketForStep ( currentStep )
66
+ const handleStepForward = ( ) => {
67
+ if ( currentStep < TOTAL_STEPS && ! isPlaying ) {
68
+ setPendingStep ( true )
154
69
setInfoText ( getInfoText ( currentStep ) )
155
- setCurrentStep ( ( prev ) => prev + 1 )
70
+ createPacketForStep ( currentStep )
156
71
}
157
72
}
158
73
159
- const resetAnimation = ( ) => {
74
+ const handleResetAnimation = ( ) => {
160
75
setIsPlaying ( false )
161
76
setCurrentStep ( 0 )
162
- setPackets ( [ ] )
77
+ setCurrentPacket ( null )
163
78
setAnimationState ( ANIMATION_STATES . IDLE )
164
79
setInfoText ( 'Click Play to start the TCP/IP visualization' )
165
80
setLayerInfo ( '' )
166
- setCurrentPacketStage ( 0 )
81
+ setPendingStep ( false )
167
82
}
168
83
84
+ const handlePacketComplete = useCallback ( ( ) => {
85
+ setCurrentPacket ( null )
86
+ if ( isPlaying || pendingStep ) {
87
+ setCurrentStep ( ( prev ) => prev + 1 )
88
+ setPendingStep ( false )
89
+ }
90
+ } , [ isPlaying , pendingStep ] )
91
+
92
+ // When currentStep changes and playing, start next animation
93
+ // (or after handleStepForward)
94
+ useMemo ( ( ) => {
95
+ if (
96
+ ( isPlaying || pendingStep ) &&
97
+ ! currentPacket &&
98
+ currentStep < TOTAL_STEPS
99
+ ) {
100
+ startAnimation ( )
101
+ }
102
+ } , [ isPlaying , pendingStep , currentPacket , currentStep , startAnimation ] )
103
+
104
+ const activePacketType = useMemo (
105
+ ( ) => ( currentPacket ? currentPacket . type . name : null ) ,
106
+ [ currentPacket ] ,
107
+ )
108
+
169
109
return (
170
110
< main className = "flex min-h-screen flex-col items-center justify-between p-4 md:p-8" >
171
111
< div className = "w-full max-w-6xl mx-auto space-y-6" >
@@ -190,7 +130,6 @@ function App() {
190
130
< div className = "flex flex-col items-center" >
191
131
< Laptop className = "w-16 h-16 text-primary" />
192
132
< span className = "mt-2 font-medium" > Client</ span >
193
-
194
133
{ /* Client TCP/IP Stack */ }
195
134
< div className = "mt-4 space-y-6" >
196
135
{ TCP_IP_LAYERS . map ( ( layer ) => (
@@ -207,11 +146,9 @@ function App() {
207
146
) ) }
208
147
</ div >
209
148
</ div >
210
-
211
149
< div className = "flex flex-col items-center" >
212
150
< Server className = "w-16 h-16 text-primary" />
213
151
< span className = "mt-2 font-medium" > Server</ span >
214
-
215
152
{ /* Server TCP/IP Stack */ }
216
153
< div className = "mt-4 space-y-6" >
217
154
{ TCP_IP_LAYERS . map ( ( layer ) => (
@@ -229,100 +166,49 @@ function App() {
229
166
</ div >
230
167
</ div >
231
168
</ div >
232
-
233
- { /* Physical Layer Connection */ }
234
169
< div className = "absolute bottom-24 left-0 w-full flex justify-center" >
235
170
< div className = "w-3/4 border-b-2 border-dashed border-gray-400 relative" >
236
171
< div className = "absolute -bottom-6 left-1/2 transform -translate-x-1/2 text-sm text-gray-500 whitespace-nowrap" >
237
172
Transmission at Physical Layer
238
173
</ div >
239
174
</ div >
240
175
</ div >
241
-
242
- { /* Layer Info */ }
243
176
{ layerInfo && (
244
177
< 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" >
245
178
< p className = "text-sm font-medium" > { layerInfo } </ p >
246
179
</ div >
247
180
) }
248
-
249
- { /* Packets */ }
250
- { packets . map ( ( packet ) => (
181
+ { currentPacket && (
251
182
< EncapsulatedPacket
252
- key = { packet . id }
253
- packet = { packet }
254
- stage = { currentPacketStage }
183
+ key = { currentPacket . id }
184
+ packet = { currentPacket }
185
+ isPlaying = { isPlaying || pendingStep }
186
+ speed = { speed }
187
+ onStageChange = { ( stage ) => {
188
+ setLayerInfo (
189
+ getLayoutInfoForStage (
190
+ stage ,
191
+ currentPacket . from === 'client' ,
192
+ ) ,
193
+ )
194
+ } }
195
+ onComplete = { handlePacketComplete }
255
196
/>
256
- ) ) }
257
-
197
+ ) }
258
198
< ConnectionStatus animationState = { animationState } />
259
199
</ div >
260
200
261
- { /* Info Panel */ }
262
- < div className = "bg-muted p-4 rounded-lg" >
263
- < div className = "flex items-start space-x-2" >
264
- < Info className = "w-5 h-5 mt-0.5 flex-shrink-0" />
265
- < p className = "text-sm" > { infoText } </ p >
266
- </ div >
267
- </ div >
268
-
269
- { /* Controls */ }
270
- < div className = "flex flex-col sm:flex-row items-center gap-4" >
271
- < div className = "flex items-center space-x-2" >
272
- < Button
273
- variant = "outline"
274
- size = "icon"
275
- onClick = { togglePlay }
276
- aria-label = { isPlaying ? 'Pause' : 'Play' }
277
- >
278
- { isPlaying ? (
279
- < Pause className = "h-4 w-4" />
280
- ) : (
281
- < Play className = "h-4 w-4" />
282
- ) }
283
- </ Button >
284
-
285
- < Button
286
- variant = "outline"
287
- size = "icon"
288
- onClick = { stepForward }
289
- disabled = { currentStep >= TOTAL_STEPS }
290
- aria-label = "Step Forward"
291
- >
292
- < SkipForward className = "h-4 w-4" />
293
- </ Button >
201
+ < InfoPanel text = { infoText } />
294
202
295
- < Button
296
- variant = "outline"
297
- size = "icon"
298
- onClick = { resetAnimation }
299
- aria-label = "Reset"
300
- >
301
- < RotateCcw className = "h-4 w-4" />
302
- </ Button >
303
- </ div >
304
-
305
-
306
- { /* TODO: Fix speed change during animation */ }
307
- < div className = "flex items-center space-x-4 flex-1" >
308
- < span className = "text-sm" > Speed:</ span >
309
- < Slider
310
- value = { [ speed ] }
311
- min = { 0.1 }
312
- max = { 1 }
313
- step = { 0.1 }
314
- onValueChange = { ( [ newSpeed ] ) => setSpeed ( newSpeed ) }
315
- className = "w-full max-w-xs"
316
- />
317
- < span className = "text-sm w-8" > { speed } x</ span >
318
- </ div >
319
-
320
- < div className = "flex items-center space-x-2" >
321
- < span className = "text-sm whitespace-nowrap" >
322
- Step: { currentStep } /{ TOTAL_STEPS }
323
- </ span >
324
- </ div >
325
- </ div >
203
+ < ControlsPanel
204
+ isPlaying = { isPlaying }
205
+ currentStep = { currentStep }
206
+ speed = { speed }
207
+ onTogglePlay = { handleTogglePlay }
208
+ onChangeSpeed = { setSpeed }
209
+ onStepForward = { handleStepForward }
210
+ onResetAnimation = { handleResetAnimation }
211
+ />
326
212
</ div >
327
213
</ div >
328
214
</ main >
0 commit comments