feat: implement drag-and-drop functionality for sidebar apps #101
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useRef } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Plus, Pencil, Trash2, ExternalLink, PanelRight, GripVertical } from "lucide-react";
|
import { Plus, Pencil, Trash2, ExternalLink, PanelRight, GripVertical } from "lucide-react";
|
||||||
import { icons as lucideIcons, type LucideIcon } from "lucide-react";
|
import { icons as lucideIcons, type LucideIcon } from "lucide-react";
|
||||||
@@ -180,10 +180,12 @@ function AppForm({
|
|||||||
export function SidebarAppsSettings() {
|
export function SidebarAppsSettings() {
|
||||||
const t = useTranslations("settings.sidebar_apps");
|
const t = useTranslations("settings.sidebar_apps");
|
||||||
const tApps = useTranslations("sidebar_apps");
|
const tApps = useTranslations("sidebar_apps");
|
||||||
const { sidebarApps, keepAppsLoaded, addSidebarApp, updateSidebarApp, removeSidebarApp, updateSetting } = useSettingsStore();
|
const { sidebarApps, keepAppsLoaded, addSidebarApp, updateSidebarApp, removeSidebarApp, reorderSidebarApps, updateSetting } = useSettingsStore();
|
||||||
const [editingApp, setEditingApp] = useState<string | null>(null);
|
const [editingApp, setEditingApp] = useState<string | null>(null);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
|
const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog();
|
||||||
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||||
|
const draggedIndexRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const handleAdd = useCallback((data: SidebarAppFormData) => {
|
const handleAdd = useCallback((data: SidebarAppFormData) => {
|
||||||
const id = `app-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
const id = `app-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||||
@@ -207,6 +209,34 @@ export function SidebarAppsSettings() {
|
|||||||
removeSidebarApp(app.id);
|
removeSidebarApp(app.id);
|
||||||
}, [confirmDialog, tApps, removeSidebarApp]);
|
}, [confirmDialog, tApps, removeSidebarApp]);
|
||||||
|
|
||||||
|
const handleDragStart = useCallback((e: React.DragEvent, index: number) => {
|
||||||
|
draggedIndexRef.current = index;
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
e.dataTransfer.setData("text/plain", String(index));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = "move";
|
||||||
|
setDragOverIndex(index);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e: React.DragEvent, dropIndex: number) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragOverIndex(null);
|
||||||
|
const fromIndex = draggedIndexRef.current;
|
||||||
|
if (fromIndex === null || fromIndex === dropIndex) return;
|
||||||
|
const newApps = [...sidebarApps];
|
||||||
|
const [moved] = newApps.splice(fromIndex, 1);
|
||||||
|
newApps.splice(dropIndex, 0, moved);
|
||||||
|
reorderSidebarApps(newApps);
|
||||||
|
}, [sidebarApps, reorderSidebarApps]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
draggedIndexRef.current = null;
|
||||||
|
setDragOverIndex(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSection title={t("title")} description={t("description")}>
|
<SettingsSection title={t("title")} description={t("description")}>
|
||||||
@@ -224,7 +254,7 @@ export function SidebarAppsSettings() {
|
|||||||
<p className="text-sm text-muted-foreground py-4 text-center">{tApps("no_apps_hint")}</p>
|
<p className="text-sm text-muted-foreground py-4 text-center">{tApps("no_apps_hint")}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sidebarApps.map((app) => {
|
{sidebarApps.map((app, index) => {
|
||||||
if (editingApp === app.id) {
|
if (editingApp === app.id) {
|
||||||
return (
|
return (
|
||||||
<AppForm
|
<AppForm
|
||||||
@@ -240,9 +270,21 @@ export function SidebarAppsSettings() {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={app.id}
|
key={app.id}
|
||||||
className="flex items-center gap-3 p-3 border border-border rounded-lg hover:bg-muted/50 transition-colors"
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, index)}
|
||||||
|
onDragOver={(e) => handleDragOver(e, index)}
|
||||||
|
onDrop={(e) => handleDrop(e, index)}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 p-3 border rounded-lg transition-colors",
|
||||||
|
dragOverIndex === index
|
||||||
|
? "border-primary bg-primary/5"
|
||||||
|
: "border-border hover:bg-muted/50"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<GripVertical className="w-4 h-4 text-muted-foreground/50 flex-shrink-0" />
|
<div className="cursor-grab active:cursor-grabbing text-muted-foreground/50 hover:text-muted-foreground flex-shrink-0">
|
||||||
|
<GripVertical className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
<div className="w-8 h-8 rounded-md bg-muted flex items-center justify-center flex-shrink-0">
|
<div className="w-8 h-8 rounded-md bg-muted flex items-center justify-center flex-shrink-0">
|
||||||
{AppIcon ? <AppIcon className="w-4 h-4" /> : null}
|
{AppIcon ? <AppIcon className="w-4 h-4" /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user