feat: integrate dnd-kit for sidebar customization and reordering functionality
- Added dnd-kit dependencies to package.json and pnpm-lock.yaml. - Implemented SidebarCustomization component using dnd-kit for drag-and-drop reordering of sidebar items. - Created SortableItem component for individual sidebar items with visibility toggle. - Enhanced SidebarCustomizer component with drag-and-drop support using react-beautiful-dnd. - Updated sidebar-new component to include dynamic shortcuts for recently played albums and playlists. - Improved user experience with import/export settings functionality and toast notifications.
This commit is contained in:
310
app/components/SidebarCustomization.tsx
Normal file
310
app/components/SidebarCustomization.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
import {
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
GripVertical,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Download,
|
||||
Upload,
|
||||
RotateCcw,
|
||||
Search,
|
||||
Home,
|
||||
List,
|
||||
Radio,
|
||||
Users,
|
||||
Disc,
|
||||
Music,
|
||||
Heart,
|
||||
Grid3X3,
|
||||
Clock,
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
import { useSidebarLayout, SidebarItem, SidebarItemType } from '@/hooks/use-sidebar-layout';
|
||||
|
||||
// Icon mapping
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
search: <Search className="h-4 w-4" />,
|
||||
home: <Home className="h-4 w-4" />,
|
||||
queue: <List className="h-4 w-4" />,
|
||||
radio: <Radio className="h-4 w-4" />,
|
||||
artists: <Users className="h-4 w-4" />,
|
||||
albums: <Disc className="h-4 w-4" />,
|
||||
playlists: <Music className="h-4 w-4" />,
|
||||
favorites: <Heart className="h-4 w-4" />,
|
||||
browse: <Grid3X3 className="h-4 w-4" />,
|
||||
songs: <Music className="h-4 w-4" />,
|
||||
history: <Clock className="h-4 w-4" />,
|
||||
settings: <Settings className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
interface SortableItemProps {
|
||||
item: SidebarItem;
|
||||
onToggleVisibility: (id: SidebarItemType) => void;
|
||||
showIcons: boolean;
|
||||
}
|
||||
|
||||
function SortableItem({ item, onToggleVisibility, showIcons }: SortableItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="flex items-center justify-between p-3 bg-secondary/50 rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="cursor-grab hover:cursor-grabbing text-muted-foreground"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
{showIcons && (
|
||||
<div className="text-muted-foreground">
|
||||
{iconMap[item.icon] || <div className="h-4 w-4" />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className={`font-medium ${!item.visible ? 'text-muted-foreground line-through' : ''}`}>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleVisibility(item.id)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{item.visible ? (
|
||||
<Eye className="h-4 w-4" />
|
||||
) : (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarCustomization() {
|
||||
const {
|
||||
settings,
|
||||
reorderItems,
|
||||
toggleItemVisibility,
|
||||
updateShortcuts,
|
||||
updateShowIcons,
|
||||
exportSettings,
|
||||
importSettings,
|
||||
resetToDefaults,
|
||||
} = useSidebarLayout();
|
||||
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importError, setImportError] = useState<string | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
reorderItems(active.id as string, over.id as string);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFile = async () => {
|
||||
if (!importFile) return;
|
||||
|
||||
setImporting(true);
|
||||
setImportError(null);
|
||||
|
||||
try {
|
||||
await importSettings(importFile);
|
||||
setImportFile(null);
|
||||
// Reset file input
|
||||
const fileInput = document.getElementById('settings-import') as HTMLInputElement;
|
||||
if (fileInput) fileInput.value = '';
|
||||
} catch (error) {
|
||||
setImportError(error instanceof Error ? error.message : 'Failed to import settings');
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Sidebar Customization</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the sidebar layout, reorder items, and manage visibility settings.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Show Icons Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label>Show Icons</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Display icons next to navigation items
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.showIcons}
|
||||
onCheckedChange={updateShowIcons}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Shortcut Type */}
|
||||
<div className="space-y-3">
|
||||
<Label>Sidebar Shortcuts</Label>
|
||||
<RadioGroup
|
||||
value={settings.shortcuts}
|
||||
onValueChange={(value: 'albums' | 'playlists' | 'both') => updateShortcuts(value)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="albums" id="shortcuts-albums" />
|
||||
<Label htmlFor="shortcuts-albums">Albums only</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="playlists" id="shortcuts-playlists" />
|
||||
<Label htmlFor="shortcuts-playlists">Playlists only</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="both" id="shortcuts-both" />
|
||||
<Label htmlFor="shortcuts-both">Both albums and playlists</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items Order */}
|
||||
<div className="space-y-3">
|
||||
<Label>Navigation Items</Label>
|
||||
<div className="text-sm text-muted-foreground mb-3">
|
||||
Drag to reorder, click the eye icon to show/hide items
|
||||
</div>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={settings.items.map(item => item.id)} strategy={verticalListSortingStrategy}>
|
||||
<div className="space-y-2">
|
||||
{settings.items.map((item) => (
|
||||
<SortableItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
onToggleVisibility={toggleItemVisibility}
|
||||
showIcons={settings.showIcons}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
|
||||
{/* Settings Import/Export */}
|
||||
<div className="space-y-3 pt-4 border-t">
|
||||
<Label>Settings Management</Label>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={exportSettings} variant="outline">
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Export Settings
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="settings-import"
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={(e) => setImportFile(e.target.files?.[0] || null)}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => document.getElementById('settings-import')?.click()}
|
||||
>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
Select File
|
||||
</Button>
|
||||
|
||||
{importFile && (
|
||||
<Button
|
||||
onClick={handleImportFile}
|
||||
disabled={importing}
|
||||
variant="default"
|
||||
>
|
||||
{importing ? 'Importing...' : 'Import'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button onClick={resetToDefaults} variant="outline">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Reset to Default
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{importFile && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Selected: {importFile.name}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importError && (
|
||||
<div className="text-sm text-destructive">
|
||||
Error: {importError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user