From a58d75c425b4da782683a7aa4e6e7b20c72632af Mon Sep 17 00:00:00 2001 From: nmemmert Date: Tue, 26 May 2026 10:09:43 -0400 Subject: [PATCH] Add drag-and-drop lesson reordering for admin study sections --- package-lock.json | 57 ++++++++++++- package.json | 3 + src/AdminPage.tsx | 213 +++++++++++++++++++++++++++++----------------- 3 files changed, 195 insertions(+), 78 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd3da01..0e6eafa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "siteforge", "version": "0.0.0", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "chart.js": "^4.4.0", "docx": "^9.6.1", "express": "^5.2.1", @@ -280,6 +283,59 @@ "node": ">=6.9.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", @@ -5766,7 +5822,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { diff --git a/package.json b/package.json index d09b41c..da0b673 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ "preview": "vite preview" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "chart.js": "^4.4.0", "docx": "^9.6.1", "express": "^5.2.1", diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index cd3c4d9..61ba870 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -1,10 +1,110 @@ import { useEffect, useRef, useState } from 'react' +import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core' +import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' import type { ChangeEvent } from 'react' import { Link } from 'react-router-dom' import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content' import { DEFAULTS } from './content' import { AnalyticsPanel } from './components/AnalyticsPanel' +interface SortableLessonSectionProps { + section: ColossiansStudySection + study: StudyProgram + updateStudySection: (studyId: string, sectionId: string, field: keyof ColossiansStudySection, value: string | string[] | number) => void + removeStudySection: (studyId: string, sectionId: string) => void +} + +function SortableLessonSection({ section, study, updateStudySection, removeStudySection }: SortableLessonSectionProps) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: section.id }) + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + background: isDragging ? '#f8f7f2' : undefined, + border: isDragging ? '2px solid #e0c070' : undefined, + marginBottom: '0.5rem', + } + + return ( +
+ +
+ Lesson {section.reference || section.id} +

{section.title || 'Untitled section'}

+
+
+ e.stopPropagation()}>Preview + Expand to edit lesson +
+
+
+
+
+
+ + +
+
+ + updateStudySection(study.id, section.id, 'reference', e.target.value)} /> +
+
+ + updateStudySection(study.id, section.id, 'title', e.target.value)} /> +
+
+ + updateStudySection(study.id, section.id, 'audioEmbedUrl', e.target.value)} /> +
+
+ + { + const val = e.target.value; + if (val) { + const date = new Date(val + ':00Z'); + updateStudySection(study.id, section.id, 'releasedAt', date.toISOString()); + } else { + updateStudySection(study.id, section.id, 'releasedAt', ''); + } + }} /> +
+
+ +