Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# APIs Externas
API_GARFO_URL=http://api.garfo.ameciclo.org
CMS_BASE_URL=http://do.strapi.ameciclo.org

# Mapbox (obtenha em https://mapbox.com)
MAPBOX_ACCESS_TOKEN=seu_token_mapbox_aqui

# Google Calendar
GOOGLE_CALENDAR_API_KEY=sua_chave_google_calendar_aqui
GOOGLE_CALENDAR_EXTERNAL_ID=seu_calendar_id_externo_aqui
GOOGLE_CALENDAR_INTERNAL_ID=seu_calendar_id_interno_aqui

# Analytics
GOOGLE_ANALYTICS_ID=G-PQNS7S7FD3

# Ambiente
NODE_ENV=development
12 changes: 12 additions & 0 deletions app/components/Commom/Maps/AmecicloMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as turf from "@turf/helpers";
import { pointData } from "../../../../typings";
import * as Remix from "@remix-run/react";
import { Move } from 'lucide-react';
import { MapboxKeyWarning } from './MapboxKeyWarning';



Expand Down Expand Up @@ -588,6 +589,17 @@ export const AmecicloMap = ({
}));
};

// Verificar se o token do Mapbox está disponível
if (!MAPBOXTOKEN) {
return (
<section className={width === "100%" ? "w-full" : "container mx-auto"} style={{height: height === "100%" ? "100%" : "auto"}}>
<div className={`relative bg-gray-200 map-container ${isFullscreen ? 'fixed inset-0 z-50 w-screen h-screen rounded shadow-2xl' : width === "100%" ? 'w-full h-full' : 'rounded shadow-2xl'}`} style={{height: height === "100%" ? "100%" : height}}>
<MapboxKeyWarning />
</div>
</section>
);
}

return (
<section className={width === "100%" ? "w-full" : "container mx-auto"} style={{height: height === "100%" ? "100%" : "auto"}}>

Expand Down
59 changes: 59 additions & 0 deletions app/components/Commom/Maps/MapboxKeyWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AlertTriangle, Info, Mail } from 'lucide-react';
import { Link, useLocation } from '@remix-run/react';

export const MapboxKeyWarning = () => {
const location = useLocation();
const currentPage = location.pathname;
const errorMessage = encodeURIComponent(`Erro no mapa da página: ${currentPage}\n\nDescrição: A chave de acesso do serviço de mapas não está configurada.`);
const subject = encodeURIComponent('Erro Técnico - Mapa não carrega');

return (
<div className="w-full h-full flex items-center justify-center bg-gray-100 p-6">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg border border-gray-200 overflow-hidden">
<div className="bg-yellow-50 border-b border-yellow-100 px-6 py-4">
<div className="flex items-center gap-3">
<AlertTriangle className="w-6 h-6 text-yellow-600 flex-shrink-0" />
<h3 className="text-lg font-semibold text-gray-900">
Problemas Técnicos com o Mapa
</h3>
</div>
</div>

<div className="p-6 space-y-4">
<p className="text-gray-700 leading-relaxed">
Não foi possível carregar o mapa interativo no momento devido a problemas de configuração técnica.
</p>

<div className="bg-blue-50 border border-blue-100 rounded-lg p-4">
<div className="flex gap-3">
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="space-y-2">
<p className="text-sm font-medium text-blue-900">
Mais informações
</p>
<p className="text-sm text-blue-800">
A chave de acesso do serviço de mapas não está configurada.
Entre em contato com o administrador do sistema.
</p>
</div>
</div>
</div>

<Link
to={`/contato?subject=${subject}&message=${errorMessage}`}
className="flex items-center justify-center gap-2 w-full bg-teal-600 hover:bg-teal-700 text-white font-medium py-3 px-4 rounded-lg transition-colors"
>
<Mail className="w-5 h-5" />
Entrar em Contato
</Link>

<div className="pt-2">
<p className="text-sm text-gray-500 text-center">
Pedimos desculpas pelo inconveniente.
</p>
</div>
</div>
</div>
</div>
);
};
2 changes: 2 additions & 0 deletions app/components/Commom/Maps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { MapboxKeyWarning } from './MapboxKeyWarning';
export { AmecicloMap } from './AmecicloMap';
2 changes: 1 addition & 1 deletion app/components/Commom/NavBar/NavbarLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export function NavbarLogo() {
return (
<svg
className="relative w-36 h-8 text-ameciclo"
className="px-0 relative w-44 h-14 lg:w-36 lg:h-8 text-ameciclo"
fill="currentColor"
viewBox="0 0 100.00014 29.999868"
>
Expand Down
13 changes: 8 additions & 5 deletions app/components/Commom/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,18 @@ export const Navbar = ({ pages }: any) => {
return (
<>
<motion.nav
animate={{ y: isMenuOpen ? -120 : 0 }}
animate={{ y: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className={"flex fixed top-0 left-0 right-0 items-center h-14 z-[80] text-white transition-shadow duration-300 bg-ameciclo shadow-sm"}
role="navigation mt-0"
>
<div className="w-full flex items-center justify-between px-8 py-0 m-0 lg:px-32 xl:px-32">
<Link to="/" aria-label="Ir para o site da Ameciclo" className="relative z-[85] pointer-events-auto" prefetch="intent">
<AmecicloLogo isScrolled={isHeaderScrolled || isSubmenuVisible || location.pathname === '/dados/ciclodados'} />
</Link>
<div className="w-full flex items-center justify-between px-4 lg:px-32 xl:px-32 py-0 m-0">
{!isMenuOpen && (
<Link to="/" aria-label="Ir para o site da Ameciclo" className="relative z-[85] pointer-events-auto" prefetch="intent">
<AmecicloLogo isScrolled={isHeaderScrolled || isSubmenuVisible || location.pathname === '/dados/ciclodados'} />
</Link>
)}
{isMenuOpen && <div className="lg:hidden" />}

<div className="hidden lg:flex space-x-8">
<BigMenu pages={pages} setIsSubmenuVisible={setIsSubmenuVisible} isSubmenuVisible={isSubmenuVisible} />
Expand Down
71 changes: 30 additions & 41 deletions app/components/Contato/ContactForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from "react";
import { useSearchParams } from "@remix-run/react";
import { Mail, MessageCircle, Check, X } from "lucide-react";
import { MessageCircle, Check, X } from "lucide-react";

interface FormData {
nome: string;
Expand Down Expand Up @@ -30,9 +30,7 @@ export function ContactForm() {
const newErrors: {[key: string]: string} = {};

if (!data.nome) newErrors.nome = 'Nome é obrigatório';
if (!data.email) {
newErrors.email = 'Email é obrigatório';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
if (data.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
newErrors.email = 'Email inválido';
}
if (data.telefone && !/^\d{10,11}$/.test(data.telefone.replace(/\D/g, ''))) {
Expand All @@ -44,7 +42,7 @@ export function ContactForm() {
return newErrors;
};

const handleFormSubmit = (callback: (data: FormData) => void) => {
const handleWhatsApp = () => {
const data = getFormData();
const validationErrors = validateForm(data);

Expand All @@ -55,7 +53,14 @@ export function ContactForm() {
}

setErrors({});
callback(data);

const parts = [`*Nome:* ${data.nome}`];
if (data.email) parts.push(`*Email:* ${data.email}`);
if (data.telefone) parts.push(`*Telefone:* ${data.ddi}${data.telefone}`);
parts.push('', data.mensagem);

const whatsappMsg = parts.join('\n');
window.open(`https://wa.me/5581994586830?text=${encodeURIComponent(whatsappMsg)}`, '_blank');
};

return (
Expand Down Expand Up @@ -85,22 +90,22 @@ export function ContactForm() {
</div>

<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">Email <span className="text-red-500">*</span></label>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">Email</label>
<div className="relative">
<input
type="email"
id="email"
required
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-[#008080] focus:border-transparent ${errors.email ? 'border-red-500 bg-red-50' : success.email ? 'border-green-500' : 'border-gray-300'}`}
placeholder="seu@email.com"
placeholder="seu@email.com (opcional)"
onChange={(e) => {
const value = e.target.value;
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
setSuccess(prev => ({...prev, email: isValid}));
if (value.length > 0 && !isValid) {
setErrors(prev => ({...prev, email: 'Email inválido'}));
} else {
if (value.length === 0) {
setErrors(prev => ({...prev, email: ''}));
setSuccess(prev => ({...prev, email: false}));
} else {
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
setSuccess(prev => ({...prev, email: isValid}));
setErrors(prev => ({...prev, email: isValid ? '' : 'Email inválido'}));
}
}}
/>
Expand Down Expand Up @@ -198,33 +203,17 @@ export function ContactForm() {
</div>
</div>

<div className="flex flex-col sm:flex-row gap-3">
<button
type="button"
onClick={() => handleFormSubmit((data) => {
const telefoneFormatted = data.telefone ? `${data.ddi}${data.telefone}` : 'Não informado';
const customSubject = searchParams.get('subject');
const subject = customSubject || `Contato via página de contato - ${data.nome}`;
const body = `Email: ${data.email}\\nTelefone: ${telefoneFormatted}\\n\\n${data.mensagem}`;
window.location.href = `mailto:contato@ameciclo.org?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
})}
className="flex-1 bg-[#008080] text-white px-4 py-2 rounded-md hover:bg-[#006666] transition-colors font-medium flex items-center justify-center gap-2"
>
<Mail size={18} /> Enviar E-mail
</button>

<button
type="button"
onClick={() => handleFormSubmit((data) => {
const telefoneFormatted = data.telefone ? `${data.ddi}${data.telefone}` : 'Não informado';
const whatsappMsg = `Olá! Me chamo ${data.nome}!\\n\\nEmail: ${data.email}\\nTelefone: ${telefoneFormatted}\\n\\n${data.mensagem}`;
window.open(`https://wa.me/5581994586830?text=${encodeURIComponent(whatsappMsg)}`, '_blank');
})}
className="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition-colors font-medium flex items-center justify-center gap-2"
>
<MessageCircle size={18} /> WhatsApp
</button>
</div>
<button
type="button"
onClick={handleWhatsApp}
className="w-full bg-green-600 text-white px-4 py-3 rounded-md hover:bg-green-700 transition-colors font-medium flex items-center justify-center gap-2 text-lg"
>
<MessageCircle size={20} /> Enviar via WhatsApp
</button>

<p className="text-center text-xs text-gray-500">
ou <a href="mailto:contato@ameciclo.org" className="underline hover:text-[#008080]">enviar e-mail</a>
</p>
</form>
</div>
);
Expand Down
35 changes: 27 additions & 8 deletions app/components/Contato/ContactMap.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { AmecicloMap } from "~/components/Commom/Maps/AmecicloMap";

const AmecicloMarker = () => (
<div className="flex flex-col items-center">
<div className="w-10 h-10 bg-white rounded-full border-3 border-[#008080] shadow-lg flex items-center justify-center overflow-hidden">
<img src="/miniatura-ameciclo-logo.webp" alt="Ameciclo" className="w-7 h-7 object-contain" />
</div>
<div className="w-0 h-0 border-l-[6px] border-r-[6px] border-t-[8px] border-l-transparent border-r-transparent border-t-[#008080] -mt-[1px]" />
</div>
);

export function ContactMap() {
return (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3950.2847!2d-34.8823!3d-8.0593!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x7ab18a59c7b4b0b%3A0x123456789!2sR.%20da%20Aurora%2C%20529%20-%20Santo%20Amaro%2C%20Recife%20-%20PE!5e0!3m2!1spt-BR!2sbr!4v1234567890"
width="100%"
<AmecicloMap
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Localização da Sede da Ameciclo"
width="100%"
pointsData={[
{
key: "sede-ameciclo",
latitude: -8.059349,
longitude: -34.880116,
size: 30,
color: "#008080",
type: "sede",
popup: { name: "Sede da Ameciclo" },
customIcon: <AmecicloMarker />,
},
]}
initialViewState={{ latitude: -8.059349, longitude: -34.880116, zoom: 15 }}
showLayersPanel={false}
/>
</div>
);
Expand Down
40 changes: 40 additions & 0 deletions app/components/Projetos/LanguageBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Link } from "@remix-run/react";
import { Globe } from "lucide-react";

interface LanguageBadgeProps {
currentSlug: string;
}

export const LanguageBadge = ({ currentSlug }: LanguageBadgeProps) => {
let baseSlug = currentSlug;

if (currentSlug.endsWith('_en')) {
baseSlug = currentSlug.replace('_en', '');
} else if (currentSlug.endsWith('_es')) {
baseSlug = currentSlug.replace('_es', '');
}

const translations = [
{ lang: 'en', flag: '🇬🇧', slug: `${baseSlug}_en`, label: 'English' },
{ lang: 'es', flag: '🇪🇸', slug: `${baseSlug}_es`, label: 'Español' },
];

return (
<div className="flex items-center gap-2">
<Globe className="w-4 h-4 text-gray-500" />
{translations.map((t) => (
<Link
key={t.lang}
to={`/projetos/${t.slug}`}
className="flex items-center gap-1 px-2 py-1 bg-gray-100 hover:bg-teal-50 rounded-md transition-colors group"
title={t.label}
>
<span className="text-base">{t.flag}</span>
<span className="text-xs text-gray-600 group-hover:text-teal-600 font-medium">
{t.lang.toUpperCase()}
</span>
</Link>
))}
</div>
);
};
Loading