From 0a36a87cd494106c048ed3d8e69763e35ab12890 Mon Sep 17 00:00:00 2001 From: Dmitri Date: Sat, 11 Apr 2026 14:32:54 +0400 Subject: [PATCH] adaptivity improvements --- backend/src/db/api/projects.js | 2 + backend/src/db/api/tour_pages.js | 18 ++ ...00001-add-design-dimensions-to-projects.js | 29 +++ ...309-add-design-dimensions-to-tour-pages.js | 30 +++ backend/src/db/models/projects.js | 12 + backend/src/db/models/tour_pages.js | 12 + backend/src/services/projects.js | 57 +++++ .../components/CanvasDimensionSuggestion.tsx | 0 .../Constructor/CanvasBackground.tsx | 9 +- .../components/Constructor/PageSelector.tsx | 21 +- frontend/src/components/RotatePrompt.tsx | 46 ++++ .../src/components/RuntimePresentation.tsx | 84 +++++-- .../UiElements/elements/CarouselElement.tsx | 36 ++- .../shared/useElementWrapperStyle.ts | 19 +- frontend/src/config/canvas.config.ts | 66 ++++++ frontend/src/context/CanvasScaleContext.tsx | 124 ++++++++++ frontend/src/hooks/queries/usePagesQuery.ts | 2 +- .../hooks/useBackgroundDimensionSuggestion.ts | 217 ++++++++++++++++++ frontend/src/hooks/useBackgroundTransition.ts | 138 ++++++++++- frontend/src/hooks/useCanvasScale.ts | 120 ++++++++++ frontend/src/hooks/useConstructorData.ts | 2 +- .../src/hooks/useConstructorPageActions.ts | 40 +++- frontend/src/lib/canvasScale.ts | 212 +++++++++++++++++ frontend/src/lib/elementStyles.ts | 139 ++++++++++- frontend/src/lib/gallerySectionStyles.ts | 101 ++++++-- frontend/src/pages/constructor.tsx | 82 ++++++- frontend/src/pages/projects/projects-edit.tsx | 82 ++++++- frontend/src/types/entities.ts | 5 + frontend/src/types/runtime.ts | 5 + 29 files changed, 1620 insertions(+), 90 deletions(-) create mode 100644 backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js create mode 100644 backend/src/db/migrations/20260409111309-add-design-dimensions-to-tour-pages.js create mode 100644 frontend/src/components/CanvasDimensionSuggestion.tsx create mode 100644 frontend/src/components/RotatePrompt.tsx create mode 100644 frontend/src/config/canvas.config.ts create mode 100644 frontend/src/context/CanvasScaleContext.tsx create mode 100644 frontend/src/hooks/useBackgroundDimensionSuggestion.ts create mode 100644 frontend/src/hooks/useCanvasScale.ts create mode 100644 frontend/src/lib/canvasScale.ts diff --git a/backend/src/db/api/projects.js b/backend/src/db/api/projects.js index 22efedd..74dec15 100644 --- a/backend/src/db/api/projects.js +++ b/backend/src/db/api/projects.js @@ -62,6 +62,8 @@ class ProjectsDBApi extends GenericDBApi { logo_url: data.logo_url || null, favicon_url: data.favicon_url || null, og_image_url: data.og_image_url || null, + design_width: data.design_width !== undefined ? data.design_width : null, + design_height: data.design_height !== undefined ? data.design_height : null, }; } diff --git a/backend/src/db/api/tour_pages.js b/backend/src/db/api/tour_pages.js index 6ae7995..48ec4e8 100644 --- a/backend/src/db/api/tour_pages.js +++ b/backend/src/db/api/tour_pages.js @@ -38,6 +38,10 @@ class Tour_pagesDBApi extends GenericDBApi { return ['environment', 'background_loop', 'requires_auth']; } + static get UUID_FIELDS() { + return ['projectId']; + } + static get CSV_FIELDS() { return [ 'id', @@ -90,6 +94,10 @@ class Tour_pagesDBApi extends GenericDBApi { data.background_video_end_time !== undefined ? data.background_video_end_time : null, + design_width: + data.design_width !== undefined ? data.design_width : null, + design_height: + data.design_height !== undefined ? data.design_height : null, requires_auth: data.requires_auth || false, ui_schema_json: data.ui_schema_json || null, }; @@ -203,6 +211,16 @@ class Tour_pagesDBApi extends GenericDBApi { } } + // Validate and filter by UUID fields (e.g., projectId) + for (const field of this.UUID_FIELDS) { + if (filter[field] !== undefined) { + if (!Utils.isValidUuid(filter[field])) { + return { rows: [], count: 0 }; + } + where[field] = filter[field]; + } + } + if (filter.active !== undefined) { where.active = filter.active === true || filter.active === 'true'; } diff --git a/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js b/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js new file mode 100644 index 0000000..1c60fe1 --- /dev/null +++ b/backend/src/db/migrations/20260409000001-add-design-dimensions-to-projects.js @@ -0,0 +1,29 @@ +'use strict'; + +/** + * Migration: Add design canvas dimensions to projects + * + * Adds design_width and design_height columns to support + * responsive canvas scaling with project-specific aspect ratios. + * + * @type {import('sequelize-cli').Migration} + */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('projects', 'design_width', { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: 1920, + }); + await queryInterface.addColumn('projects', 'design_height', { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: 1080, + }); + }, + + async down(queryInterface, _Sequelize) { + await queryInterface.removeColumn('projects', 'design_width'); + await queryInterface.removeColumn('projects', 'design_height'); + }, +}; diff --git a/backend/src/db/migrations/20260409111309-add-design-dimensions-to-tour-pages.js b/backend/src/db/migrations/20260409111309-add-design-dimensions-to-tour-pages.js new file mode 100644 index 0000000..2883ac9 --- /dev/null +++ b/backend/src/db/migrations/20260409111309-add-design-dimensions-to-tour-pages.js @@ -0,0 +1,30 @@ +'use strict'; + +/** + * Migration: Add design_width and design_height to tour_pages + * + * These fields store the canvas dimensions for presentations. + * They are copied from the project's design dimensions when pages are saved/published. + * This ensures presentations use the dimensions that were active at save time, + * not the current project dimensions (safe migration pattern). + */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn('tour_pages', 'design_width', { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }); + + await queryInterface.addColumn('tour_pages', 'design_height', { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null, + }); + }, + + async down(queryInterface) { + await queryInterface.removeColumn('tour_pages', 'design_width'); + await queryInterface.removeColumn('tour_pages', 'design_height'); + }, +}; diff --git a/backend/src/db/models/projects.js b/backend/src/db/models/projects.js index 1a07ff0..557fb7b 100644 --- a/backend/src/db/models/projects.js +++ b/backend/src/db/models/projects.js @@ -53,6 +53,18 @@ module.exports = function (sequelize, DataTypes) { type: DataTypes.TEXT, }, + design_width: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 1920, + }, + + design_height: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: 1080, + }, + importHash: { type: DataTypes.STRING(255), allowNull: true, diff --git a/backend/src/db/models/tour_pages.js b/backend/src/db/models/tour_pages.js index fc80139..7040788 100644 --- a/backend/src/db/models/tour_pages.js +++ b/backend/src/db/models/tour_pages.js @@ -103,6 +103,18 @@ module.exports = function (sequelize, DataTypes) { defaultValue: null, }, + design_width: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + + design_height: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + }, + requires_auth: { type: DataTypes.BOOLEAN, diff --git a/backend/src/services/projects.js b/backend/src/services/projects.js index 1ae730d..1977863 100644 --- a/backend/src/services/projects.js +++ b/backend/src/services/projects.js @@ -224,6 +224,63 @@ class ProjectsService extends BaseProjectsService { } } + // Clone tour pages (dev environment only - stage/production are populated via publishing) + const sourcePages = await db.tour_pages.findAll({ + where: { projectId: sourceProjectId, environment: 'dev' }, + transaction, + }); + + for (const sourcePage of sourcePages) { + const pageData = sourcePage.toJSON(); + // Remove fields that should be regenerated + delete pageData.id; + delete pageData.createdAt; + delete pageData.updatedAt; + delete pageData.deletedAt; + delete pageData.deletedBy; + delete pageData.importHash; + + await db.tour_pages.create( + { + ...pageData, + projectId: clonedProject.id, + environment: 'dev', + source_key: sourcePage.id, // Link back to original page + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + + // Clone audio tracks (dev environment only) + const sourceAudioTracks = await db.project_audio_tracks.findAll({ + where: { projectId: sourceProjectId, environment: 'dev' }, + transaction, + }); + + for (const sourceTrack of sourceAudioTracks) { + const trackData = sourceTrack.toJSON(); + delete trackData.id; + delete trackData.createdAt; + delete trackData.updatedAt; + delete trackData.deletedAt; + delete trackData.deletedBy; + delete trackData.importHash; + + await db.project_audio_tracks.create( + { + ...trackData, + projectId: clonedProject.id, + environment: 'dev', + source_key: sourceTrack.id, + createdById: currentUser.id, + updatedById: currentUser.id, + }, + { transaction }, + ); + } + await transaction.commit(); return clonedProject; } catch (error) { diff --git a/frontend/src/components/CanvasDimensionSuggestion.tsx b/frontend/src/components/CanvasDimensionSuggestion.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/Constructor/CanvasBackground.tsx b/frontend/src/components/Constructor/CanvasBackground.tsx index 04be30e..98e662b 100644 --- a/frontend/src/components/Constructor/CanvasBackground.tsx +++ b/frontend/src/components/Constructor/CanvasBackground.tsx @@ -72,7 +72,7 @@ const CanvasBackground: React.FC = ({ key={`bg_image_${backgroundImageUrl}`} src={backgroundImageUrl} alt='Background' - className='absolute inset-0 h-full w-full object-cover' + className='absolute inset-0 h-full w-full object-contain' draggable={false} onLoad={handleLoad} onError={handleError} @@ -84,7 +84,7 @@ const CanvasBackground: React.FC = ({ alt='Background' fill sizes='100vw' - className='object-cover' + className='object-contain' draggable={false} unoptimized onLoad={handleLoad} @@ -100,8 +100,9 @@ const CanvasBackground: React.FC = ({ className='pointer-events-none absolute inset-0 z-10' style={{ backgroundImage: `url("${previousBgImageUrl}")`, - backgroundSize: 'cover', + backgroundSize: 'contain', backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', }} /> )} @@ -111,7 +112,7 @@ const CanvasBackground: React.FC = ({