/* global React, ReactDOM, HomePage, PortfolioPage, AboutPage, ContactPage, Nav, Footer, WhatsAppFloat, FullMenu, TweaksPanel, TWEAK_DEFAULTS, COPY, applyPalette, PAOLA_DATA */
const { useState, useEffect, useCallback } = React;
function normId(s) {
return s.toLowerCase()
.normalize('NFD').replace(/[̀-ͯ]/g, '')
.replace(/[^a-z0-9]/g, '')
.replace(/(.)\1+/g, '$1'); // colapsa letras dobles: infanntil → infantil
}
function folderToName(folder) {
return folder
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
function parseHash() {
const h = window.location.hash.replace('#', '') || 'home';
const [route, sub] = h.split('/');
return { route: route || 'home', sub };
}
function App() {
const [{ route, sub }, setRoute] = useState(parseHash());
const [menuOpen, setMenuOpen] = useState(false);
const [dynGallery, setDynGallery] = useState(null);
const [dynCats, setDynCats] = useState([]);
const [heroCopyOverride, setHeroCopyOverride] = useState(null);
const [contentReady, setContentReady] = useState(false);
const [tweaks, setTweaks] = useState(() => {
try {
const saved = localStorage.getItem('paola-tweaks');
return saved ? { ...TWEAK_DEFAULTS, ...JSON.parse(saved) } : TWEAK_DEFAULTS;
} catch { return TWEAK_DEFAULTS; }
});
useEffect(() => {
const onHash = () => setRoute(parseHash());
window.addEventListener('hashchange', onHash);
return () => window.removeEventListener('hashchange', onHash);
}, []);
// Actualiza el título de la pestaña según la ruta
useEffect(() => {
const titles = {
home: 'Paola García · Fotógrafa Materna e Infantil',
portfolio: 'Portafolio · Paola García',
about: 'Sobre mí · Paola García',
contact: 'Contacto · Paola García',
};
document.title = titles[route] || titles.home;
}, [route]);
useEffect(() => { applyPalette(tweaks.palette); }, [tweaks.palette]);
useEffect(() => {
try { localStorage.setItem('paola-tweaks', JSON.stringify(tweaks)); } catch {}
}, [tweaks]);
const setTweak = useCallback((k, v) => {
setTweaks(t => {
const next = typeof k === 'object' ? { ...t, ...k } : { ...t, [k]: v };
try { window.parent.postMessage({ type: '__edit_mode_set_keys', edits: next }, '*'); } catch {}
return next;
});
}, []);
const go = useCallback((r, s) => {
const hash = s ? `${r}/${s}` : r;
window.location.hash = hash;
window.scrollTo({ top: 0, behavior: 'instant' });
}, []);
// Carga contenido editable desde content.json vía PHP
useEffect(() => {
fetch('/api/content.php')
.then(r => r.json())
.then(data => {
// Hero copy (frases del banner)
if (data.hero?.copy?.line1) setHeroCopyOverride(data.hero.copy);
// Hero slides: an empty array from the admin means no banner images.
if (Array.isArray(data.hero?.slides)) {
const slides = data.hero.slides.filter(Boolean);
PAOLA_DATA.hero.slides = slides;
PAOLA_DATA.hero.main = slides[0] || '';
PAOLA_DATA.hero.alt = slides[1] || slides[0] || '';
}
// Sobre mí — fotos de Paola
if (data.about?.portrait) PAOLA_DATA.about.portrait = data.about.portrait;
if (data.about?.landscape) PAOLA_DATA.about.landscape = data.about.landscape;
if (data.about?.bio) PAOLA_DATA.about.bio = data.about.bio;
// Bloque "Experiencia" del home
if (data.experience?.length) PAOLA_DATA.experience = data.experience;
// Contacto
if (data.contact) Object.assign(PAOLA_DATA.contact, data.contact);
// Testimonios
if (data.testimonials?.length) PAOLA_DATA.testimonials = data.testimonials;
// Paquetes — fusionar por id de categoría
if (data.packages?.length) {
data.packages.forEach(pkg => {
const cat = PAOLA_DATA.categories.find(c => c.id === pkg.id);
if (cat) cat.packages = pkg.groups;
});
}
setContentReady(true);
})
.catch(() => setContentReady(true)); // si falla, usa data.js como fallback
}, []);
// Carga galería dinámica desde categorias/ vía PHP API
useEffect(() => {
fetch('/api/gallery.php')
.then(r => r.json())
.then(({ categories }) => {
if (!categories?.length) return;
const newGallery = [];
const newCats = [];
const existingIds = PAOLA_DATA.categories.map(c => normId(c.id));
categories.forEach(({ folder, photos }) => {
if (!photos.length) return;
const folderNorm = normId(folder);
const matchedCat = PAOLA_DATA.categories.find(c => normId(c.id) === folderNorm)
|| PAOLA_DATA.categories.find(c =>
folderNorm.includes(normId(c.id)) || normId(c.id).includes(folderNorm));
const catId = matchedCat ? matchedCat.id : folder.toLowerCase().replace(/[\s_]+/g, '-');
const catName = matchedCat ? matchedCat.name : folderToName(folder);
photos.forEach((photo, i) => {
const src = typeof photo === 'object' ? photo.src : photo;
const alt = typeof photo === 'object' ? (photo.alt || '') : '';
const title = typeof photo === 'object' ? (photo.title || '') : '';
newGallery.push({ src, alt, title, cat: catId, label: `${catName} · ${i + 1}` });
});
const cover0 = typeof photos[0] === 'object' ? photos[0].src : photos[0];
if (!matchedCat && !existingIds.includes(folderNorm)) {
newCats.push({
id: catId,
idx: String(PAOLA_DATA.categories.length + newCats.length + 1).padStart(2, '0'),
name: catName,
desc: '',
cover: cover0,
packages: [],
});
} else if (matchedCat) {
matchedCat.cover = cover0;
}
});
if (newGallery.length) setDynGallery(newGallery);
if (newCats.length) setDynCats(newCats);
})
.catch(() => {});
}, []);
// Boot fade
useEffect(() => {
const boot = document.getElementById('boot');
if (!boot) return;
const t = setTimeout(() => boot.classList.add('gone'), 350);
const t2 = setTimeout(() => boot.remove(), 1500);
return () => { clearTimeout(t); clearTimeout(t2); };
}, []);
const isDarkPage = route === 'home';
const heroCopy = heroCopyOverride || COPY[tweaks.language || 'es']?.[tweaks.heroVariant || 'amor'] || COPY.es.amor;
// Galería vacía hasta que el API responda — nunca muestra placeholders
const gallery = dynGallery ?? [];
// Solo muestra filtros de categorías que tengan al menos 1 foto real
const catsWithPhotos = new Set(gallery.map(g => g.cat));
const allCats = [...PAOLA_DATA.categories, ...dynCats]
.filter(c => catsWithPhotos.has(c.id));
let Page;
if (route === 'portfolio') Page =