This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// Imported from investidor/resources/js/modules/news/sections/about/AboutStory.tsx; original content preserved.
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Trans } from './source-text';
|
||||
|
||||
const aboutimgstory = '/assets/imgs/aboutimgstory.png';
|
||||
|
||||
interface CounterProps {
|
||||
end: number;
|
||||
suffix?: string;
|
||||
}
|
||||
|
||||
const Counter = ({ end, suffix = '' }: CounterProps) => {
|
||||
const [count, setCount] = useState<number>(0);
|
||||
const countRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const duration = 2000;
|
||||
const increment = end / (duration / 16);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
countRef.current += increment;
|
||||
if (countRef.current >= end) {
|
||||
clearInterval(timer);
|
||||
countRef.current = end;
|
||||
}
|
||||
setCount(Math.floor(countRef.current));
|
||||
}, 16);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [end]);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{count}
|
||||
{suffix}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const AboutStory = () => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const [counted, setCounted] = useState<boolean>(false);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIsVisible(true);
|
||||
setCounted(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 },
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className={`bg-primary px-6 transition-all duration-1000 md:px-12 lg:px-20 ${
|
||||
isVisible
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-10 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="grid grid-cols-1 items-stretch gap-x-12 gap-y-2 lg:grid-cols-2">
|
||||
{/* Imagem */}
|
||||
<div
|
||||
className={`relative transition-all delay-200 duration-1000 ${
|
||||
isVisible
|
||||
? 'translate-x-0 opacity-100'
|
||||
: '-translate-x-12 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="group relative h-64 overflow-hidden rounded-2xl md:h-72">
|
||||
<img
|
||||
src={aboutimgstory}
|
||||
alt="Equipe Tempo"
|
||||
className="h-full w-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src =
|
||||
'/assets/images/info.png';
|
||||
}}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0 bg-black/12" />
|
||||
|
||||
<div className="absolute right-4 bottom-4 left-4">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:gap-8">
|
||||
<div className="rounded-xl bg-black/20 p-3 backdrop-blur-sm transition-all sm:bg-transparent sm:p-0 sm:backdrop-blur-none">
|
||||
<div
|
||||
className="text-4xl font-black tracking-tighter text-white md:text-6xl"
|
||||
style={{
|
||||
textShadow:
|
||||
'0 4px 12px rgba(0,0,0,0.95), 0 2px 6px rgba(0,0,0,0.9)',
|
||||
}}
|
||||
>
|
||||
{counted ? (
|
||||
<Counter end={50} suffix="+" />
|
||||
) : (
|
||||
'50+'
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 text-xs font-bold tracking-widest text-white uppercase opacity-90 md:text-lg"
|
||||
style={{
|
||||
textShadow:
|
||||
'0 3px 10px rgba(0,0,0,0.9), 0 1px 4px black',
|
||||
}}
|
||||
>
|
||||
<Trans>Anos de Experiência</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl bg-black/20 p-3 backdrop-blur-sm transition-all sm:bg-transparent sm:p-0 sm:backdrop-blur-none">
|
||||
<div
|
||||
className="text-4xl font-black tracking-tighter text-[#ED1C24] md:text-6xl"
|
||||
style={{
|
||||
textShadow:
|
||||
'0 4px 12px rgba(0,0,0,0.95), 0 2px 6px rgba(0,0,0,0.9)',
|
||||
}}
|
||||
>
|
||||
{counted ? (
|
||||
<Counter end={200} suffix="+" />
|
||||
) : (
|
||||
'200+'
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 text-xs font-bold tracking-widest text-white uppercase opacity-90 md:text-lg"
|
||||
style={{
|
||||
textShadow:
|
||||
'0 3px 10px rgba(0,0,0,0.9), 0 1px 4px black',
|
||||
}}
|
||||
>
|
||||
<Trans>Projetos Realizados</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute -right-6 -bottom-6 -z-10 h-32 w-32 rounded-2xl border-2 border-red-600/30 transition-all delay-500 duration-1000 ${
|
||||
isVisible
|
||||
? 'scale-100 opacity-100'
|
||||
: 'scale-0 opacity-0'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Texto */}
|
||||
<div
|
||||
className={`flex flex-col justify-center space-y-4 transition-all delay-500 duration-1000 ${
|
||||
isVisible
|
||||
? 'translate-x-0 opacity-100'
|
||||
: '-translate-x-12 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<p className="text-base leading-relaxed font-light text-white md:text-lg">
|
||||
<Trans>
|
||||
Somos detentores de um vasto património cultural
|
||||
e tecnológico, resultado da integração e da
|
||||
cooperação entre diversas empresas com ampla
|
||||
experiência nas áreas de Tecnologias de
|
||||
Informação e Comunicação, produção e
|
||||
disseminação de conteúdos informativos, educação
|
||||
vocacional e gestão empresarial.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-base leading-relaxed font-light text-white md:text-lg">
|
||||
<Trans>
|
||||
Dispomos de uma equipa qualificada, com sólida
|
||||
experiência na concepção e implementação de
|
||||
soluções de TI, de cibersegurança, de
|
||||
desenvolvimento institucional, de reengenharia
|
||||
de processos organizacionais e de formação
|
||||
profissional.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default AboutStory;
|
||||
@@ -0,0 +1,156 @@
|
||||
// Imported from investidor; return destinations are restricted to this platform.
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { router } from '@inertiajs/react';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Trans } from './source-text'; // Import Trans component
|
||||
|
||||
interface BackButtonProps {
|
||||
to?: string;
|
||||
label?: React.ReactNode; // Change type to React.ReactNode to accept <Trans> component
|
||||
className?: string;
|
||||
theme?: 'light' | 'dark';
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const RETURN_URL_KEY = 'news:return-url';
|
||||
|
||||
function normalizeInternalUrl(value: string | null): string | null {
|
||||
if (!value || typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = value.startsWith('http')
|
||||
? new URL(value)
|
||||
: new URL(value, window.location.origin);
|
||||
|
||||
if (url.origin !== window.location.origin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = `${url.pathname}${url.search}${url.hash}`;
|
||||
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
return normalized !== current ? normalized : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredReturnUrl(): string | null {
|
||||
try {
|
||||
return normalizeInternalUrl(sessionStorage.getItem(RETURN_URL_KEY));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getReturnUrlFromPage(): string | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const fromParam = normalizeInternalUrl(params.get('from'));
|
||||
|
||||
if (fromParam) {
|
||||
return fromParam;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getInternalReferrerUrl(): string | null {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalizeInternalUrl(document.referrer);
|
||||
}
|
||||
|
||||
export const BackButton: React.FC<BackButtonProps> = ({
|
||||
to,
|
||||
label = <Trans>Voltar</Trans>, // Use Trans component for default label
|
||||
className = '',
|
||||
theme = 'light',
|
||||
onClick,
|
||||
}) => {
|
||||
const [showFloatingButton, setShowFloatingButton] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setShowFloatingButton(window.scrollY > 220);
|
||||
};
|
||||
|
||||
handleScroll();
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const themeClasses =
|
||||
theme === 'dark'
|
||||
? 'border-white/20 bg-white/10 text-white shadow-lg backdrop-blur-md hover:bg-white/20'
|
||||
: 'border-red-100 bg-white text-[#ED1C24] shadow-sm hover:border-red-200 hover:bg-red-50';
|
||||
|
||||
const baseClasses =
|
||||
'inline-flex items-center gap-2 rounded-full border px-4 py-2 text-sm font-semibold transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-[#ED1C24]/25 active:scale-[0.98]';
|
||||
|
||||
const floatingClasses = [
|
||||
'fixed bottom-5 left-4 z-50 max-w-[calc(100vw-2rem)] sm:bottom-6 sm:left-6',
|
||||
'border-red-100 bg-white text-[#ED1C24] shadow-xl ring-1 ring-black/5 hover:border-red-200 hover:bg-red-50',
|
||||
showFloatingButton
|
||||
? 'pointer-events-auto translate-y-0 opacity-100'
|
||||
: 'pointer-events-none translate-y-3 opacity-0',
|
||||
].join(' ');
|
||||
|
||||
const renderContent = () => (
|
||||
<>
|
||||
<ArrowLeft size={16} aria-hidden="true" />
|
||||
<span className="truncate">{label}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (onClick) {
|
||||
onClick();
|
||||
return;
|
||||
}
|
||||
|
||||
const explicitReturnUrl = getReturnUrlFromPage();
|
||||
const storedReturnUrl = getStoredReturnUrl();
|
||||
|
||||
if (explicitReturnUrl) {
|
||||
router.visit(explicitReturnUrl);
|
||||
} else if (storedReturnUrl) {
|
||||
router.visit(storedReturnUrl);
|
||||
} else {
|
||||
router.visit(getInternalReferrerUrl() || to || '/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`${baseClasses} ${themeClasses} ${className}`}
|
||||
aria-label={typeof label === 'string' ? label : undefined} // aria-label should be a string
|
||||
>
|
||||
{renderContent()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`${baseClasses} ${floatingClasses}`}
|
||||
aria-label={typeof label === 'string' ? label : undefined} // aria-label should be a string
|
||||
aria-hidden={!showFloatingButton}
|
||||
tabIndex={showFloatingButton ? undefined : -1}
|
||||
>
|
||||
{renderContent()}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackButton;
|
||||
@@ -0,0 +1,218 @@
|
||||
// Imported from investidor/resources/js/modules/news/sections/about/CompanyValues.tsx; original content preserved.
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Users, Lightbulb, Globe, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Trans } from './source-text';
|
||||
|
||||
const img01 = '/assets/imgs/img01.jpg';
|
||||
const img02 = '/assets/imgs/img02.jpg';
|
||||
const img03 = '/assets/imgs/img03.jpg';
|
||||
const img04 = '/assets/imgs/img04.jpg';
|
||||
|
||||
interface CompanyValue {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
image?: string;
|
||||
icon?: React.ReactNode;
|
||||
color?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const CompanyValues = () => {
|
||||
const [hoveredId, setHoveredId] = useState<number | null>(null);
|
||||
const [isInView, setIsInView] = useState<boolean>(false);
|
||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (sectionRef.current) {
|
||||
const rect = sectionRef.current.getBoundingClientRect();
|
||||
setIsInView(
|
||||
rect.top < window.innerHeight * 0.8 && rect.bottom > 100,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
handleScroll();
|
||||
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const values: CompanyValue[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Diversidade & Inclusão',
|
||||
description:
|
||||
'Representando todas as vozes e realidades de Moçambique',
|
||||
image: img01,
|
||||
icon: <Users className="h-6 w-6" />,
|
||||
color: 'from-red-500 to-orange-500',
|
||||
label: 'Valor Central',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Inovação',
|
||||
description:
|
||||
'Soluções tecnológicas e sustentáveis que impulsionam o desenvolvimento',
|
||||
image: img02,
|
||||
icon: <Lightbulb className="h-6 w-6" />,
|
||||
color: 'from-red-600 to-red-700',
|
||||
label: 'Essencial',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Excelência',
|
||||
description:
|
||||
'Acreditamos que cada história merece ser contada com propósito. Combinamos visão criativa e conhecimento estratégico para dar vida às ideias.',
|
||||
image: img03,
|
||||
label: 'Somos mais que comunicação, somos transformação',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'Impacto Positivo',
|
||||
description:
|
||||
'Criamos marcas que inspiram, conteúdo que conecta e experiências memoráveis. Transformamos visão em realidade com estratégia, criatividade e tecnologia.',
|
||||
image: img04,
|
||||
icon: <Globe className="h-6 w-6" />,
|
||||
color: 'from-red-600 to-red-700',
|
||||
label: 'Propósito',
|
||||
},
|
||||
];
|
||||
|
||||
const toggleExpand = (id: number) => {
|
||||
setExpandedId(expandedId === id ? null : id);
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className="bg-primary relative overflow-hidden py-4"
|
||||
>
|
||||
<div className="mx-auto mb-4 max-w-7xl space-y-2 px-6 text-center">
|
||||
<div
|
||||
className={`transition-all duration-1000 ${isInView ? 'translate-y-0 opacity-100' : 'translate-y-8 opacity-0'}`}
|
||||
>
|
||||
<h2 className="text-3xl font-semibold tracking-wide text-white md:text-3xl">
|
||||
<span
|
||||
className="font-bold italic not-italic"
|
||||
style={{ color: '#FFFFFF' }}
|
||||
>
|
||||
<Trans>Valores</Trans>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-7xl px-6">
|
||||
<div className="grid grid-cols-1 items-stretch gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{values.map((value, i) => {
|
||||
const isExpanded = expandedId === value.id;
|
||||
const hasLongText = value.id === 3 || value.id === 4;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value.id}
|
||||
onMouseEnter={() => setHoveredId(value.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
className={`group relative transition-all duration-500 ${isInView ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'}`}
|
||||
style={{ transitionDelay: `${i * 100}ms` }}
|
||||
>
|
||||
<div
|
||||
className={`relative flex h-full flex-col overflow-hidden rounded-2xl border bg-white/5 backdrop-blur-xl transition-all duration-500 ${
|
||||
hoveredId === value.id
|
||||
? 'translate-y-[-6px] border-red-500/40 shadow-2xl shadow-red-600/20'
|
||||
: 'border-white/10'
|
||||
}`}
|
||||
>
|
||||
<div className="relative flex h-44 flex-1 flex-col overflow-hidden">
|
||||
<img
|
||||
src={value.image}
|
||||
alt={value.title}
|
||||
className={`h-full w-full object-cover transition-transform duration-700 ${
|
||||
hoveredId === value.id
|
||||
? 'scale-110'
|
||||
: 'scale-100'
|
||||
}`}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/40" />
|
||||
|
||||
{value.icon && value.color && (
|
||||
<div
|
||||
className={`absolute top-4 left-4 h-11 w-11 rounded-xl bg-gradient-to-br ${value.color} flex items-center justify-center text-white shadow-lg transition-transform duration-500 ${
|
||||
hoveredId === value.id
|
||||
? 'scale-110 rotate-12'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
{value.icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{value.label && (
|
||||
<div className="absolute top-4 right-5 px-3 py-1 text-lg font-bold text-white drop-shadow-md md:text-xl">
|
||||
<Trans>{value.label}</Trans>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col bg-gradient-to-t from-black/60 via-black/30 to-transparent p-5">
|
||||
<h3 className="mb-2 text-2xl font-bold text-white drop-shadow-md md:text-3xl">
|
||||
<Trans>{value.title}</Trans>
|
||||
</h3>
|
||||
<p
|
||||
className={`text-sm leading-relaxed text-gray-100 drop-shadow-md md:text-base ${
|
||||
!isExpanded && hasLongText
|
||||
? 'line-clamp-2'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<Trans>{value.description}</Trans>
|
||||
</p>
|
||||
{hasLongText && (
|
||||
<button
|
||||
onClick={() =>
|
||||
toggleExpand(value.id)
|
||||
}
|
||||
className="mt-1 flex items-center gap-1 text-sm font-medium text-red-400 transition-colors hover:text-red-300"
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<Trans>Ver menos</Trans>{' '}
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trans>Ver mais</Trans>{' '}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hoveredId === value.id && (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl">
|
||||
<div className="animate-shimmer absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
.animate-shimmer { animation: shimmer 1.5s ease-in-out infinite; }
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyValues;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Imported from investidor/resources/js/modules/news/sections/about/MissionVision.tsx; original content preserved.
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Target, Eye } from 'lucide-react';
|
||||
import { Trans } from './source-text';
|
||||
|
||||
interface MissionVisionItem {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
gradient: string;
|
||||
}
|
||||
|
||||
export const MissionVision = () => {
|
||||
const [hoveredId, setHoveredId] = useState<number | null>(null);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const [isInView, setIsInView] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => setIsInView(entry.isIntersecting),
|
||||
{ threshold: 0.1, rootMargin: '0px 0px -80px 0px' },
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const items: MissionVisionItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Missão',
|
||||
description:
|
||||
'Fornecer soluções integradas e inovadoras em comércio, comunicação, tecnologia e informação, com excelência na intermediação comercial, na informação pública e em plataformas digitais promovendo crescimento sustentável e impacto positivo no mercado moçambicano.',
|
||||
icon: <Target className="h-7 w-7 sm:h-8 sm:w-8" />,
|
||||
gradient: 'from-red-500 to-red-600',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Visão',
|
||||
description:
|
||||
'Ser a referência líder em Moçambique, impulsionando o progresso do país através de soluções inovadoras e sustentáveis, para um futuro mais conectado, próspero e dinâmico.',
|
||||
icon: <Eye className="h-7 w-7 sm:h-8 sm:w-8" />,
|
||||
gradient: 'from-red-600 to-red-800',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section ref={sectionRef} className="relative overflow-hidden py-6">
|
||||
<div className="relative mx-auto max-w-7xl px-5 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 gap-4 sm:gap-5 lg:grid-cols-2 lg:gap-6">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onMouseEnter={() => setHoveredId(item.id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
className={`transition-all duration-1200 ease-in-out ${isInView ? 'translate-y-0 scale-100 opacity-100' : 'translate-y-6 scale-95 opacity-0'} `}
|
||||
style={{ transitionDelay: `${index * 200}ms` }}
|
||||
>
|
||||
<div
|
||||
className={`relative h-full rounded-2xl border bg-white/5 backdrop-blur-lg transition-all duration-700 ease-in-out sm:rounded-3xl ${
|
||||
hoveredId === item.id
|
||||
? '-translate-y-2 border-red-500/50 shadow-xl shadow-red-600/20 sm:-translate-y-3'
|
||||
: 'border-white/10 shadow-sm'
|
||||
} `}
|
||||
>
|
||||
<div className="p-5 sm:p-6 lg:p-8">
|
||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start sm:gap-6">
|
||||
<div
|
||||
className={`h-14 w-14 flex-shrink-0 rounded-xl bg-gradient-to-br sm:h-16 sm:w-16 sm:rounded-2xl ${item.gradient} flex items-center justify-center p-3 text-white shadow-lg transition-all duration-700 ease-in-out sm:p-3.5 ${
|
||||
hoveredId === item.id
|
||||
? 'scale-125 rotate-12 animate-pulse shadow-2xl'
|
||||
: 'animate-float scale-100 rotate-0'
|
||||
} `}
|
||||
>
|
||||
{item.icon}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 min-w-0 flex-1 text-center sm:mt-0 sm:text-left">
|
||||
<h3
|
||||
className={`mb-2 text-2xl font-light tracking-tight transition-colors duration-500 sm:mb-3 sm:text-3xl lg:text-4xl ${hoveredId === item.id ? 'text-red-400' : 'text-red-500'} `}
|
||||
>
|
||||
<Trans>{item.title}</Trans>
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed font-light text-[#ffffff] sm:text-base lg:text-lg">
|
||||
<Trans>
|
||||
{item.description}
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hoveredId === item.id && (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-2xl sm:rounded-3xl">
|
||||
<div className="animate-shimmer absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(200%); }
|
||||
}
|
||||
.animate-shimmer { animation: shimmer 2.5s ease-in-out infinite; }
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
.animate-float { animation: float 3s ease-in-out infinite; }
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MissionVision;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
const InstituitionalMaps = () => {
|
||||
// Google Maps EMBED – Prédio Revista Tempo (Maputo)
|
||||
const mapUrl =
|
||||
'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3586.601889886872!2d32.5840525!3d-25.9697003!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x1ee69b0710fc41f7%3A0x4fa9cf4e964b7e50!2sPr%C3%A9dio%20Revista%20Tempo!5e0!3m2!1spt-PT!2smz!4v1733750000000';
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{/* MAPA */}
|
||||
<iframe
|
||||
src={mapUrl}
|
||||
className="absolute inset-0 h-full w-full rounded-xl"
|
||||
style={{ border: 0 }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Localização - Prédio Revista Tempo, Maputo"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstituitionalMaps;
|
||||
@@ -0,0 +1,36 @@
|
||||
// Empty-slot presentation copied from investidor's AdvertisementSlot.
|
||||
// Preserve the original reserved spaces without importing its advertising backend or tracking.
|
||||
export function AdvertisementSlot({
|
||||
position,
|
||||
className = '',
|
||||
}: {
|
||||
page: string;
|
||||
position: string;
|
||||
slot?: string;
|
||||
limit?: number;
|
||||
fallbackToUnslotted?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const placement = position === 'top' ? 'article.top' : 'article.bottom';
|
||||
return (
|
||||
<aside
|
||||
aria-label={`Espaço de publicidade ${placement}`}
|
||||
className={`mx-auto flex w-full items-center justify-center overflow-hidden border border-dashed border-slate-300 bg-slate-50/80 text-center ${className}`}
|
||||
style={{ aspectRatio: '970 / 250', maxWidth: '970px' }}
|
||||
data-ad-placement={placement}
|
||||
data-ad-state="empty"
|
||||
>
|
||||
<div className="px-4 py-6">
|
||||
<p className="text-[10px] font-semibold tracking-[0.2em] text-slate-400 uppercase">
|
||||
Publicidade
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Espaço disponível para publicidade
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400">
|
||||
970 × 250px · {placement}
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useTempoUi } from '@/components/tempo-brand';
|
||||
|
||||
// Bridge the original Trans calls to this platform's existing language context.
|
||||
export function Trans({ children }: { children: string }) {
|
||||
const { t } = useTempoUi();
|
||||
return <span>{t(children)}</span>;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import { ChevronDown, LogOut, UserRound } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ChevronDown,
|
||||
ArrowUpRight,
|
||||
LogOut,
|
||||
Menu,
|
||||
UserRound,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
import { useNavigationContext } from '@/components/navigation-context';
|
||||
import { TempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
|
||||
import {
|
||||
@@ -61,6 +68,11 @@ export function PortalGroupedMenu() {
|
||||
const current = currentNavigationGroup(groups, url);
|
||||
const currentItem = currentNavigationItem(groups, url);
|
||||
const [loggingOut, setLoggingOut] = useState(false);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const institutionalId = useId();
|
||||
const toolsId = useId();
|
||||
const toggle = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => router.on('navigate', () => setExpanded(false)), []);
|
||||
const items = (children: PortalNavItem[]) =>
|
||||
children.map((item) =>
|
||||
item.children ? (
|
||||
@@ -97,101 +109,100 @@ export function PortalGroupedMenu() {
|
||||
!['home', 'discovery', 'realization', 'support'].includes(group.id),
|
||||
);
|
||||
return (
|
||||
<header className="tempo-header" lang={language}>
|
||||
<div className="tempo-header-inner mx-auto flex max-w-[1440px] flex-wrap items-center justify-between gap-x-3 gap-y-0 px-4 sm:px-6 xl:min-h-22">
|
||||
<header
|
||||
className="tempo-header"
|
||||
lang={language}
|
||||
data-expanded={expanded || undefined}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
expanded &&
|
||||
!event.defaultPrevented
|
||||
) {
|
||||
setExpanded(false);
|
||||
toggle.current?.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="tempo-header-inner">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Tempo — início"
|
||||
className="tempo-logo-link my-4 shrink-0"
|
||||
className="tempo-logo-link"
|
||||
>
|
||||
<img
|
||||
src="/images/logo/logo-vermelho.png"
|
||||
alt="Tempo"
|
||||
width="1787"
|
||||
height="564"
|
||||
className="h-9 w-auto object-contain sm:h-10"
|
||||
className="tempo-header-logo"
|
||||
/>
|
||||
</Link>
|
||||
<nav
|
||||
aria-label="Áreas do portal"
|
||||
className="order-3 w-full border-t border-white/10 xl:order-none xl:w-auto xl:flex-1 xl:border-0"
|
||||
id={institutionalId}
|
||||
aria-label="Navegação institucional"
|
||||
className="tempo-primary-navigation"
|
||||
>
|
||||
<ul className="flex items-center justify-between gap-0 py-2 xl:justify-center xl:py-0">
|
||||
<ul>
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
current === 'home' ? 'page' : undefined
|
||||
url.split('?')[0] === '/'
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
current === 'home' || undefined
|
||||
url.split('?')[0] === '/' || undefined
|
||||
}
|
||||
>
|
||||
{t('Início')}
|
||||
</Link>
|
||||
</li>
|
||||
{groups
|
||||
.filter((group) =>
|
||||
[
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
<li key={group.id}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
current === group.id ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
{t(group.label)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{current === group.id && (
|
||||
<span className="sr-only">
|
||||
Grupo actual
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={12}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
{items(group.children ?? [])}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<a
|
||||
href="https://revista.tempo.co.mz/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="tempo-nav-trigger"
|
||||
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
|
||||
>
|
||||
{t('Revista Tempo')}
|
||||
<ArrowUpRight size={14} aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
{[
|
||||
['/sobre', 'Sobre nós'],
|
||||
['/contacto', 'Contacto'],
|
||||
].map(([href, label]) => (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className="tempo-nav-trigger"
|
||||
aria-current={
|
||||
url.split('?')[0] === href
|
||||
? 'page'
|
||||
: undefined
|
||||
}
|
||||
data-current-group={
|
||||
url.split('?')[0] === href || undefined
|
||||
}
|
||||
>
|
||||
{t(label)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="tempo-panel-summary">
|
||||
<Link href="/investidor/manifestacoes">
|
||||
{t('O meu painel')}
|
||||
</Link>
|
||||
<span aria-live="polite" aria-atomic="true">
|
||||
{requestCount}{' '}
|
||||
{t(
|
||||
requestCount === 1
|
||||
? 'pedido recebido'
|
||||
: 'pedidos recebidos',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="order-5 flex w-full items-center justify-between gap-2 border-t border-white/10 py-2 xl:order-none xl:w-auto xl:justify-end xl:border-0 xl:py-0">
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<TempoWeather />
|
||||
<TempoLanguageSelector />
|
||||
<div id={toolsId} className="tempo-header-extras">
|
||||
<div className="tempo-header-utilities">
|
||||
<div className="flex items-center gap-1">
|
||||
<TempoWeather />
|
||||
<TempoLanguageSelector />
|
||||
</div>
|
||||
<TempoSocialLinks />
|
||||
</div>
|
||||
<TempoSocialLinks />
|
||||
</div>
|
||||
<div className="tempo-header-actions">
|
||||
{props.auth?.user ? (
|
||||
@@ -242,6 +253,94 @@ export function PortalGroupedMenu() {
|
||||
{t('Entrar')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
ref={toggle}
|
||||
type="button"
|
||||
className="tempo-header-toggle"
|
||||
aria-label={
|
||||
expanded
|
||||
? 'Fechar menu e ferramentas'
|
||||
: 'Abrir menu e ferramentas'
|
||||
}
|
||||
aria-expanded={expanded}
|
||||
aria-controls={`${institutionalId} ${toolsId}`}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<X size={22} aria-hidden="true" />
|
||||
) : (
|
||||
<Menu size={22} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="tempo-section-bar">
|
||||
<div className="tempo-section-inner">
|
||||
<Link
|
||||
href="/investidor/manifestacoes"
|
||||
className="tempo-panel-summary"
|
||||
>
|
||||
<span>{t('O meu painel')}</span>
|
||||
<span
|
||||
className="tempo-panel-count"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
{requestCount}{' '}
|
||||
{t(
|
||||
requestCount === 1
|
||||
? 'pedido recebido'
|
||||
: 'pedidos recebidos',
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
<nav
|
||||
aria-label="Áreas do portal"
|
||||
className="tempo-section-navigation"
|
||||
>
|
||||
<ul>
|
||||
{groups
|
||||
.filter((group) =>
|
||||
[
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
<li key={group.id}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
current === group.id ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
{t(group.label)}
|
||||
<ChevronDown
|
||||
size={14}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{current === group.id && (
|
||||
<span className="sr-only">
|
||||
Grupo actual
|
||||
</span>
|
||||
)}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
collisionPadding={12}
|
||||
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
|
||||
>
|
||||
{items(group.children ?? [])}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<Link
|
||||
href="/manifestar-interesse"
|
||||
className="tempo-interest-button"
|
||||
|
||||
@@ -30,6 +30,14 @@ export const tempoSocialLinks = [
|
||||
];
|
||||
|
||||
const menuTranslations = [
|
||||
['Sobre nós', 'About us', 'À propos', 'Sobre nosotros'],
|
||||
['Revista Tempo', 'Revista Tempo', 'Revista Tempo', 'Revista Tempo'],
|
||||
[
|
||||
'Abre num novo separador',
|
||||
'Opens in a new tab',
|
||||
'Ouvre un nouvel onglet',
|
||||
'Abre en una nueva pestaña',
|
||||
],
|
||||
['O meu painel', 'My dashboard', 'Mon espace', 'Mi panel'],
|
||||
[
|
||||
'Manifestar interesse',
|
||||
|
||||
@@ -10,34 +10,41 @@ const navigation = [
|
||||
export function TempoFooter() {
|
||||
const { t, language } = useTempoUi();
|
||||
return (
|
||||
<footer className="tempo-footer" lang={language}>
|
||||
<div className="mx-auto grid max-w-[1440px] gap-9 px-5 py-12 sm:grid-cols-2 sm:px-8 lg:grid-cols-[1.25fr_1.2fr_1fr_1fr_1.1fr] lg:gap-8 lg:py-14">
|
||||
<div className="space-y-6">
|
||||
<Link href="/" aria-label="Tempo — início">
|
||||
<footer
|
||||
className="tempo-footer border-t border-white/5"
|
||||
lang={language}
|
||||
>
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-12 px-6 py-10 md:grid-cols-2 lg:grid-cols-5 lg:gap-16 lg:px-12">
|
||||
<div className="space-y-8">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="Tempo — início"
|
||||
className="block w-fit"
|
||||
>
|
||||
<img
|
||||
src="/images/logo/logo-vermelho.png"
|
||||
alt="Tempo"
|
||||
width="1787"
|
||||
height="564"
|
||||
className="h-11 w-auto object-contain"
|
||||
className="h-10 w-auto object-contain md:h-12"
|
||||
loading="lazy"
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex items-start gap-4">
|
||||
<MapPin
|
||||
className="mt-1 size-5 shrink-0 text-[#ed1c24]"
|
||||
className="mt-1 size-[22px] shrink-0 text-[#ed1c24]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<address className="border-l border-white/15 pl-3 text-sm leading-7 text-slate-300 not-italic">
|
||||
<address className="border-l border-white/15 pl-5 text-sm leading-relaxed text-slate-300 not-italic">
|
||||
Av. Ahmed Sekou Touré, 1126 RC
|
||||
<br />
|
||||
Maputo Cidade, Moçambique
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<h2 className="tempo-footer-heading">{t('Contactos')}</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-5">
|
||||
{[
|
||||
['+258868010880', '+258 86 801 0880'],
|
||||
['+258833219644', '+258 83 321 9644'],
|
||||
@@ -62,15 +69,17 @@ export function TempoFooter() {
|
||||
<span className="tempo-contact-icon">
|
||||
<Mail size={16} aria-hidden="true" />
|
||||
</span>
|
||||
<strong className="text-sm font-medium [overflow-wrap:anywhere]">
|
||||
comercial@tempo.co.mz
|
||||
<strong className="min-w-0 text-sm font-medium [overflow-wrap:anywhere]">
|
||||
comercial@
|
||||
<wbr />
|
||||
tempo.co.mz
|
||||
</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Navegação do rodapé">
|
||||
<nav aria-label="Navegação do rodapé" className="space-y-8">
|
||||
<h2 className="tempo-footer-heading">{t('Navegação')}</h2>
|
||||
<ul className="space-y-3">
|
||||
<ul className="space-y-4">
|
||||
{navigation.map(([href, label]) => (
|
||||
<li key={href}>
|
||||
<Link href={href} className="tempo-footer-link">
|
||||
@@ -84,9 +93,9 @@ export function TempoFooter() {
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-label="Informação legal">
|
||||
<nav aria-label="Informação legal" className="space-y-8">
|
||||
<h2 className="tempo-footer-heading">Legal</h2>
|
||||
<ul className="space-y-3">
|
||||
<ul className="space-y-4">
|
||||
{[
|
||||
[
|
||||
'politica-de-privacidade',
|
||||
@@ -109,9 +118,9 @@ export function TempoFooter() {
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<h2 className="tempo-footer-heading">Social</h2>
|
||||
<p className="mb-5 text-sm leading-6 text-slate-300">
|
||||
<p className="text-xs leading-relaxed font-medium text-slate-300">
|
||||
{t(
|
||||
'Siga a Revista Tempo para actualizações exclusivas.',
|
||||
)}
|
||||
@@ -119,8 +128,8 @@ export function TempoFooter() {
|
||||
<TempoSocialLinks footer />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-white/10">
|
||||
<div className="mx-auto flex max-w-[1440px] flex-col justify-between gap-3 px-5 py-5 text-[11px] tracking-wide text-slate-400 sm:flex-row sm:px-8">
|
||||
<div className="border-t border-white/10 py-2">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-6 px-6 text-center text-[10px] tracking-[0.2em] text-slate-400 md:flex-row md:text-left md:text-xs">
|
||||
<p>
|
||||
© {new Date().getFullYear()}{' '}
|
||||
<span className="text-[#ed1c24]">Tempo</span>.{' '}
|
||||
|
||||
Reference in New Issue
Block a user