Exemplo de página

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 (
setIsAdmin(false)}
onPreview={() => setIsAdminPreview(true)}
theme={theme}
/>
);
}

return (

{/* HEADER NAVBAR */}

0.5 ? ‘auto’ : ‘none’,
visibility: headerOpacity === 0 ? ‘hidden’ : ‘visible’
}}
>

handleSearchChange(e.target.value)} placeholder=”Pesquise por pessoas…” className=”bg-transparent border-none outline-none text-sm w-full font-light text-zinc-950 dark:text-white placeholder:text-zinc-500″ />

{}

{/* PAINEL LATERAL */}

setIsMenuOpen(false)} />

{}
{currentView === ‘gallery’ && (
<>
{/* HERO SECTION */}



Cobertura ao vivo

Emoção em tempo real.

Acompanhe as imagens exclusivas do casamento de Norton e Jebley


handleSearchChange(e.target.value)}
className=”w-full h-14 sm:h-16 pl-14 pr-6 bg-white dark:bg-[#111] border border-zinc-200 dark:border-zinc-800 focus:border-zinc-300 dark:focus:border-zinc-700 focus:shadow-md text-zinc-950 dark:text-white text-base rounded-full outline-none transition-all placeholder:text-zinc-400 font-light”
/>

{/* CATEGORIAS STICKY */}

{categories.map(cat => {
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 ? (
<>

{paginatedPhotos.map((photo, index) => (
setSelectedPhoto(photo)}
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’ && }

{}

@ – Tecnologia IA e diversos |

Design by: – contacto 931 81 41 11

{isAdmin && isAdminPreview && (

)}

{selectedPhoto && (
setSelectedPhoto(null)}
onNext={handleNext}
onPrev={handlePrev}
hasNext={hasNext}
hasPrev={hasPrev}
categories={categories}
isLiked={likedPhotoIds.has(selectedPhoto.id)}
onToggleLike={handleLikeToggle}
/>
)}

{showLogin && (
setShowLogin(false)}
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) */}
Fotografia do evento 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 && (

{namesText}

)}
{rolesAndDescriptions.length > 0 && (

{rolesAndDescriptions.map((role, idx) => (

{role}
{idx < rolesAndDescriptions.length - 1 && }

))}

)}

Visualização em ecrã inteiro

);
}

// ==========================================
// 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: , title: “Fotografia Premium”, desc: “Cobertura completa com estilo fotojornalístico e editorial.” },
{ icon:

Profissionais no Local

{/* Fotógrafo Jorge / Marcus */}

Fotógrafo

Jorge Carvalho

Fotógrafo Principal / Design

Videógrafa

Sofia Chen

Diretora de Fotografia (Vídeo)

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

Quinta das Flores

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

{error && (

{error}

)}


setUser(e.target.value)} className=”w-full h-12 px-4 rounded-xl bg-zinc-50 dark:bg-[#0a0a0a] border border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-600 outline-none transition-all text-zinc-900 dark:text-white font-mono” placeholder=”ID de Acesso” autoFocus />

setPass(e.target.value)} className=”w-full h-12 px-4 rounded-xl bg-zinc-50 dark:bg-[#0a0a0a] border border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-600 outline-none transition-all text-zinc-900 dark:text-white font-mono” placeholder=”••••••” />


);
}

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’ && (

Ou do dispositivo

{preview ? (
<>
Preview do carregamento

Substituir Fotografia


) : (

Clique ou arraste a imagem aqui

)}


setNames(e.target.value)} placeholder=”Ex: Joana Silva, Tiago Costa” className=”w-full h-12 px-4 rounded-xl bg-zinc-50 dark:bg-[#0a0a0a] border border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-600 outline-none transition-all text-zinc-900 dark:text-white” />

setRelation(e.target.value)} placeholder=”Ex: Padrinhos, Amigos da noiva, Avós” className=”w-full h-12 px-4 rounded-xl bg-zinc-50 dark:bg-[#0a0a0a] border border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-600 outline-none transition-all text-zinc-900 dark:text-white” />

)}

{activeTab === ‘manage_photos’ && (

{editingPhotoData ? (

Editar Fotografia

Edição