Skip to content

Commit c77f72f

Browse files
committed
feat: refactor App component and add ControlsPanel for improved animation control
1 parent 750a0e3 commit c77f72f

File tree

3 files changed

+246
-247
lines changed

3 files changed

+246
-247
lines changed

src/App.tsx

Lines changed: 96 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -1,171 +1,111 @@
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'
132
import { ANIMATION_STATES, TCP_IP_LAYERS } from './data'
143
import getPacketForStep, { type PacketStep } from './utils/getPacketForStep'
154
import getInfoText, { TOTAL_STEPS } from './utils/getInfoText'
165
import getLayoutInfoForStage from './utils/getLayerInfoForStage'
176
import ProtocolOverview, { type Tab } from './components/ProtocolOverview'
187
import EncapsulatedPacket from './components/EncapsulatedPacket'
198
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'
2212

2313
function App() {
2414
const [activeTab, setActiveTab] = useState<Tab>('overview')
2515
const [animationState, setAnimationState] = useState(ANIMATION_STATES.IDLE)
2616
const [isPlaying, setIsPlaying] = useState(false)
2717
const [speed, setSpeed] = useState(0.5)
2818
const [currentStep, setCurrentStep] = useState(0)
29-
const [packets, setPackets] = useState<PacketStep[]>([])
19+
const [currentPacket, setCurrentPacket] = useState<PacketStep | null>(null)
3020
const [layerInfo, setLayerInfo] = useState('')
31-
const [currentPacketStage, setCurrentPacketStage] = useState(0)
3221
const [infoText, setInfoText] = useState(
3322
'Click Play to start the TCP/IP visualization',
3423
)
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
4333
}, [])
4434

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
11442
}
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)
12449
}
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])
13153

132-
const togglePlay = () => {
54+
const handleTogglePlay = () => {
13355
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()
15161
}
62+
return next
63+
})
64+
}
15265

153-
createPacketForStep(currentStep)
66+
const handleStepForward = () => {
67+
if (currentStep < TOTAL_STEPS && !isPlaying) {
68+
setPendingStep(true)
15469
setInfoText(getInfoText(currentStep))
155-
setCurrentStep((prev) => prev + 1)
70+
createPacketForStep(currentStep)
15671
}
15772
}
15873

159-
const resetAnimation = () => {
74+
const handleResetAnimation = () => {
16075
setIsPlaying(false)
16176
setCurrentStep(0)
162-
setPackets([])
77+
setCurrentPacket(null)
16378
setAnimationState(ANIMATION_STATES.IDLE)
16479
setInfoText('Click Play to start the TCP/IP visualization')
16580
setLayerInfo('')
166-
setCurrentPacketStage(0)
81+
setPendingStep(false)
16782
}
16883

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+
169109
return (
170110
<main className="flex min-h-screen flex-col items-center justify-between p-4 md:p-8">
171111
<div className="w-full max-w-6xl mx-auto space-y-6">
@@ -190,7 +130,6 @@ function App() {
190130
<div className="flex flex-col items-center">
191131
<Laptop className="w-16 h-16 text-primary" />
192132
<span className="mt-2 font-medium">Client</span>
193-
194133
{/* Client TCP/IP Stack */}
195134
<div className="mt-4 space-y-6">
196135
{TCP_IP_LAYERS.map((layer) => (
@@ -207,11 +146,9 @@ function App() {
207146
))}
208147
</div>
209148
</div>
210-
211149
<div className="flex flex-col items-center">
212150
<Server className="w-16 h-16 text-primary" />
213151
<span className="mt-2 font-medium">Server</span>
214-
215152
{/* Server TCP/IP Stack */}
216153
<div className="mt-4 space-y-6">
217154
{TCP_IP_LAYERS.map((layer) => (
@@ -229,100 +166,49 @@ function App() {
229166
</div>
230167
</div>
231168
</div>
232-
233-
{/* Physical Layer Connection */}
234169
<div className="absolute bottom-24 left-0 w-full flex justify-center">
235170
<div className="w-3/4 border-b-2 border-dashed border-gray-400 relative">
236171
<div className="absolute -bottom-6 left-1/2 transform -translate-x-1/2 text-sm text-gray-500 whitespace-nowrap">
237172
Transmission at Physical Layer
238173
</div>
239174
</div>
240175
</div>
241-
242-
{/* Layer Info */}
243176
{layerInfo && (
244177
<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">
245178
<p className="text-sm font-medium">{layerInfo}</p>
246179
</div>
247180
)}
248-
249-
{/* Packets */}
250-
{packets.map((packet) => (
181+
{currentPacket && (
251182
<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}
255196
/>
256-
))}
257-
197+
)}
258198
<ConnectionStatus animationState={animationState} />
259199
</div>
260200

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} />
294202

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+
/>
326212
</div>
327213
</div>
328214
</main>

0 commit comments

Comments
 (0)