fix: restore automation lanes and effects sections in two-column layout

Restored the automation lane and effects sections that were removed during
the Track component refactoring. These sections now render as full-width
rows below each track, spanning across both the controls and waveforms columns.

Changes:
- Created TrackExtensions component for effects section rendering
- Added automation lane rendering in TrackList after each track waveform
- Added placeholder spacers in left controls column to maintain alignment
- Effects section shows collapsible device rack with mini preview when collapsed
- Automation lanes render when track.automation.showAutomation is true
- Import dialog moved to waveform-only rendering mode

The automation and effects sections are now properly unfoldable again.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-19 11:24:38 +01:00
parent 90e66e8bef
commit 7a7d6891cd
3 changed files with 513 additions and 248 deletions

View File

@@ -816,90 +816,101 @@ export function Track({
// Render only waveform // Render only waveform
if (renderWaveformOnly) { if (renderWaveformOnly) {
return ( return (
<div <>
className={cn(
"relative bg-waveform-bg border-b transition-all duration-200",
isSelected && "bg-primary/5",
)}
style={{ height: trackHeight }}
>
{/* Inner container with dynamic width */}
<div <div
className="relative h-full" className={cn(
style={{ "relative bg-waveform-bg border-b transition-all duration-200",
minWidth: isSelected && "bg-primary/5",
track.audioBuffer && zoom > 1 )}
? `${duration * zoom * 100}px` style={{ height: trackHeight }}
: "100%",
}}
> >
{/* Delete Button - Top Right Overlay */} {/* Inner container with dynamic width */}
<button <div
onClick={(e) => { className="relative h-full"
e.stopPropagation(); style={{
onRemove(); minWidth:
track.audioBuffer && zoom > 1
? `${duration * zoom * 100}px`
: "100%",
}} }}
className={cn(
"absolute top-2 right-2 z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
"bg-card/80 hover:bg-destructive/90 text-muted-foreground hover:text-white",
"border border-border/50 hover:border-destructive",
"backdrop-blur-sm shadow-sm hover:shadow-md",
)}
title="Remove track"
> >
<Trash2 className="h-3 w-3" /> {/* Delete Button - Top Right Overlay */}
</button> <button
onClick={(e) => {
e.stopPropagation();
onRemove();
}}
className={cn(
"absolute top-2 right-2 z-20 h-6 w-6 rounded flex items-center justify-center transition-all",
"bg-card/80 hover:bg-destructive/90 text-muted-foreground hover:text-white",
"border border-border/50 hover:border-destructive",
"backdrop-blur-sm shadow-sm hover:shadow-md",
)}
title="Remove track"
>
<Trash2 className="h-3 w-3" />
</button>
{track.audioBuffer ? ( {track.audioBuffer ? (
<>
{/* Waveform Canvas */}
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full cursor-pointer"
onMouseDown={handleCanvasMouseDown}
onMouseMove={handleCanvasMouseMove}
onMouseUp={handleCanvasMouseUp}
/>
</>
) : (
!track.collapsed && (
<> <>
<div {/* Waveform Canvas */}
className={cn( <canvas
"absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer group", ref={canvasRef}
isDragging className="absolute inset-0 w-full h-full cursor-pointer"
? "bg-primary/20 text-primary border-2 border-primary border-dashed" onMouseDown={handleCanvasMouseDown}
: "hover:bg-accent/50", onMouseMove={handleCanvasMouseMove}
)} onMouseUp={handleCanvasMouseUp}
onClick={(e) => {
e.stopPropagation();
handleLoadAudioClick();
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="h-6 w-6 mb-2 opacity-50 group-hover:opacity-100" />
<p>
{isDragging
? "Drop audio file here"
: "Click to load audio file"}
</p>
<p className="text-xs opacity-75 mt-1">or drag & drop</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={handleFileChange}
className="hidden"
/> />
</> </>
) ) : (
)} !track.collapsed && (
</div>{" "} <>
{/* Close inner container with minWidth */} <div
</div> className={cn(
"absolute inset-0 flex flex-col items-center justify-center text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer group",
isDragging
? "bg-primary/20 text-primary border-2 border-primary border-dashed"
: "hover:bg-accent/50",
)}
onClick={(e) => {
e.stopPropagation();
handleLoadAudioClick();
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="h-6 w-6 mb-2 opacity-50 group-hover:opacity-100" />
<p>
{isDragging
? "Drop audio file here"
: "Click to load audio file"}
</p>
<p className="text-xs opacity-75 mt-1">or drag & drop</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="audio/*"
onChange={handleFileChange}
className="hidden"
/>
</>
)
)}
</div>
</div>
{/* Import Dialog */}
<ImportDialog
open={showImportDialog}
onClose={handleImportCancel}
onImport={handleImport}
fileName={pendingFile?.name}
sampleRate={fileMetadata.sampleRate}
channels={fileMetadata.channels}
/>
</>
); );
} }

View File

@@ -0,0 +1,128 @@
'use client';
import * as React from 'react';
import { Plus, ChevronDown, ChevronRight } from 'lucide-react';
import type { Track as TrackType } from '@/types/track';
import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils/cn';
import { EffectDevice } from '@/components/effects/EffectDevice';
import { EffectBrowser } from '@/components/effects/EffectBrowser';
import type { EffectType } from '@/lib/audio/effects/chain';
export interface TrackExtensionsProps {
track: TrackType;
onUpdateTrack: (trackId: string, updates: Partial<TrackType>) => void;
onToggleEffect?: (effectId: string) => void;
onRemoveEffect?: (effectId: string) => void;
onUpdateEffect?: (effectId: string, parameters: any) => void;
onAddEffect?: (effectType: EffectType) => void;
}
export function TrackExtensions({
track,
onUpdateTrack,
onToggleEffect,
onRemoveEffect,
onUpdateEffect,
onAddEffect,
}: TrackExtensionsProps) {
const [effectBrowserOpen, setEffectBrowserOpen] = React.useState(false);
// Don't render if track is collapsed
if (track.collapsed) {
return null;
}
return (
<>
{/* Effects Section (Collapsible, Full Width) */}
<div className="bg-muted/50 border-b border-border/50">
{/* Effects Header - clickable to toggle */}
<div
className="flex items-center gap-2 px-3 py-1.5 cursor-pointer hover:bg-accent/30 transition-colors"
onClick={() => {
onUpdateTrack(track.id, {
showEffects: !track.showEffects,
});
}}
>
{track.showEffects ? (
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)}
{/* Show mini effect chain when collapsed */}
{!track.showEffects && track.effectChain.effects.length > 0 ? (
<div className="flex-1 flex items-center gap-1 overflow-x-auto custom-scrollbar">
{track.effectChain.effects.map((effect) => (
<div
key={effect.id}
className={cn(
'px-2 py-0.5 rounded text-[10px] font-medium flex-shrink-0',
effect.enabled
? 'bg-primary/20 text-primary border border-primary/30'
: 'bg-muted/30 text-muted-foreground border border-border'
)}
>
{effect.name}
</div>
))}
</div>
) : (
<span className="text-xs font-medium text-muted-foreground">
Devices ({track.effectChain.effects.length})
</span>
)}
<Button
variant="ghost"
size="icon-sm"
onClick={(e) => {
e.stopPropagation();
setEffectBrowserOpen(true);
}}
title="Add effect"
className="h-5 w-5 flex-shrink-0"
>
<Plus className="h-3 w-3" />
</Button>
</div>
{/* Horizontal scrolling device rack - expanded state */}
{track.showEffects && (
<div className="h-48 overflow-x-auto custom-scrollbar bg-muted/70">
<div className="flex h-full">
{track.effectChain.effects.length === 0 ? (
<div className="text-xs text-muted-foreground text-center py-8 w-full">
No devices. Click + to add an effect.
</div>
) : (
track.effectChain.effects.map((effect) => (
<EffectDevice
key={effect.id}
effect={effect}
onToggleEnabled={() => onToggleEffect?.(effect.id)}
onRemove={() => onRemoveEffect?.(effect.id)}
onUpdateParameters={(params) => onUpdateEffect?.(effect.id, params)}
/>
))
)}
</div>
</div>
)}
</div>
{/* Effect Browser Dialog */}
<EffectBrowser
open={effectBrowserOpen}
onClose={() => setEffectBrowserOpen(false)}
onSelectEffect={(effectType) => {
if (onAddEffect) {
onAddEffect(effectType);
}
}}
/>
</>
);
}

View File

@@ -4,9 +4,13 @@ import * as React from 'react';
import { Plus, Upload } from 'lucide-react'; import { Plus, Upload } from 'lucide-react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Track } from './Track'; import { Track } from './Track';
import { TrackExtensions } from './TrackExtensions';
import { ImportTrackDialog } from './ImportTrackDialog'; import { ImportTrackDialog } from './ImportTrackDialog';
import type { Track as TrackType } from '@/types/track'; import type { Track as TrackType } from '@/types/track';
import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain'; import { createEffect, type EffectType, EFFECT_NAMES } from '@/lib/audio/effects/chain';
import { AutomationLane } from '@/components/automation/AutomationLane';
import type { AutomationPoint as AutomationPointType } from '@/types/automation';
import { createAutomationPoint } from '@/lib/audio/automation/utils';
export interface TrackListProps { export interface TrackListProps {
tracks: TrackType[]; tracks: TrackType[];
@@ -102,91 +106,103 @@ export function TrackList({
{/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */} {/* Left Column: Track Controls (Fixed Width, No Scroll - synced with waveforms) */}
<div ref={controlsScrollRef} className="w-48 flex-shrink-0 overflow-hidden pb-3 border-r border-border"> <div ref={controlsScrollRef} className="w-48 flex-shrink-0 overflow-hidden pb-3 border-r border-border">
{tracks.map((track) => ( {tracks.map((track) => (
<Track <React.Fragment key={track.id}>
key={track.id} {/* Track Controls */}
track={track} <Track
zoom={zoom} track={track}
currentTime={currentTime} zoom={zoom}
duration={duration} currentTime={currentTime}
isSelected={selectedTrackId === track.id} duration={duration}
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined} isSelected={selectedTrackId === track.id}
onToggleMute={() => onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
onUpdateTrack(track.id, { mute: !track.mute }) onToggleMute={() =>
} onUpdateTrack(track.id, { mute: !track.mute })
onToggleSolo={() => }
onUpdateTrack(track.id, { solo: !track.solo }) onToggleSolo={() =>
} onUpdateTrack(track.id, { solo: !track.solo })
onToggleCollapse={() => }
onUpdateTrack(track.id, { collapsed: !track.collapsed }) onToggleCollapse={() =>
} onUpdateTrack(track.id, { collapsed: !track.collapsed })
onVolumeChange={(volume) => }
onUpdateTrack(track.id, { volume }) onVolumeChange={(volume) =>
} onUpdateTrack(track.id, { volume })
onPanChange={(pan) => }
onUpdateTrack(track.id, { pan }) onPanChange={(pan) =>
} onUpdateTrack(track.id, { pan })
onRemove={() => onRemoveTrack(track.id)} }
onNameChange={(name) => onRemove={() => onRemoveTrack(track.id)}
onUpdateTrack(track.id, { name }) onNameChange={(name) =>
} onUpdateTrack(track.id, { name })
onUpdateTrack={onUpdateTrack} }
onSeek={onSeek} onUpdateTrack={onUpdateTrack}
onLoadAudio={(buffer) => onSeek={onSeek}
onUpdateTrack(track.id, { audioBuffer: buffer }) onLoadAudio={(buffer) =>
} onUpdateTrack(track.id, { audioBuffer: buffer })
onToggleEffect={(effectId) => { }
const updatedChain = { onToggleEffect={(effectId) => {
...track.effectChain, const updatedChain = {
effects: track.effectChain.effects.map((e) => ...track.effectChain,
e.id === effectId ? { ...e, enabled: !e.enabled } : e effects: track.effectChain.effects.map((e) =>
), e.id === effectId ? { ...e, enabled: !e.enabled } : e
}; ),
onUpdateTrack(track.id, { effectChain: updatedChain }); };
}} onUpdateTrack(track.id, { effectChain: updatedChain });
onRemoveEffect={(effectId) => { }}
const updatedChain = { onRemoveEffect={(effectId) => {
...track.effectChain, const updatedChain = {
effects: track.effectChain.effects.filter((e) => e.id !== effectId), ...track.effectChain,
}; effects: track.effectChain.effects.filter((e) => e.id !== effectId),
onUpdateTrack(track.id, { effectChain: updatedChain }); };
}} onUpdateTrack(track.id, { effectChain: updatedChain });
onUpdateEffect={(effectId, parameters) => { }}
const updatedChain = { onUpdateEffect={(effectId, parameters) => {
...track.effectChain, const updatedChain = {
effects: track.effectChain.effects.map((e) => ...track.effectChain,
e.id === effectId ? { ...e, parameters } : e effects: track.effectChain.effects.map((e) =>
), e.id === effectId ? { ...e, parameters } : e
}; ),
onUpdateTrack(track.id, { effectChain: updatedChain }); };
}} onUpdateTrack(track.id, { effectChain: updatedChain });
onAddEffect={(effectType) => { }}
const newEffect = createEffect( onAddEffect={(effectType) => {
effectType, const newEffect = createEffect(
EFFECT_NAMES[effectType] effectType,
); EFFECT_NAMES[effectType]
const updatedChain = { );
...track.effectChain, const updatedChain = {
effects: [...track.effectChain.effects, newEffect], ...track.effectChain,
}; effects: [...track.effectChain.effects, newEffect],
onUpdateTrack(track.id, { effectChain: updatedChain }); };
}} onUpdateTrack(track.id, { effectChain: updatedChain });
onSelectionChange={ }}
onSelectionChange onSelectionChange={
? (selection) => onSelectionChange(track.id, selection) onSelectionChange
: undefined ? (selection) => onSelectionChange(track.id, selection)
} : undefined
onToggleRecordEnable={ }
onToggleRecordEnable onToggleRecordEnable={
? () => onToggleRecordEnable(track.id) onToggleRecordEnable
: undefined ? () => onToggleRecordEnable(track.id)
} : undefined
isRecording={recordingTrackId === track.id} }
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} isRecording={recordingTrackId === track.id}
playbackLevel={trackLevels[track.id] || 0} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
onParameterTouched={onParameterTouched} playbackLevel={trackLevels[track.id] || 0}
isPlaying={isPlaying} onParameterTouched={onParameterTouched}
renderControlsOnly={true} isPlaying={isPlaying}
/> renderControlsOnly={true}
/>
{/* Spacer for Automation Lane */}
{!track.collapsed && track.automation?.showAutomation && (
<div className="h-32 bg-muted/30 border-b border-border" />
)}
{/* Spacer for Effects Section */}
{!track.collapsed && (
<div className={track.showEffects ? "h-64 bg-muted/50 border-b border-border/50" : "h-8 bg-muted/50 border-b border-border/50"} />
)}
</React.Fragment>
))} ))}
</div> </div>
@@ -196,93 +212,203 @@ export function TrackList({
onScroll={handleWaveformScroll} onScroll={handleWaveformScroll}
className="flex-1 overflow-auto custom-scrollbar" className="flex-1 overflow-auto custom-scrollbar"
> >
{tracks.map((track) => ( <div className="flex flex-col">
<Track {tracks.map((track) => (
key={track.id} <React.Fragment key={track.id}>
track={track} {/* Track Waveform Row */}
zoom={zoom} <Track
currentTime={currentTime} track={track}
duration={duration} zoom={zoom}
isSelected={selectedTrackId === track.id} currentTime={currentTime}
onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined} duration={duration}
onToggleMute={() => isSelected={selectedTrackId === track.id}
onUpdateTrack(track.id, { mute: !track.mute }) onSelect={onSelectTrack ? () => onSelectTrack(track.id) : undefined}
} onToggleMute={() =>
onToggleSolo={() => onUpdateTrack(track.id, { mute: !track.mute })
onUpdateTrack(track.id, { solo: !track.solo }) }
} onToggleSolo={() =>
onToggleCollapse={() => onUpdateTrack(track.id, { solo: !track.solo })
onUpdateTrack(track.id, { collapsed: !track.collapsed }) }
} onToggleCollapse={() =>
onVolumeChange={(volume) => onUpdateTrack(track.id, { collapsed: !track.collapsed })
onUpdateTrack(track.id, { volume }) }
} onVolumeChange={(volume) =>
onPanChange={(pan) => onUpdateTrack(track.id, { volume })
onUpdateTrack(track.id, { pan }) }
} onPanChange={(pan) =>
onRemove={() => onRemoveTrack(track.id)} onUpdateTrack(track.id, { pan })
onNameChange={(name) => }
onUpdateTrack(track.id, { name }) onRemove={() => onRemoveTrack(track.id)}
} onNameChange={(name) =>
onUpdateTrack={onUpdateTrack} onUpdateTrack(track.id, { name })
onSeek={onSeek} }
onLoadAudio={(buffer) => onUpdateTrack={onUpdateTrack}
onUpdateTrack(track.id, { audioBuffer: buffer }) onSeek={onSeek}
} onLoadAudio={(buffer) =>
onToggleEffect={(effectId) => { onUpdateTrack(track.id, { audioBuffer: buffer })
const updatedChain = { }
...track.effectChain, onToggleEffect={(effectId) => {
effects: track.effectChain.effects.map((e) => const updatedChain = {
e.id === effectId ? { ...e, enabled: !e.enabled } : e ...track.effectChain,
), effects: track.effectChain.effects.map((e) =>
}; e.id === effectId ? { ...e, enabled: !e.enabled } : e
onUpdateTrack(track.id, { effectChain: updatedChain }); ),
}} };
onRemoveEffect={(effectId) => { onUpdateTrack(track.id, { effectChain: updatedChain });
const updatedChain = { }}
...track.effectChain, onRemoveEffect={(effectId) => {
effects: track.effectChain.effects.filter((e) => e.id !== effectId), const updatedChain = {
}; ...track.effectChain,
onUpdateTrack(track.id, { effectChain: updatedChain }); effects: track.effectChain.effects.filter((e) => e.id !== effectId),
}} };
onUpdateEffect={(effectId, parameters) => { onUpdateTrack(track.id, { effectChain: updatedChain });
const updatedChain = { }}
...track.effectChain, onUpdateEffect={(effectId, parameters) => {
effects: track.effectChain.effects.map((e) => const updatedChain = {
e.id === effectId ? { ...e, parameters } : e ...track.effectChain,
), effects: track.effectChain.effects.map((e) =>
}; e.id === effectId ? { ...e, parameters } : e
onUpdateTrack(track.id, { effectChain: updatedChain }); ),
}} };
onAddEffect={(effectType) => { onUpdateTrack(track.id, { effectChain: updatedChain });
const newEffect = createEffect( }}
effectType, onAddEffect={(effectType) => {
EFFECT_NAMES[effectType] const newEffect = createEffect(
); effectType,
const updatedChain = { EFFECT_NAMES[effectType]
...track.effectChain, );
effects: [...track.effectChain.effects, newEffect], const updatedChain = {
}; ...track.effectChain,
onUpdateTrack(track.id, { effectChain: updatedChain }); effects: [...track.effectChain.effects, newEffect],
}} };
onSelectionChange={ onUpdateTrack(track.id, { effectChain: updatedChain });
onSelectionChange }}
? (selection) => onSelectionChange(track.id, selection) onSelectionChange={
: undefined onSelectionChange
} ? (selection) => onSelectionChange(track.id, selection)
onToggleRecordEnable={ : undefined
onToggleRecordEnable }
? () => onToggleRecordEnable(track.id) onToggleRecordEnable={
: undefined onToggleRecordEnable
} ? () => onToggleRecordEnable(track.id)
isRecording={recordingTrackId === track.id} : undefined
recordingLevel={recordingTrackId === track.id ? recordingLevel : 0} }
playbackLevel={trackLevels[track.id] || 0} isRecording={recordingTrackId === track.id}
onParameterTouched={onParameterTouched} recordingLevel={recordingTrackId === track.id ? recordingLevel : 0}
isPlaying={isPlaying} playbackLevel={trackLevels[track.id] || 0}
renderWaveformOnly={true} onParameterTouched={onParameterTouched}
/> isPlaying={isPlaying}
))} renderWaveformOnly={true}
/>
{/* Automation Lane Section */}
{!track.collapsed && track.automation?.showAutomation && (
<div className="bg-muted/30 border-b border-border">
{track.automation.lanes
.filter((lane) => lane.parameterId === track.automation.selectedParameterId)
.map((lane) => (
<AutomationLane
key={lane.id}
lane={lane}
trackId={track.id}
zoom={zoom}
currentTime={currentTime}
duration={duration}
isPlaying={isPlaying}
onAddPoint={(time, value) => {
const newPoint = createAutomationPoint(time, value);
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: [...l.points, newPoint].sort((a, b) => a.time - b.time) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onUpdatePoint={(pointId, updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? {
...l,
points: l.points.map((p) =>
p.id === pointId ? { ...p, ...updates } : p
),
}
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onRemovePoint={(pointId) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id
? { ...l, points: l.points.filter((p) => p.id !== pointId) }
: l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onUpdateLane={(updates) => {
const updatedLanes = track.automation.lanes.map((l) =>
l.id === lane.id ? { ...l, ...updates } : l
);
onUpdateTrack(track.id, {
automation: { ...track.automation, lanes: updatedLanes },
});
}}
onSeek={onSeek}
/>
))}
</div>
)}
{/* Effects Section */}
<TrackExtensions
track={track}
onUpdateTrack={onUpdateTrack}
onToggleEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, enabled: !e.enabled } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onRemoveEffect={(effectId) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.filter((e) => e.id !== effectId),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onUpdateEffect={(effectId, parameters) => {
const updatedChain = {
...track.effectChain,
effects: track.effectChain.effects.map((e) =>
e.id === effectId ? { ...e, parameters } : e
),
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
onAddEffect={(effectType) => {
const newEffect = createEffect(
effectType,
EFFECT_NAMES[effectType]
);
const updatedChain = {
...track.effectChain,
effects: [...track.effectChain.effects, newEffect],
};
onUpdateTrack(track.id, { effectChain: updatedChain });
}}
/>
</React.Fragment>
))}
</div>
</div> </div>
</div> </div>