Files
bruitiste/src/components/EffectsBar.tsx
2025-10-06 11:21:02 +02:00

221 lines
8.7 KiB
TypeScript

import { useState } from 'react'
import { Dices } from 'lucide-react'
import { Slider } from './Slider'
import { Switch } from './Switch'
import { Dropdown } from './Dropdown'
import { EFFECTS } from '../config/effects'
import type { EffectValues } from '../types/effects'
interface EffectsBarProps {
values: EffectValues
onChange: (parameterId: string, value: number | boolean | string) => void
onMapClick?: (paramId: string, lfoIndex: number) => void
getMappedLFOs?: (paramId: string) => number[]
}
export function EffectsBar({ values, onChange, onMapClick, getMappedLFOs }: EffectsBarProps) {
const [activeTab, setActiveTab] = useState<string>(EFFECTS[0].id)
const [isCollapsed, setIsCollapsed] = useState(true)
const randomizeEffect = (effect: typeof EFFECTS[number]) => {
effect.parameters.forEach(param => {
if (param.id.endsWith('Enable')) return
if (param.options) {
const randomOption = param.options[Math.floor(Math.random() * param.options.length)]
onChange(param.id, randomOption.value)
} else {
const range = param.max - param.min
const steps = Math.floor(range / param.step)
const randomStep = Math.floor(Math.random() * (steps + 1))
const randomValue = param.min + (randomStep * param.step)
onChange(param.id, randomValue)
}
})
}
return (
<div className="bg-black border-t-2 border-white px-2 lg:px-6 py-3 lg:py-4">
{/* Desktop: Grid layout */}
<div className="hidden lg:grid lg:grid-cols-4 lg:gap-4">
{EFFECTS.map(effect => {
return (
<div key={effect.id} className="border-2 border-white p-3">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h3 className="font-mono text-[10px] tracking-[0.2em] text-white">
{effect.name.toUpperCase()}
</h3>
<button
onClick={() => randomizeEffect(effect)}
className="p-1 text-white hover:bg-white hover:text-black transition-colors"
>
<Dices size={12} strokeWidth={2} />
</button>
</div>
{effect.bypassable && (
<Switch
checked={!values[`${effect.id}Bypass`]}
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
label={values[`${effect.id}Bypass`] ? 'OFF' : 'ON'}
/>
)}
</div>
<div className="flex flex-col gap-3">
{effect.parameters.map(param => {
if (param.options) {
return (
<Dropdown
key={param.id}
label={param.label}
value={values[param.id] as string ?? param.default as string}
options={param.options}
onChange={(value) => onChange(param.id, value)}
/>
)
}
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
if (isSwitch) {
return (
<div key={param.id} className="flex flex-col gap-1 mt-2">
<div className="flex items-center justify-between">
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
{param.label.toUpperCase()}
</span>
<Switch
checked={Boolean(values[param.id])}
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
label={values[param.id] ? 'ON' : 'OFF'}
/>
</div>
</div>
)
}
return (
<Slider
key={param.id}
label={param.label}
value={values[param.id] as number ?? param.default as number}
min={param.min}
max={param.max}
step={param.step}
unit={param.unit}
onChange={(value) => onChange(param.id, value)}
valueId={param.id}
paramId={param.id}
onMapClick={onMapClick}
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
/>
)
})}
</div>
</div>
)
})}
</div>
{/* Mobile: Tabbed layout */}
<div className="lg:hidden flex flex-col">
<div className="flex border-2 border-white">
{EFFECTS.map(effect => (
<button
key={effect.id}
onClick={() => {
if (activeTab === effect.id && !isCollapsed) {
setIsCollapsed(true)
} else {
setActiveTab(effect.id)
setIsCollapsed(false)
}
}}
className={`flex-1 p-2 font-mono text-[9px] tracking-[0.15em] transition-colors ${
activeTab === effect.id && !isCollapsed
? 'bg-white text-black'
: 'bg-black text-white hover:bg-white/10'
}`}
>
{effect.name.toUpperCase()}
</button>
))}
</div>
{!isCollapsed && EFFECTS.map(effect => {
if (activeTab !== effect.id) return null
return (
<div key={effect.id} className="border-2 border-t-0 border-white p-3">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<button
onClick={() => randomizeEffect(effect)}
className="p-1 text-white hover:bg-white hover:text-black transition-colors"
>
<Dices size={14} strokeWidth={2} />
</button>
</div>
{effect.bypassable && (
<Switch
checked={!values[`${effect.id}Bypass`]}
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
label={values[`${effect.id}Bypass`] ? 'OFF' : 'ON'}
/>
)}
</div>
<div className="flex flex-col gap-3">
{effect.parameters.map(param => {
if (param.options) {
return (
<Dropdown
key={param.id}
label={param.label}
value={values[param.id] as string ?? param.default as string}
options={param.options}
onChange={(value) => onChange(param.id, value)}
/>
)
}
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
if (isSwitch) {
return (
<div key={param.id} className="flex flex-col gap-1 mt-2">
<div className="flex items-center justify-between">
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
{param.label.toUpperCase()}
</span>
<Switch
checked={Boolean(values[param.id])}
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
label={values[param.id] ? 'ON' : 'OFF'}
/>
</div>
</div>
)
}
return (
<Slider
key={param.id}
label={param.label}
value={values[param.id] as number ?? param.default as number}
min={param.min}
max={param.max}
step={param.step}
unit={param.unit}
onChange={(value) => onChange(param.id, value)}
valueId={param.id}
paramId={param.id}
onMapClick={onMapClick}
mappedLFOs={getMappedLFOs ? getMappedLFOs(param.id) : []}
/>
)
})}
</div>
</div>
)
})}
</div>
</div>
)
}