diff --git a/src/components/settings/ArtistLayoutCustomizer.tsx b/src/components/settings/ArtistLayoutCustomizer.tsx index 080ec5fa..718c34dc 100644 --- a/src/components/settings/ArtistLayoutCustomizer.tsx +++ b/src/components/settings/ArtistLayoutCustomizer.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useRef, useState } from 'react'; +import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { GripVertical } from 'lucide-react'; -import { useDragDrop, useDragSource } from '../../contexts/DragDropContext'; import { useArtistLayoutStore, type ArtistSectionConfig, type ArtistSectionId } from '../../store/artistLayoutStore'; +import { useListReorderDnd } from '../../hooks/useListReorderDnd'; +import { applyListReorderById, type ListReorderDropTarget } from '../../utils/componentHelpers/listReorder'; +import { ReorderGripHandle } from './ReorderGripHandle'; const ARTIST_SECTION_LABEL_KEYS: Record = { bio: 'settings.artistLayoutBio', @@ -12,111 +13,45 @@ const ARTIST_SECTION_LABEL_KEYS: Record = { featured: 'settings.artistLayoutFeatured', }; -type ArtistDropTarget = { idx: number; before: boolean } | null; - -function ArtistSectionGripHandle({ idx, label }: { idx: number; label: string }) { - const { t } = useTranslation(); - const { onMouseDown } = useDragSource(() => ({ - data: JSON.stringify({ type: 'artist_section_reorder', index: idx }), - label, - })); - return ( - - - - ); -} +const REORDER_TYPE = 'artist_section_reorder'; export function ArtistLayoutCustomizer() { const { t } = useTranslation(); const sections = useArtistLayoutStore(s => s.sections); const setSections = useArtistLayoutStore(s => s.setSections); const toggleSection = useArtistLayoutStore(s => s.toggleSection); - const { isDragging: isPsyDragging } = useDragDrop(); - const [containerEl, setContainerEl] = useState(null); - const [dropTarget, setDropTarget] = useState(null); - const dropTargetRef = useRef(null); const sectionsRef = useRef(sections); - // React Compiler refs rule: ref kept in sync with the latest value for use in effects/handlers/cleanup; not render data. + // React Compiler refs rule: ref kept in sync with the latest value for use in handlers; not render data. // eslint-disable-next-line react-hooks/refs sectionsRef.current = sections; - useEffect(() => { - // React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effect’s dependencies change. - // eslint-disable-next-line react-hooks/set-state-in-effect - if (!isPsyDragging) { dropTargetRef.current = null; setDropTarget(null); } - }, [isPsyDragging]); + const apply = useCallback((draggedId: string, target: ListReorderDropTarget) => { + const next = applyListReorderById(sectionsRef.current, draggedId, target); + if (next) setSections(next); + }, [setSections]); - useEffect(() => { - if (!containerEl) return; - const onPsyDrop = (e: Event) => { - const detail = (e as CustomEvent).detail; - if (!detail?.data) return; - let parsed: { type?: string; index?: number }; - try { parsed = JSON.parse(detail.data as string); } catch { return; } - if (parsed.type !== 'artist_section_reorder' || parsed.index == null) return; - - const fromIdx = parsed.index; - const target = dropTargetRef.current; - dropTargetRef.current = null; setDropTarget(null); - if (!target) return; - - const insertBefore = target.before ? target.idx : target.idx + 1; - if (insertBefore === fromIdx || insertBefore === fromIdx + 1) return; - - const next = [...sectionsRef.current]; - const [moved] = next.splice(fromIdx, 1); - next.splice(insertBefore > fromIdx ? insertBefore - 1 : insertBefore, 0, moved); - setSections(next); - }; - containerEl.addEventListener('psy-drop', onPsyDrop); - return () => containerEl.removeEventListener('psy-drop', onPsyDrop); - }, [containerEl, setSections]); - - const handleMouseMove = (e: React.MouseEvent) => { - if (!isPsyDragging || !containerEl) return; - const rows = containerEl.querySelectorAll('[data-artist-idx]'); - let target: ArtistDropTarget = null; - for (const row of rows) { - const rect = row.getBoundingClientRect(); - const idx = Number(row.dataset.artistIdx); - if (e.clientY < rect.top + rect.height / 2) { target = { idx, before: true }; break; } - target = { idx, before: false }; - } - dropTargetRef.current = target; - setDropTarget(target); - }; + const { isDragging, setContainer, onMouseMove, dropEdge } = useListReorderDnd({ type: REORDER_TYPE, apply }); return ( <>

{t('settings.artistLayoutDesc')}

-
- {sections.map((section: ArtistSectionConfig, i) => { +
+ {sections.map((section: ArtistSectionConfig) => { const label = t(ARTIST_SECTION_LABEL_KEYS[section.id]); - const isBefore = isPsyDragging && dropTarget?.idx === i && dropTarget.before; - const isAfter = isPsyDragging && dropTarget?.idx === i && !dropTarget.before; + const edge = isDragging ? dropEdge(section.id) : null; return (
- + {label}