import React, { useState, useEffect, useMemo, useCallback, useRef } from ‘react’;
import {
Search, Moon, Sun,
Aperture, X,
Compass, ArrowLeft, ChevronLeft, ChevronRight, ArrowUp,
Lock, LogOut, Upload, Trash2, Plus, Image as ImageIcon, Edit2, Check, Eye,
Camera, Video, Instagram, Globe, MapPin, Calendar, Clock, Info,
Heart, Download, Share2
} from ‘lucide-react’;
const MOCK_USERS = {
1: { id: 1, name: ‘Elena L.’, avatar: ‘https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80’, bio: ‘Fotografia de luz natural. Foco na quietude.’, followers: ‘12.4k’ },
2: { id: 2, name: ‘Marcus Silva’, avatar: ‘https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=150&q=80’, bio: ‘Formas, linhas e arquitetura em tons de cinza.’, followers: ‘8.1k’ },
3: { id: 3, name: ‘Sofia Chen’, avatar: ‘https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=150&q=80’, bio: ‘Onde o oceano encontra a terra.’, followers: ‘22.3k’ }
};
// Adicionado mock de “likes” iniciais para demonstração
const INITIAL_PHOTOS = [
{ id: 101, url: ‘https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[1], tags: [‘Maria’, ‘Joana’, ‘Madrinhas’, ‘Preparativos’], width: 800, height: 1200, likes: 14 },
{ id: 102, url: ‘https://images.unsplash.com/photo-1707343843437-caacff5cfa74?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[2], tags: [‘João’, ‘Carlos’, ‘Família Silva’, ‘Recepção’], width: 1200, height: 800, likes: 5 },
{ id: 103, url: ‘https://images.unsplash.com/photo-1682687982501-1e58f813fc5b?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[3], tags: [‘António’, ‘Fátima’, ‘Pais dos noivos’, ‘Cerimônia’], width: 800, height: 1200, likes: 32 },
{ id: 104, url: ‘https://images.unsplash.com/photo-1699894013444-53982467d022?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[1], tags: [‘Ricardo’, ‘Tiago’, ‘Padrinhos’, ‘Festa’], width: 1200, height: 800, likes: 8 },
{ id: 105, url: ‘https://images.unsplash.com/photo-1707345512638-997d31a10eaa?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[2], tags: [‘Marta’, ‘José’, ‘Avós’, ‘Recepção’], width: 800, height: 1200, likes: 45 },
{ id: 106, url: ‘https://images.unsplash.com/photo-1682695794816-7b9da18ed470?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[3], tags: [‘Norton’, ‘Jebley’, ‘Noivos’, ‘Ensaio dos noivos’], width: 1200, height: 800, likes: 89 },
{ id: 107, url: ‘https://images.unsplash.com/photo-1472214103451-9374bd1c798e?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[1], tags: [‘Norton’, ‘Jebley’, ‘Alianças’, ‘Momentos especiais’], width: 800, height: 1200, likes: 112 },
{ id: 108, url: ‘https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[3], tags: [‘DJ Silva’, ‘Convidados’, ‘Pista de dança’, ‘Festa’], width: 1200, height: 800, likes: 21 },
{ id: 109, url: ‘https://images.unsplash.com/photo-1518173946687-a4c8892bbd9f?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[2], tags: [‘Ana’, ‘Pedro’, ‘Amigos da Noiva’, ‘Fotos protocolares’], width: 800, height: 1200, likes: 17 },
{ id: 110, url: ‘https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[1], tags: [‘Norton’, ‘Jebley’, ‘Saída dos noivos’], width: 1200, height: 800, likes: 67 },
{ id: 111, url: ‘https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=800&h=1200&q=80’, user: MOCK_USERS[3], tags: [‘Matilde’, ‘Lucas’, ‘Pajens e Daminhas’, ‘Festa’], width: 800, height: 1200, likes: 34 },
{ id: 112, url: ‘https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&w=1200&h=800&q=80’, user: MOCK_USERS[2], tags: [‘Norton’, ‘Jebley’, ‘Brinde’, ‘Encerramento’], width: 1200, height: 800, likes: 55 },
];
const INITIAL_CATEGORIES = [
‘Todas’,
‘Preparativos’,
‘Chegada dos convidados’,
‘Cerimônia’,
‘Saída dos noivos’,
‘Cumprimentos’,
‘Fotos protocolares’,
‘Ensaio dos noivos’,
‘Recepção’,
‘Momentos especiais’,
‘Festa’,
‘Encerramento’
];
const Button = ({ children, variant = ‘primary’, size = ‘default’, className = ”, …props }) => {
const base = “inline-flex items-center justify-center font-medium transition-all duration-300 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 tracking-wide rounded-full active:scale-95”;
const variants = {
primary: “bg-zinc-950 text-white hover:bg-zinc-800 hover:shadow-md dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-200”,
secondary: “bg-zinc-100/80 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-900/80 dark:text-zinc-50 dark:hover:bg-zinc-800 backdrop-blur-sm”,
outline: “border border-zinc-200 bg-transparent hover:bg-zinc-50 text-zinc-900 dark:border-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-900”,
ghost: “hover:bg-zinc-100 hover:text-zinc-900 text-zinc-500 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50”,
};
const sizes = {
default: “h-10 px-5 text-sm md:h-11 md:px-6 md:text-base”,
sm: “h-8 px-4 text-xs”,
lg: “h-12 px-8 text-base md:h-14 md:text-lg”,
icon: “h-10 w-10 md:h-11 md:w-11”,
};
return (
);
};
// Utilitário para remover acentos e criar um motor de busca flexível
const normalizeString = (str) => {
return str.normalize(“NFD”).replace(/[\u0300-\u036f]/g, “”).toLowerCase().trim();
};
const ITEMS_PER_PAGE = 8; // Quantidade de miniaturas carregadas a cada “scroll”
export default function App() {
// Para demonstrar o scroll infinito, vamos triplicar os dados iniciais de teste
const extendedMockPhotos = [
…INITIAL_PHOTOS,
…INITIAL_PHOTOS.map(p => ({ …p, id: p.id + 100 })),
…INITIAL_PHOTOS.map(p => ({ …p, id: p.id + 200 }))
];
const [photos, setPhotos] = useState(extendedMockPhotos);
const [categories, setCategories] = useState(INITIAL_CATEGORIES);
const [theme, setTheme] = useState(‘dark’);
const [searchQuery, setSearchQuery] = useState(”);
const [activeCategory, setActiveCategory] = useState(‘Todas’); // Separado a categoria da pesquisa
const [likedPhotoIds, setLikedPhotoIds] = useState(new Set()); // Controlo de favoritos do dispositivo atual
const [selectedPhoto, setSelectedPhoto] = useState(null);
const [scrollY, setScrollY] = useState(0);
const [showScrollTop, setShowScrollTop] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [currentView, setCurrentView] = useState(‘gallery’); // ‘gallery’ | ‘producao’ | ‘lamia’
const [page, setPage] = useState(1); // Controlo da paginação do Scroll Infinito
// Estados de Segurança / Admin
const [showLogin, setShowLogin] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const [isAdminPreview, setIsAdminPreview] = useState(false);
const galleryRef = useRef(null);
const loaderRef = useRef(null); // Ref para o trigger do scroll infinito
useEffect(() => {
document.documentElement.classList.toggle(‘dark’, theme === ‘dark’);
}, [theme]);
useEffect(() => {
document.body.style.overflow = (selectedPhoto || isMenuOpen) ? ‘hidden’ : ‘unset’;
return () => { document.body.style.overflow = ‘unset’; };
}, [selectedPhoto, isMenuOpen]);
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
setScrollY(window.scrollY);
setShowScrollTop(window.scrollY > 800);
ticking = false;
});
ticking = true;
}
};
window.addEventListener(‘scroll’, handleScroll, { passive: true });
handleScroll();
return () => window.removeEventListener(‘scroll’, handleScroll);
}, []);
const toggleTheme = () => setTheme(t => t === ‘dark’ ? ‘light’ : ‘dark’);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: ‘smooth’ });
};
const scrollToGallery = () => {
setIsMenuOpen(false);
if (galleryRef.current) {
const yOffset = -80;
const y = galleryRef.current.getBoundingClientRect().top + window.scrollY + yOffset;
window.scrollTo({ top: y, behavior: ‘smooth’ });
}
};
const handleSearchChange = (val) => {
// Intercetar a frase secreta
if (val === ‘GOLPE MUITO BAIXO 99’) {
setShowLogin(true);
setSearchQuery(”);
} else {
setSearchQuery(val);
}
};
// Função global para dar/remover gosto
const handleLikeToggle = useCallback((id, e) => {
if (e) e.stopPropagation(); // Evita abrir o modal ao clicar no coração da grelha
setLikedPhotoIds(prev => {
const newSet = new Set(prev);
const isLiking = !newSet.has(id);
if (isLiking) newSet.add(id);
else newSet.delete(id);
// Atualiza o contador global na foto correspondente
setPhotos(currentPhotos => currentPhotos.map(p => {
if (p.id === id) {
return { …p, likes: (p.likes || 0) + (isLiking ? 1 : -1) };
}
return p;
}));
return newSet;
});
}, []);
// Motor de Busca Inteligente Combinado
const filteredPhotos = useMemo(() => {
let result = photos;
// 1. Filtrar por Categoria
if (activeCategory !== ‘Todas’) {
result = result.filter(photo => photo.tags.includes(activeCategory));
}
// 2. Filtrar por Texto
if (searchQuery.trim()) {
const query = normalizeString(searchQuery);
result = result.filter(photo => {
const matchInTags = photo.tags.some(tag => normalizeString(tag).includes(query));
const matchInUser = normalizeString(photo.user.name).includes(query);
return matchInTags || matchInUser;
});
}
return result;
}, [searchQuery, activeCategory, photos]);
// Fotos visíveis na página atual (Scroll Infinito)
const paginatedPhotos = useMemo(() => {
return filteredPhotos.slice(0, page * ITEMS_PER_PAGE);
}, [filteredPhotos, page]);
// Resetar a página de scroll quando os filtros mudam
useEffect(() => {
setPage(1);
}, [searchQuery, activeCategory]);
// Lógica central do Scroll Infinito (Intersection Observer API)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const target = entries[0];
// Se a div do loader estiver visível e ainda houver fotos para carregar
if (target.isIntersecting && paginatedPhotos.length < filteredPhotos.length) {
// Delay opcional para simular pedido de rede e dar um aspeto mais orgânico
setTimeout(() => {
setPage(prev => prev + 1);
}, 300);
}
}, { rootMargin: “300px” }); // Ativa 300px antes de chegar fisicamente ao fim do scroll
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => observer.disconnect();
}, [paginatedPhotos.length, filteredPhotos.length]);
const handleSearchSubmit = (e) => {
if (e) e.preventDefault();
scrollToGallery();
};
const currentIndex = selectedPhoto ? filteredPhotos.findIndex(p => p.id === selectedPhoto.id) : -1;
const hasNext = currentIndex !== -1 && currentIndex < filteredPhotos.length - 1;
const hasPrev = currentIndex > 0;
const handleNext = useCallback(() => {
if (hasNext) setSelectedPhoto(filteredPhotos[currentIndex + 1]);
}, [hasNext, currentIndex, filteredPhotos]);
const handlePrev = useCallback(() => {
if (hasPrev) setSelectedPhoto(filteredPhotos[currentIndex – 1]);
}, [hasPrev, currentIndex, filteredPhotos]);
useEffect(() => {
const handleKeyDown = (e) => {
if (!selectedPhoto) return;
if (e.key === ‘ArrowRight’) handleNext();
if (e.key === ‘ArrowLeft’) handlePrev();
if (e.key === ‘Escape’) setSelectedPhoto(null);
};
window.addEventListener(‘keydown’, handleKeyDown);
return () => window.removeEventListener(‘keydown’, handleKeyDown);
}, [selectedPhoto, handleNext, handlePrev]);
const headerOpacity = Math.min(scrollY / 120, 1);
const isSearching = searchQuery.trim() !== ” || activeCategory !== ‘Todas’;
// Se for Admin e não estiver no modo de pré-visualização, mostra o Dashboard
if (isAdmin && !isAdminPreview) {
return (
onPreview={() => setIsAdminPreview(true)}
theme={theme}
/>
);
}
return (
{/* HEADER NAVBAR */}
visibility: headerOpacity === 0 ? ‘hidden’ : ‘visible’
}}
>
{}
{/* PAINEL LATERAL */}
{}
{currentView === ‘gallery’ && (
<>
{/* HERO SECTION */}
Cobertura ao vivo
Emoção em tempo real.
Acompanhe as imagens exclusivas do casamento de Norton e Jebley
{/* CATEGORIAS STICKY */}
const isSelected = cat === activeCategory;
return (
);
})}
{/* MASONRY GRID */}
{isSearching && filteredPhotos.length > 0 && (
Resultados
{activeCategory !== ‘Todas’ && ` em “${activeCategory}”`}
{searchQuery.trim() !== ” && ` para “${searchQuery}”`}
{filteredPhotos.length} {filteredPhotos.length === 1 ? ‘fotografia encontrada’ : ‘fotografias encontradas’}
)}
{paginatedPhotos.length > 0 ? (
<>
onToggleLike={handleLikeToggle}
isLiked={likedPhotoIds.has(photo.id)}
/>
))}
{/* Div gatilho do Scroll Infinito */}
{paginatedPhotos.length < filteredPhotos.length && (
A revelar mais momentos…
)}
>
) : (
Ainda não há fotos aqui.
A pesquisa {searchQuery ? `por “${searchQuery}”` : ”} não encontrou resultados. O nosso fotógrafo ainda está a captar memórias incríveis, volta a verificar em breve!
)}
>
)}
{currentView === ‘producao’ &&
{currentView === ‘lamia’ &&
{currentView === ‘evento’ &&
{}
{isAdmin && isAdminPreview && (
)}
{selectedPhoto && (
onNext={handleNext}
onPrev={handlePrev}
hasNext={hasNext}
hasPrev={hasPrev}
categories={categories}
isLiked={likedPhotoIds.has(selectedPhoto.id)}
onToggleLike={handleLikeToggle}
/>
)}
{showLogin && (
onSuccess={() => {
setShowLogin(false);
setIsAdmin(true);
}}
/>
)}
);
}
function PhotoCard({ photo, onClick, isLiked, onToggleLike }) {
const [isLoaded, setIsLoaded] = useState(false);
const aspectRatio = `${photo.width} / ${photo.height}`;
// Otimização de Performance (Thumbnails vs Blur)
// No WordPress, irás substituir isto pelas “Featured Images” com sizes ‘medium’/’thumbnail’ gerados nativamente pelo WP.
const thumbnailUrl = photo.url.replace(/w=\d+/, ‘w=600’).replace(/h=\d+/, ‘h=600’);
// Efeito Blur-up (Resolucão diminuta carregada instantaneamente e desfocada via CSS)
const blurPlaceholderUrl = photo.url.replace(/w=\d+/, ‘w=20’).replace(/h=\d+/, ‘h=20’) + ‘&blur=100’;
return (
{/* Imagem Placeholder com Blur Intenso */}
{/* Thumbnail de Alta Qualidade mas leve para a grelha (Lazy Load real nativo do Browser) */}
setIsLoaded(true)}
/>
{/* Botão de Gosto Integrado na Grelha */}
);
}
function PhotoModal({ photo, onClose, onNext, onPrev, hasNext, hasPrev, categories, isLiked, onToggleLike }) {
const [scale, setScale] = useState(1);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isInteracting, setIsInteracting] = useState(false);
const touchStart = useRef({ x: 0, y: 0 });
const lastTouch = useRef({ x: 0, y: 0 });
const pinchStartDist = useRef(0);
const lastScale = useRef(1);
const isDragging = useRef(false);
const lastTap = useRef(0);
useEffect(() => {
setScale(1);
setPosition({ x: 0, y: 0 });
}, [photo]);
// Versão de máxima resolução para o Modal (Ecrã Inteiro)
// No WordPress deves mapear isto para o array original do URL (‘full’) da Media Library
const highResUrl = photo.url
.replace(/w=\d+/, ‘w=2400’)
.replace(/h=\d+/, ‘h=2400’);
// Funcionalidade de Download
const handleDownload = async (e) => {
e.stopPropagation();
try {
const response = await fetch(highResUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement(‘a’);
link.href = url;
link.download = `claver_filmes_${photo.id}.jpg`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
// Fallback caso o fetch bloqueie por CORS nalguns browsers
window.open(highResUrl, ‘_blank’);
}
};
// Funcionalidade de Partilha Nativa
const handleShare = async (e) => {
e.stopPropagation();
if (navigator.share) {
try {
await navigator.share({
title: ‘Claver Filmes – Casamento’,
text: ‘Olha esta fotografia incrível!’,
url: highResUrl
});
} catch (error) {
console.log(‘Partilha cancelada ou falhou.’, error);
}
} else {
alert(‘A funcionalidade de partilha nativa não é suportada neste dispositivo/browser.’);
}
};
const getDistance = (touches) => {
const dx = touches[0].clientX – touches[1].clientX;
const dy = touches[0].clientY – touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
};
const handleTouchStart = (e) => {
setIsInteracting(true);
if (e.touches.length === 2) {
pinchStartDist.current = getDistance(e.touches);
lastScale.current = scale;
isDragging.current = false;
} else if (e.touches.length === 1) {
touchStart.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
lastTouch.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
isDragging.current = scale > 1;
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2) {
const dist = getDistance(e.touches);
const newScale = Math.min(Math.max(1, lastScale.current * (dist / pinchStartDist.current)), 5);
setScale(newScale);
} else if (e.touches.length === 1 && isDragging.current) {
const deltaX = e.touches[0].clientX – lastTouch.current.x;
const deltaY = e.touches[0].clientY – lastTouch.current.y;
setPosition(prev => ({ x: prev.x + deltaX, y: prev.y + deltaY }));
lastTouch.current = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
};
const handleTouchEnd = (e) => {
setIsInteracting(false);
if (e.touches.length < 2) {
lastScale.current = scale;
}
const now = Date.now();
const isDoubleTap = (now - lastTap.current < 300) && e.changedTouches.length === 1;
lastTap.current = now;
if (isDoubleTap) {
if (scale > 1) {
setScale(1);
setPosition({ x: 0, y: 0 });
} else {
setScale(2.5);
setPosition({ x: 0, y: 0 });
}
return;
}
if (scale <= 1) {
setPosition({ x: 0, y: 0 });
setScale(1);
if (e.changedTouches.length === 1 && !isDragging.current) {
const endX = e.changedTouches[0].clientX;
const swipeThreshold = 50;
if (touchStart.current.x - endX > swipeThreshold && hasNext) onNext();
if (endX – touchStart.current.x > swipeThreshold && hasPrev) onPrev();
}
}
isDragging.current = false;
};
const handleDoubleClick = () => {
if (scale > 1) {
setScale(1);
setPosition({ x: 0, y: 0 });
} else {
setScale(2.5);
setPosition({ x: 0, y: 0 });
}
};
const lowerCategories = categories.map(c => c.toLowerCase());
const genericTerms = [‘noivos’, ‘alianças’, ‘festa’, ‘convidados’, ‘pista de dança’, ‘brinde’, ‘bolo’, ‘dj’, ‘amigos’, ‘família’, ‘padrinhos’, ‘madrinhas’, ‘pais’, ‘avós’, ‘crianças’, ‘pajens’, ‘daminhas’];
const isGeneric = (tag) => {
const lowerTag = tag.toLowerCase();
return lowerCategories.includes(lowerTag) || genericTerms.some(term => lowerTag.includes(term));
};
const names = photo.tags.filter(tag => !isGeneric(tag));
const rolesAndDescriptions = photo.tags.filter(tag => isGeneric(tag));
let namesText = ”;
if (names.length === 1) namesText = names[0];
else if (names.length === 2) namesText = `${names[0]} e ${names[1]}`;
else if (names.length > 2) namesText = `${names.slice(0, -1).join(‘, ‘)} e ${names[names.length – 1]}`;
return (
{/* Barra de Ferramentas Superior */}
{hasPrev && (
)}
{hasNext && (
)}
{namesText}
)}
{rolesAndDescriptions.length > 0 && (
{rolesAndDescriptions.map((role, idx) => (
{role}
{idx < rolesAndDescriptions.length - 1 && }
))}
)}
);
}
// ==========================================
// PÁGINA DA PRODUÇÃO / EQUIPA
// ==========================================
function ProducaoView() {
return (
A equipa por trás das lentes.
Somos a Claver Filmes. Especialistas em eternizar momentos através de uma perspetiva cinematográfica e documental. A nossa missão é garantir que cada emoção do seu dia seja preservada para sempre com a mais alta qualidade.
{ icon:
{ icon: , title: “Cinematografia”, desc: “Vídeos em 4K captados com equipamento de cinema digital.” },
{ icon:
].map((item, i) => (
{item.title}
{item.desc}
))}
Profissionais no Local
Gostou da nossa abordagem?
Estamos disponíveis para o seu próximo evento. Acompanhe o nosso portfólio completo nas nossas plataformas.
);
}
// ==========================================
// PÁGINA DA LAMIA APP (Tecnologia IA)
// ==========================================
function LamiaView() {
return (
Lamia APP
A infraestrutura tecnológica por trás desta galeria. Inteligência artificial, reconhecimento de padrões e entrega de memórias em tempo recorde.
O que fazemos?
-
Distribuição em Tempo Real: As fotos chegam da câmara diretamente para o seu dispositivo durante o evento. -
Pesquisa Inteligente: Algoritmos avançados permitem encontrar convidados pelo nome quase instantaneamente. -
Otimização de Imagem: Compressão inteligente que não sacrifica a qualidade visual.
);
}
// ==========================================
// PÁGINA DO EVENTO
// ==========================================
function EventoView() {
return (
Celebrando o Amor.
Todos os detalhes sobre o grande dia de Norton & Jebley. Estamos muito felizes por partilhar este momento especial convosco.
Data e Hora
Cerimónia às 15:00
Recepção às 17:30
Sábado, 24 de Agosto
Localização
Rua dos Romances, nº 123
Sintra, Portugal
Dress Code
Formal / Passeio Completo. Pedimos gentilmente a todos os convidados que evitem os tons de branco e bege claro, reservando-os para a noiva.
);
}
// ==========================================
// COMPONENTES DE ADMINISTRAÇÃO
// ==========================================
function AdminLogin({ onClose, onSuccess }) {
const [user, setUser] = useState(”);
const [pass, setPass] = useState(”);
const [error, setError] = useState(”);
const handleSubmit = (e) => {
e.preventDefault();
if (user === ‘931814111’ && pass === ‘252493’) {
onSuccess();
} else {
setError(‘As credenciais inseridas estão incorretas.’);
}
};
return (
Acesso Restrito
);
}
function AdminDashboard({ photos, setPhotos, categories, setCategories, onLogout, onPreview, theme }) {
const [activeTab, setActiveTab] = useState(‘upload’);
const [preview, setPreview] = useState(null);
const [imageUrl, setImageUrl] = useState(”);
const [names, setNames] = useState(”);
const [relation, setRelation] = useState(”);
const [selectedCategory, setSelectedCategory] = useState(‘Todas’);
const [newCat, setNewCat] = useState(”);
const [showConfirmUpload, setShowConfirmUpload] = useState(false);
const [editingCat, setEditingCat] = useState(null);
const [editCatValue, setEditCatValue] = useState(”);
const [editingPhotoData, setEditingPhotoData] = useState(null);
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
setImageUrl(”);
};
reader.readAsDataURL(file);
}
};
const handleUrlChange = (e) => {
const val = e.target.value;
setImageUrl(val);
setPreview(val);
};
const handleUpload = (e) => {
e.preventDefault();
if (!preview) {
alert(‘Por favor, selecione uma fotografia do seu dispositivo.’);
return;
}
setShowConfirmUpload(true);
};
const confirmUpload = () => {
const newTags = [];
if (names.trim()) {
names.split(‘,’).forEach(n => newTags.push(n.trim()));
}
if (relation.trim()) {
newTags.push(relation.trim());
}
if (selectedCategory && selectedCategory !== ‘Todas’) {
newTags.push(selectedCategory);
}
const newPhoto = {
id: Date.now(),
url: preview,
user: { id: 99, name: ‘Claver Administrador’, avatar: ”, bio: ”, followers: ” },
tags: newTags,
width: 1200,
height: 800,
likes: 0 // Nova foto começa com zero gostos
};
setPhotos([newPhoto, …photos]);
setPreview(null);
setImageUrl(”);
setNames(”);
setRelation(”);
setSelectedCategory(‘Todas’);
setShowConfirmUpload(false);
};
const handleAddCategory = (e) => {
e.preventDefault();
if (newCat.trim() && !categories.includes(newCat.trim())) {
setCategories([…categories, newCat.trim()]);
setNewCat(”);
}
};
const handleDeleteCategory = (cat) => {
if (cat === ‘Todas’) return;
setCategories(categories.filter(c => c !== cat));
};
const startEditCategory = (cat) => {
setEditingCat(cat);
setEditCatValue(cat);
};
const saveEditCategory = (oldCat) => {
const newCatName = editCatValue.trim();
if (!newCatName || newCatName === oldCat) {
setEditingCat(null);
return;
}
setCategories(categories.map(c => c === oldCat ? newCatName : c));
setPhotos(photos.map(p => ({
…p,
tags: p.tags.map(t => t === oldCat ? newCatName : t)
})));
setEditingCat(null);
};
const handleDeletePhoto = (id) => {
if (window.confirm(“Tem a certeza que deseja apagar esta fotografia?”)) {
setPhotos(photos.filter(p => p.id !== id));
}
};
const startEditPhoto = (photo) => {
const cat = photo.tags.find(t => categories.includes(t)) || ‘Todas’;
const otherTags = photo.tags.filter(t => t !== cat).join(‘, ‘);
setEditingPhotoData({
…photo,
editCategory: cat,
editTagsString: otherTags
});
};
const saveEditPhoto = (e) => {
e.preventDefault();
const newTags = editingPhotoData.editTagsString.split(‘,’).map(t => t.trim()).filter(t => t);
if (editingPhotoData.editCategory && editingPhotoData.editCategory !== ‘Todas’) {
newTags.push(editingPhotoData.editCategory);
}
setPhotos(photos.map(p => p.id === editingPhotoData.id ? {
…p,
tags: newTags
} : p));
setEditingPhotoData(null);
};
return (
Claver Studio
Painel do Fotógrafo
{activeTab === ‘upload’ && (
)}
{activeTab === ‘manage_photos’ && (
Editar Fotografia
) : (
Gerir Galeria
Podes editar as informações de qualquer foto ou removê-las permanentemente.
{photos.length === 0 ? (
) : (
{/* Indicador de Gostos no Admin */}
{photo.likes || 0}
{photo.tags.join(‘, ‘)}
))}
)}
)}
)}
{activeTab === ‘categories’ && (
Estrutura do Evento
Edite, adicione ou remova as categorias que aparecerão no menu principal do site dos noivos.
{editingCat === cat ? (
onKeyDown={(e) => e.key === ‘Enter’ && saveEditCategory(cat)}
className=”w-full h-10 px-3 rounded-lg bg-white dark:bg-[#111] border border-zinc-300 dark:border-zinc-600 focus:border-zinc-500 outline-none text-zinc-900 dark:text-white font-medium”
autoFocus
/>
) : (
{cat}
)}
{cat !== ‘Todas’ && (
<>
>
) : (
<>
>
)}
)}
))}
)}
{showConfirmUpload && (
Confirmar Publicação
A fotografia será adicionada à galeria principal com os seguintes detalhes:
{names ? names : Sem nomes identificados}
{relation ? relation : Sem ligação}
{selectedCategory}
)}
);
}