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
9 changes: 6 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,19 @@ jspm_packages/
/dist
/.cache

# TanStack Router generated route tree
app/routeTree.gen.ts

# Cloudflare
.wrangler/

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Workspace files
*.code-workspace
ameciclo.code-workspace
Expand Down
30 changes: 30 additions & 0 deletions app/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StartClient } from "@tanstack/react-start/client";
import { StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

// Suppress react-map-gl warnings and errors
const originalWarn = console.warn;
const originalError = console.error;

console.warn = (...args: any[]) => {
const message = String(args[0] || "");
if (message.includes("Marker") && message.includes("defaultProps")) {
return;
}
originalWarn.apply(console, args);
};

console.error = (...args: any[]) => {
const message = String(args[0] || "");
if (message.includes("setLayerProperty is not a function")) {
return;
}
originalError.apply(console, args);
};

hydrateRoot(
document,
<StrictMode>
<StartClient />
</StrictMode>
);
9 changes: 7 additions & 2 deletions app/components/Agenda/EventCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import { ClientOnly } from "remix-utils/client-only";
import { useState, useEffect, type ReactNode } from "react";

const CALENDAR_VIEW_COOKIE = "ameciclo_calendar_view";

Expand Down Expand Up @@ -118,6 +117,12 @@ function CalendarComponent({ googleCalendarApiKey, externalCalendarId, internalC
);
}

function ClientOnly({ children, fallback }: { children: () => ReactNode; fallback: ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return <>{mounted ? children() : fallback}</>;
}

export default function EventCalendar(props: any) {
return (
<ClientOnly fallback={
Expand Down
2 changes: 1 addition & 1 deletion app/components/Biciclopedia/AccordionFAQ.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Link } from "@remix-run/react";
import { Link } from "@tanstack/react-router";
import { motion, AnimatePresence } from "framer-motion";
import FAQIcon from "./FAQIcon";

Expand Down
2 changes: 1 addition & 1 deletion app/components/Biciclopedia/SearchComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Link } from "@remix-run/react";
import { Link } from "@tanstack/react-router";
import { Highlight } from "react-highlighter-ts";
import type { FuseResult } from "fuse.js";

Expand Down
141 changes: 48 additions & 93 deletions app/components/Commom/ApiAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,22 @@
import { useLocation } from '@remix-run/react';
import { useRouterState } from '@tanstack/react-router';
import { useApiStatus } from '~/contexts/ApiStatusContext';
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect } from 'react';
import { X } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '~/components/ui/dialog';
import { Alert, AlertTitle, AlertDescription } from '~/components/ui/alert';
import { Button } from '~/components/ui/button';

export function ApiAlert() {
const { isApiDown, apiErrors, clearErrors } = useApiStatus();
const location = useLocation();
const location = useRouterState({ select: (s) => s.location });
const [showDetails, setShowDetails] = useState(false);
const [dismissed, setDismissed] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);


useEffect(() => {
// Limpar erros ao trocar de página
setDismissed(false);
}, [location.pathname]);

useEffect(() => {
if (showDetails && modalRef.current) {
const focusableElements = modalRef.current.querySelectorAll(
'button, a, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

const handleTab = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
}
}
if (e.key === 'Escape') {
setShowDetails(false);
}
};

document.addEventListener('keydown', handleTab);
firstElement?.focus();

return () => document.removeEventListener('keydown', handleTab);
}
}, [showDetails]);



const generateErrorReport = () => {
const errorDetails = `RELATÓRIO DE ERRO AUTOMÁTICO\n\nPágina: ${location.pathname}\nHorário: ${new Date().toLocaleString('pt-BR')}\n\nAPIs com falha:\n${apiErrors.map(error => `- ${error.url}\n Erro: ${error.error}\n Página: ${error.page}\n Horário: ${error.timestamp}`).join('\n\n')}\n\nEmail (obrigatório):\n\nTelefone (opcional):\n\nInformações adicionais (opcional):`;
return encodeURIComponent(errorDetails);
Expand All @@ -64,14 +30,14 @@ export function ApiAlert() {
<div className="fixed top-16 left-1/2 -translate-x-1/2 w-fit bg-orange-500 text-white px-6 py-2 text-center rounded-lg shadow-lg relative pointer-events-auto">
<p className="text-sm">
Esta página está sofrendo instabilidade agora.{' '}
<button
<button
onClick={() => setShowDetails(true)}
className="underline hover:no-underline pointer-events-auto"
>
Ver detalhes do erro
</button>
</p>
<button
<button
onClick={() => setDismissed(true)}
className="absolute top-1 right-2 text-sm opacity-70 hover:opacity-100 transition-opacity pointer-events-auto"
title="Fechar aviso"
Expand All @@ -80,64 +46,53 @@ export function ApiAlert() {
</button>
</div>
</div>
{showDetails && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={() => setShowDetails(false)}

<Dialog open={showDetails} onOpenChange={setShowDetails}>
<DialogContent
onClose={() => setShowDetails(false)}
className="max-w-2xl max-h-[80vh] overflow-y-auto"
>
<div
ref={modalRef}
className="bg-white p-6 rounded-lg max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto relative"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby="error-dialog-title"
>
<button
onClick={() => setShowDetails(false)}
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
aria-label="Fechar"
>
<X size={20} />
</button>

<h3 id="error-dialog-title" className="text-lg font-semibold mb-4 text-gray-900 pr-8">Detalhes do Erro</h3>
<div className="text-sm text-gray-700 space-y-4">
<DialogHeader>
<DialogTitle>Detalhes do Erro</DialogTitle>
</DialogHeader>

<div className="text-sm text-gray-700 space-y-4">
<div>
<p><strong>Página atual:</strong> {location.pathname}</p>
<p><strong>Horário:</strong> {new Date().toLocaleString('pt-BR')}</p>
</div>

{apiErrors.length > 0 ? (
<div>
<p><strong>Página atual:</strong> {location.pathname}</p>
<p><strong>Horário:</strong> {new Date().toLocaleString('pt-BR')}</p>
</div>

{apiErrors.length > 0 ? (
<div>
<h4 className="font-semibold mb-2">APIs com falha:</h4>
<div className="space-y-3 max-h-60 overflow-y-auto">
{apiErrors.map((error, index) => (
<div key={index} className="border-l-4 border-red-400 pl-3 py-2 bg-red-50">
<h4 className="font-semibold mb-2">APIs com falha:</h4>
<div className="space-y-3 max-h-60 overflow-y-auto">
{apiErrors.map((error, index) => (
<Alert key={index} variant="destructive" className="border-l-4 border-red-400">
<AlertDescription>
<p><strong>API:</strong> {error.url}</p>
<p><strong>Erro:</strong> {error.error}</p>
<p><strong>Página:</strong> {error.page}</p>
<p><strong>Horário:</strong> {error.timestamp}</p>
</div>
))}
</div>
</AlertDescription>
</Alert>
))}
</div>
) : (
<p>Nenhum erro específico registrado.</p>
)}
</div>
<div className="flex justify-end mt-6">
<a
href={`/contato?message=${generateErrorReport()}&subject=${encodeURIComponent('Aviso de erro via página de contato - Ver detalhes do Erro')}`}
className="font-bold text-blue-600 hover:text-blue-800 underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded"
>
</div>
) : (
<p>Nenhum erro específico registrado.</p>
)}
</div>
<div className="flex justify-end mt-2">
<a
href={`/contato?message=${generateErrorReport()}&subject=${encodeURIComponent('Aviso de erro via página de contato - Ver detalhes do Erro')}`}
>
<Button variant="link" className="font-bold">
Avisar desenvolvedores
</a>
</div>
</Button>
</a>
</div>
</div>
)}
</DialogContent>
</Dialog>
</>
);
}
}
2 changes: 1 addition & 1 deletion app/components/Commom/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link } from "@remix-run/react";
import { Link } from "@tanstack/react-router";

interface BreadcrumbItemProps {
slug: string;
Expand Down
81 changes: 32 additions & 49 deletions app/components/Commom/CachePermissionModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
import { useFocusTrap } from '~/hooks/useFocusTrap';
import { X } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '~/components/ui/dialog';
import { Button } from '~/components/ui/button';

interface CachePermissionBarProps {
onAllow: () => void;
Expand All @@ -9,7 +9,6 @@ interface CachePermissionBarProps {

export default function CachePermissionBar({ onAllow, onDeny }: CachePermissionBarProps) {
const [isVisible, setIsVisible] = useState(false);
const modalRef = useFocusTrap(isVisible);

useEffect(() => {
if (typeof window !== 'undefined') {
Expand All @@ -23,10 +22,6 @@ export default function CachePermissionBar({ onAllow, onDeny }: CachePermissionB
}
}, []);

const handleClose = () => {
setIsVisible(false);
};

const handleAllow = () => {
localStorage.setItem('cache-permission', 'allowed');
setIsVisible(false);
Expand All @@ -39,59 +34,47 @@ export default function CachePermissionBar({ onAllow, onDeny }: CachePermissionB
onDeny();
};

if (!isVisible) return null;

return (
<div
ref={modalRef}
className="fixed bottom-4 left-4 right-4 md:bottom-6 md:left-1/2 md:transform md:-translate-x-1/2 md:w-[70%] md:max-w-4xl bg-white rounded-xl shadow-2xl border border-gray-200 z-[9999]"
role="dialog"
aria-labelledby="cache-modal-title"
aria-describedby="cache-modal-description"
>
<button
onClick={handleClose}
className="absolute top-2 right-2 p-0.5 text-gray-400 hover:text-gray-600 rounded-md hover:bg-gray-100 transition-colors"
aria-label="Fechar tela de cache"
<Dialog open={isVisible} onOpenChange={setIsVisible}>
<DialogContent
onClose={() => setIsVisible(false)}
className="sm:max-w-4xl"
>
<X size={14} />
</button>
<div className="px-4 py-4 md:px-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-start gap-3 flex-1">
<DialogHeader>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 mt-1">
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
</svg>
</div>
<div className="flex-1">
<h4 className="text-sm font-semibold text-gray-900 mb-1" id="cache-modal-title">
<DialogTitle className="text-sm">
Melhorar sua experiência
</h4>
<p className="text-sm text-gray-600 leading-relaxed" id="cache-modal-description">
Utilizamos cache local para acelerar o carregamento das páginas e melhorar sua navegação.
</DialogTitle>
<p className="text-sm text-gray-600 leading-relaxed mt-1">
Utilizamos cache local para acelerar o carregamento das páginas e melhorar sua navegação.
Os dados são armazenados temporariamente apenas no seu navegador.
</p>
</div>
</div>
<div className="flex flex-col gap-2 w-full md:flex-row md:gap-2 md:flex-shrink-0 md:w-auto">
<button
onClick={handleDeny}
className="w-full md:w-auto px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
aria-label="Não permitir cache"
>
Não permitir
</button>
<button
onClick={handleAllow}
className="w-full md:w-auto px-4 py-2 text-sm font-medium text-white bg-ameciclo hover:bg-green-600 rounded-md transition-colors shadow-sm"
aria-label="Permitir cache"
>
Permitir cache
</button>
</div>
</div>
</div>
</div>
</DialogHeader>
<DialogFooter className="flex-col gap-2 sm:flex-row">
<Button
variant="outline"
onClick={handleDeny}
aria-label="Não permitir cache"
>
Não permitir
</Button>
<Button
onClick={handleAllow}
className="bg-ameciclo hover:bg-green-600"
aria-label="Permitir cache"
>
Permitir cache
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
}
Loading