(
pages={pages}
activePageId={activePageId}
onPageChange={onPageChange}
+ disabled={isReorderingPages}
/>
+
+
+
+
+
{/* Mode Toggle - reuse with compact=true */}
= ({
>
{sortedPages.map((page, index) => (
))}
diff --git a/frontend/src/components/Constructor/types.ts b/frontend/src/components/Constructor/types.ts
index f099b0d..6df01e6 100644
--- a/frontend/src/components/Constructor/types.ts
+++ b/frontend/src/components/Constructor/types.ts
@@ -251,6 +251,8 @@ export interface ConstructorToolbarProps {
pages: TourPage[];
activePageId: string;
onPageChange: (pageId: string) => void;
+ onMovePage?: (direction: 'up' | 'down') => void;
+ isReorderingPages?: boolean;
// Mode toggle (reuse InteractionModeToggle with compact=true)
interactionMode: ConstructorInteractionMode;
diff --git a/frontend/src/lib/backgroundAudioController.ts b/frontend/src/lib/backgroundAudioController.ts
index ccbd158..baf290a 100644
--- a/frontend/src/lib/backgroundAudioController.ts
+++ b/frontend/src/lib/backgroundAudioController.ts
@@ -36,12 +36,7 @@ class BackgroundAudioController {
setWaitingForInteraction(waiting: boolean): void {
this.waitingForInteraction = waiting;
- if (
- waiting &&
- this.hasUserInteracted &&
- !this.muted &&
- this.audioElement
- ) {
+ if (waiting && this.hasUserInteracted && !this.muted && this.audioElement) {
this.audioElement.play().catch(() => undefined);
this.waitingForInteraction = false;
}
diff --git a/frontend/src/pages/constructor.tsx b/frontend/src/pages/constructor.tsx
index 44450c0..9e0843f 100644
--- a/frontend/src/pages/constructor.tsx
+++ b/frontend/src/pages/constructor.tsx
@@ -1,4 +1,5 @@
import { mdiContentSave, mdiExitToApp, mdiPlus } from '@mdi/js';
+import axios from 'axios';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, {
@@ -108,6 +109,7 @@ import {
import { useCanvasScale } from '../hooks/useCanvasScale';
import { useVideoSoundControl } from '../hooks/useVideoSoundControl';
import { useNetworkAware } from '../hooks/useNetworkAware';
+import { queryClient, queryKeys } from '../lib/queryClient';
// TourPage type is imported from '../types/entities'
// NavigationElementType is imported from '../context/ConstructorContext'
@@ -118,6 +120,16 @@ type ConstructorPageProps = {
type ConstructorInteractionMode = 'edit' | 'interact';
+const sortTourPagesForDisplay = (items: TourPage[]) =>
+ [...items].sort((a, b) => {
+ const orderA =
+ typeof a.sort_order === 'number' ? a.sort_order : Number.MAX_SAFE_INTEGER;
+ const orderB =
+ typeof b.sort_order === 'number' ? b.sort_order : Number.MAX_SAFE_INTEGER;
+ if (orderA !== orderB) return orderA - orderB;
+ return (a.name || '').localeCompare(b.name || '');
+ });
+
// Use ELEMENT_TYPE_LABELS from elementDefaults for label lookup
const labelByType = ELEMENT_TYPE_LABELS;
@@ -131,6 +143,7 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const elementEditorRef = useRef(null);
const toolbarRef = useRef(null);
const [isAuthReady, setIsAuthReady] = useState(false);
+ const [isReorderingPages, setIsReorderingPages] = useState(false);
const isElementEditMode = mode === 'element_edit';
const projectId = useMemo(() => {
@@ -892,6 +905,73 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
}
}, [activePageId, pages, refetchData]);
+ const handleMovePage = useCallback(
+ async (direction: 'up' | 'down') => {
+ if (!projectId || !activePageId || isReorderingPages) return;
+
+ const sortedPages = sortTourPagesForDisplay(pages);
+ const currentIndex = sortedPages.findIndex(
+ (page) => page.id === activePageId,
+ );
+ const targetIndex =
+ direction === 'up' ? currentIndex - 1 : currentIndex + 1;
+
+ if (
+ currentIndex < 0 ||
+ targetIndex < 0 ||
+ targetIndex >= sortedPages.length
+ ) {
+ return;
+ }
+
+ const reorderedPages = [...sortedPages];
+ const [movedPage] = reorderedPages.splice(currentIndex, 1);
+ reorderedPages.splice(targetIndex, 0, movedPage);
+
+ try {
+ setIsReorderingPages(true);
+ setErrorMessage('');
+ await axios.post('/tour_pages/reorder', {
+ data: {
+ projectId,
+ environment: activePage?.environment || 'dev',
+ orderedPageIds: reorderedPages.map((page) => page.id),
+ },
+ });
+
+ await queryClient.invalidateQueries({
+ queryKey: queryKeys.tourPages.all,
+ });
+ await handleReload();
+ setActivePageId(activePageId);
+ setSuccessMessage('Page order updated.');
+ } catch (error: unknown) {
+ const axiosError = error as {
+ response?: { data?: { message?: string } };
+ };
+ const message =
+ axiosError?.response?.data?.message ||
+ (error instanceof Error ? error.message : null) ||
+ 'Failed to reorder pages.';
+ setErrorMessage(message);
+ logger.error(
+ 'Failed to reorder pages:',
+ error instanceof Error ? error : { error },
+ );
+ } finally {
+ setIsReorderingPages(false);
+ }
+ },
+ [
+ activePage?.environment,
+ activePageId,
+ handleReload,
+ isReorderingPages,
+ pages,
+ projectId,
+ ],
+ );
+
// Page actions (save, create page, save to stage)
const {
isSaving,
@@ -1912,6 +1992,8 @@ const ConstructorPage = ({ mode = 'constructor' }: ConstructorPageProps) => {
const page = pages.find((p) => p.id === pageId);
if (page) switchToPage(page);
}}
+ onMovePage={handleMovePage}
+ isReorderingPages={isReorderingPages}
interactionMode={constructorInteractionMode}
onModeChange={setConstructorInteractionMode}
onSelectMenuItem={selectMenuItemForEdit}