feat: hero carousel CMS + responsive mobile/iPad fix + flat-scope assets

Replaces the filesystem-scan hero (fs.readdirSync of /public/footage/main)
with a fully CMS-driven HeroSlide model. Editors can now drag-drop reorder,
toggle slides on/off, set focal points for proper mobile cropping, and
auto-translate per-slide captions.

NEW SCHEMA (additive — does not touch existing tables)
- HeroSlide: mediaUrl, mediaType, altText, order, isActive, focalPointX,
  focalPointY, translationsJson, timestamps
- SiteSetting: key-value JSON store for site-wide config (favicon, logo,
  footer, OG image) — wired up in next commit
- Migration 20260504120000_add_hero_slides_and_site_settings/migration.sql
  uses CREATE TABLE IF NOT EXISTS, additive only

HERO REEL REFACTOR (Bug #4 — responsive mobile/iPad)
- Switches from `images: string[]` to `slides: HeroSlideData[]` while
  keeping a backwards-compat path so legacy callers still work
- w-screen → w-full max-w-[100vw] (no horizontal scroll on iOS)
- h-[100vh] → h-[100svh] so iOS Safari URL bar doesn't push content
- Reduces title font sizes on small viewports (text-3xl → text-4xl
  → text-5xl → text-[5.5rem]) so the headline stays inside the canvas
- objectPosition driven by focal-point fields per slide
- Native <video> support for video slides

HQ COMMAND — /hq-command/dashboard/hero
- Drag-drop reorder, click-to-set-focal-point, inline alt-text editing
- Auto-save with "Saving…" / "Saved ✓" indicators
- Per-slide caption overrides (title, subtitle, descriptions)
- Optional one-click AI translation to IT, VEC, ES, DE
- Drop-zone uploader → /api/assets (scope=footage, flat folder)

API — /api/assets
- New flat scopes: "footage" (writes to /public/footage/main) and
  "branding" (writes to /public/branding) — slug-less for site-wide assets
- New buildPublicUrl helper centralises the URL convention
- Revalidate helper expanded with branding + settings scopes

HOME PAGE
- Reads hero slides from DB first; falls back to filesystem scan when
  HeroSlide table is empty (so production keeps working immediately
  after migration runs but before the editor populates rows)

DEPLOY NOTES
- After git pull on VPS, run the migration ONCE:
    docker compose exec app npx prisma migrate deploy
  Then:
    docker compose up -d --build app
  Existing data (AdminUser w/ 2FA, ClientUser, GlobalNode, Application,
  TimelineEvent, NewsArticle, HeritageSection, SparePart, OperationsSignal,
  NotificationRoute, PageContent) is NOT touched. Migration only creates
  two new tables.
This commit is contained in:
2026-05-04 09:34:49 -05:00
parent 6e46808c27
commit b9201a437c
9 changed files with 855 additions and 84 deletions
@@ -0,0 +1,35 @@
-- ─────────────────────────────────────────────────────────────────────────
-- ADDITIVE MIGRATION — only adds new tables, never modifies or drops.
-- Existing data (AdminUser, ClientUser w/ 2FA, GlobalNode, etc.) untouched.
-- ─────────────────────────────────────────────────────────────────────────
-- HeroSlide: carousel slides shown on the home hero section.
-- Replaces filesystem-scan of /public/footage/main with CMS control.
CREATE TABLE IF NOT EXISTS "HeroSlide" (
"id" TEXT NOT NULL,
"mediaUrl" TEXT NOT NULL,
"mediaType" TEXT NOT NULL DEFAULT 'image',
"altText" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"focalPointX" DOUBLE PRECISION NOT NULL DEFAULT 0.5,
"focalPointY" DOUBLE PRECISION NOT NULL DEFAULT 0.5,
"translationsJson" TEXT DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HeroSlide_pkey" PRIMARY KEY ("id")
);
-- SiteSetting: key-value config for favicon, logo, footer, OG image, etc.
CREATE TABLE IF NOT EXISTS "SiteSetting" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"valueJson" TEXT NOT NULL DEFAULT '{}',
"translationsJson" TEXT DEFAULT '{}',
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SiteSetting_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "SiteSetting_key_key" ON "SiteSetting"("key");
+47 -1
View File
@@ -248,7 +248,53 @@ model PageContent {
}
// ------------------------------------------------------
// 11. CLIENT PORTAL (Usuarios B2B Aprobados) 🔥 NUEVO
// 11. HERO REEL (Carrusel principal del Home)
// ------------------------------------------------------
// Manages the rotating images/videos shown in the home hero section.
// Replaces the previous filesystem-scan approach (fs.readdirSync of
// /public/footage/main) with full CMS control: ordering, on/off toggle,
// focal-point per slide for proper responsive cropping on mobile/tablet,
// and per-slide alt text for SEO.
model HeroSlide {
id String @id @default(cuid())
mediaUrl String // Public path, e.g. "/footage/main/01_tifas.png"
mediaType String @default("image") // "image" | "video"
altText String? // For accessibility + SEO; falls back to title if null
order Int @default(0)
isActive Boolean @default(true)
// Focal point for object-position on mobile/tablet crops (01 range).
// Lets the editor pick "what should stay visible when the image is cropped".
focalPointX Float @default(0.5)
focalPointY Float @default(0.5)
// Optional per-slide caption that overrides the global Hero text.
// Stored as JSON keyed by locale: {"en":{"title":"...","subtitle":"..."}}
translationsJson String? @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 12. SITE SETTINGS (Favicon, Footer, Branding global)
// ------------------------------------------------------
// Single-row pattern (key-value) for global site config that doesn't
// fit any other model: favicon, logos, footer, OG image, social links.
model SiteSetting {
id String @id @default(cuid())
key String @unique // e.g. "favicon", "footer", "logo", "og_image", "hero_text"
valueJson String @default("{}") // Flexible JSON payload per key
// 🌍 Translation engine (used for things like footer link labels)
translationsJson String? @default("{}")
updatedAt DateTime @updatedAt
}
// ------------------------------------------------------
// 13. CLIENT PORTAL (Usuarios B2B Aprobados)
// ------------------------------------------------------
model ClientUser {
id String @id @default(cuid())