slideshare/src/hooks/useSlideOperations.ts
Michael Mainguy d25eb56794 feat: Add drag-and-drop slide reordering functionality
- Add reorderSlides function to useSlideOperations hook
- Update SlidesSidebar to handle drag-and-drop events
- Make SlideThumbnail components draggable with visual feedback
- Add CSS styles for drag states (dragged, drag-over, drag handle)
- Implement proper slide order management and persistence
- Add visual indicators for drag operations (opacity, transform, borders)
- Disable drag operations when saving to prevent conflicts
This completes the slide reordering feature from the user flows document.
2025-09-27 16:48:17 -05:00

183 lines
6.0 KiB
TypeScript

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { Presentation, Slide } from '../types/presentation.ts';
import { updatePresentation } from '../utils/presentationStorage.ts';
import { loggers } from '../utils/logger.ts';
interface UseSlideOperationsProps {
presentation: Presentation | null;
presentationId: string;
onPresentationUpdate: (presentation: Presentation) => void;
onError: (error: string) => void;
confirmDelete: (message: string) => Promise<boolean>;
}
export const useSlideOperations = ({
presentation,
presentationId,
onPresentationUpdate,
onError,
confirmDelete
}: UseSlideOperationsProps) => {
const navigate = useNavigate();
const [saving, setSaving] = useState(false);
const duplicateSlide = async (slideIndex: number) => {
if (!presentation) return;
const slideToDuplicate = presentation.slides[slideIndex];
if (!slideToDuplicate) return;
try {
setSaving(true);
onError('');
// Create a duplicate slide with new ID
const duplicatedSlide: Slide = {
...slideToDuplicate,
id: `slide_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
order: slideIndex + 1 // Insert right after the original
};
// Create updated presentation with the duplicated slide
const updatedPresentation = { ...presentation };
const newSlides = [...presentation.slides];
// Insert the duplicated slide after the original
newSlides.splice(slideIndex + 1, 0, duplicatedSlide);
// Update slide order for all slides after the insertion point
newSlides.forEach((slide, index) => {
slide.order = index;
});
updatedPresentation.slides = newSlides;
// Save the updated presentation
await updatePresentation(updatedPresentation);
// Update local state
onPresentationUpdate(updatedPresentation);
// Navigate to the duplicated slide
const newSlideNumber = slideIndex + 2; // +2 because we inserted after and slide numbers are 1-based
navigate(`/presentations/${presentationId}/edit/slides/${newSlideNumber}`);
} catch (err) {
loggers.presentation.error('Failed to duplicate slide', err instanceof Error ? err : new Error(String(err)));
onError(err instanceof Error ? err.message : 'Failed to duplicate slide');
} finally {
setSaving(false);
}
};
const deleteSlide = async (slideIndex: number) => {
if (!presentation) return;
const slideToDelete = presentation.slides[slideIndex];
if (!slideToDelete) return;
const slideNumber = slideIndex + 1;
const totalSlides = presentation.slides.length;
let confirmMessage = `Are you sure you want to delete slide ${slideNumber}?`;
if (totalSlides === 1) {
confirmMessage = `Are you sure you want to delete the only slide in this presentation? The presentation will be empty after deletion.`;
} else {
confirmMessage += ` This will remove the slide and renumber all subsequent slides. This action cannot be undone.`;
}
const confirmed = await confirmDelete(confirmMessage);
if (!confirmed) {
return;
}
try {
setSaving(true);
onError('');
// Create updated presentation with the slide removed
const updatedPresentation = { ...presentation };
updatedPresentation.slides = presentation.slides.filter((_, index) => index !== slideIndex);
// Update slide order for remaining slides
updatedPresentation.slides.forEach((slide, index) => {
slide.order = index;
});
// Save the updated presentation
await updatePresentation(updatedPresentation);
// Update local state
onPresentationUpdate(updatedPresentation);
// Handle navigation after deletion
const totalSlides = updatedPresentation.slides.length;
if (totalSlides === 0) {
// No slides left, stay on editor main view
navigate(`/presentations/${presentationId}/edit`);
} else {
// Navigate to appropriate slide
let newSlideIndex = slideIndex;
if (slideIndex >= totalSlides) {
// If we deleted the last slide, go to the new last slide
newSlideIndex = totalSlides - 1;
}
// Navigate to the adjusted slide number
const slideNumber = newSlideIndex + 1;
navigate(`/presentations/${presentationId}/edit/slides/${slideNumber}`);
}
} catch (err) {
loggers.presentation.error('Failed to delete slide', err instanceof Error ? err : new Error(String(err)));
onError(err instanceof Error ? err.message : 'Failed to delete slide');
} finally {
setSaving(false);
}
};
const reorderSlides = async (fromIndex: number, toIndex: number) => {
if (!presentation || fromIndex === toIndex) return;
try {
setSaving(true);
onError('');
// Create updated presentation with reordered slides
const updatedPresentation = { ...presentation };
const newSlides = [...presentation.slides];
// Remove the slide from its current position
const [movedSlide] = newSlides.splice(fromIndex, 1);
// Insert it at the new position
newSlides.splice(toIndex, 0, movedSlide);
// Update slide order for all slides
newSlides.forEach((slide, index) => {
slide.order = index;
});
updatedPresentation.slides = newSlides;
// Save the updated presentation
await updatePresentation(updatedPresentation);
// Update local state
onPresentationUpdate(updatedPresentation);
} catch (err) {
loggers.presentation.error('Failed to reorder slides', err instanceof Error ? err : new Error(String(err)));
onError(err instanceof Error ? err.message : 'Failed to reorder slides');
} finally {
setSaving(false);
}
};
return {
duplicateSlide,
deleteSlide,
reorderSlides,
saving
};
};