diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..37d3fac7 --- /dev/null +++ b/.env.example @@ -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 diff --git a/app/components/Commom/Maps/AmecicloMap.tsx b/app/components/Commom/Maps/AmecicloMap.tsx index 0c4f1690..a7cb2ab7 100644 --- a/app/components/Commom/Maps/AmecicloMap.tsx +++ b/app/components/Commom/Maps/AmecicloMap.tsx @@ -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'; @@ -588,6 +589,17 @@ export const AmecicloMap = ({ })); }; + // Verificar se o token do Mapbox está disponível + if (!MAPBOXTOKEN) { + return ( +
+
+ +
+
+ ); + } + return (
diff --git a/app/components/Commom/Maps/MapboxKeyWarning.tsx b/app/components/Commom/Maps/MapboxKeyWarning.tsx new file mode 100644 index 00000000..e47845a1 --- /dev/null +++ b/app/components/Commom/Maps/MapboxKeyWarning.tsx @@ -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 ( +
+
+
+
+ +

+ Problemas Técnicos com o Mapa +

+
+
+ +
+

+ Não foi possível carregar o mapa interativo no momento devido a problemas de configuração técnica. +

+ +
+
+ +
+

+ Mais informações +

+

+ A chave de acesso do serviço de mapas não está configurada. + Entre em contato com o administrador do sistema. +

+
+
+
+ + + + Entrar em Contato + + +
+

+ Pedimos desculpas pelo inconveniente. +

+
+
+
+
+ ); +}; diff --git a/app/components/Commom/Maps/index.ts b/app/components/Commom/Maps/index.ts new file mode 100644 index 00000000..a20433a8 --- /dev/null +++ b/app/components/Commom/Maps/index.ts @@ -0,0 +1,2 @@ +export { MapboxKeyWarning } from './MapboxKeyWarning'; +export { AmecicloMap } from './AmecicloMap'; diff --git a/app/components/Commom/NavBar/NavbarLogo.tsx b/app/components/Commom/NavBar/NavbarLogo.tsx index f01655f5..627eef7b 100644 --- a/app/components/Commom/NavBar/NavbarLogo.tsx +++ b/app/components/Commom/NavBar/NavbarLogo.tsx @@ -1,7 +1,7 @@ export function NavbarLogo() { return ( diff --git a/app/components/Commom/Navbar.tsx b/app/components/Commom/Navbar.tsx index 213cad58..0517e580 100644 --- a/app/components/Commom/Navbar.tsx +++ b/app/components/Commom/Navbar.tsx @@ -70,15 +70,18 @@ export const Navbar = ({ pages }: any) => { return ( <> -
- - - +
+ {!isMenuOpen && ( + + + + )} + {isMenuOpen &&
}
diff --git a/app/components/Contagens/CountingComparisionTable.tsx b/app/components/Contagens/CountingComparisionTable.tsx index 3e2db21d..bf9a0eca 100644 --- a/app/components/Contagens/CountingComparisionTable.tsx +++ b/app/components/Contagens/CountingComparisionTable.tsx @@ -5,6 +5,7 @@ import { Link } from "@remix-run/react"; import { ColumnFilter, NumberRangeColumnFilter } from "~/components/Commom/Table/TableFilters"; import Table from "~/components/Commom/Table/Table"; import { IntlDateStr } from "~/services/utils"; +import { contagemSlug } from "~/utils/slugify"; function fuzzyTextFilterFn(rows: any[], id: string, filterValue: string) { return matchSorter(rows, filterValue, { keys: [(row: any) => row.values[id]] }); @@ -55,15 +56,20 @@ export const CountingComparisionTable = ({ data, firstSlug }: { data: any[], fir { Header: "Nome", accessor: "name", - Cell: ({ row }: { row: any }) => ( - - {row.original.name} - - ), + Cell: ({ row }: { row: any }) => { + const slug = row.original.date + ? contagemSlug(row.original.date, row.original.name) + : String(row.original.id); + return ( + + {row.original.name} + + ); + }, Filter: ColumnFilter, }, { @@ -87,15 +93,20 @@ export const CountingComparisionTable = ({ data, firstSlug }: { data: any[], fir }, { Header: "COMPARE", - accessor: "compare", // Adiciona accessor - Cell: ({ row }: { row: any }) => ( - - COMPARE - - ), + accessor: "compare", + Cell: ({ row }: { row: any }) => { + const compareSlug = row.original.date + ? contagemSlug(row.original.date, row.original.name) + : String(row.original.id); + return ( + + COMPARE + + ); + }, disableFilters: true, disableSortBy: true, }, diff --git a/app/components/Contagens/CountsTable.tsx b/app/components/Contagens/CountsTable.tsx index 4928bcfa..d72c432d 100644 --- a/app/components/Contagens/CountsTable.tsx +++ b/app/components/Contagens/CountsTable.tsx @@ -4,6 +4,7 @@ import type { ContagemData } from "~/services/contagens.service"; import { IntlDateStr } from "~/services/utils"; import Table from "~/components/Commom/Table/Table"; import { ColumnFilter } from "~/components/Commom/Table/TableFilters"; +import { contagemSlug } from "~/utils/slugify"; interface ContagensTableProps { data: ContagemData[]; @@ -50,8 +51,9 @@ export function CountsTable({ data }: ContagensTableProps) { Header: "Nome", accessor: "name", Cell: ({ row }: any) => { - // Generate slug from ID and name if slug doesn't exist - const slug = row.original.id || `${row.original.id}`; + const slug = row.original.date + ? contagemSlug(row.original.date, row.original.name) + : String(row.original.id); return ( {point.popup.obs}
)} + + {point.popup?.url && ( + + Ver mais + + )}
diff --git a/app/components/Projetos/LanguageBadge.tsx b/app/components/Projetos/LanguageBadge.tsx new file mode 100644 index 00000000..82eaba8b --- /dev/null +++ b/app/components/Projetos/LanguageBadge.tsx @@ -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 ( +
+ + {translations.map((t) => ( + + {t.flag} + + {t.lang.toUpperCase()} + + + ))} +
+ ); +}; diff --git a/app/components/Projetos/LanguageSelector.tsx b/app/components/Projetos/LanguageSelector.tsx index d7047066..2a347f21 100644 --- a/app/components/Projetos/LanguageSelector.tsx +++ b/app/components/Projetos/LanguageSelector.tsx @@ -1,55 +1,87 @@ -import { useState, useRef, useEffect } from 'react'; +import { Link } from "@remix-run/react"; +import { Globe } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; -const LanguageSelector = ({ links }: { links: any[] }) => { +interface LanguageSelectorProps { + currentSlug: string; + availableLanguages?: Array<{ + lang: string; + slug: string; + }>; +} + +export const LanguageSelector = ({ currentSlug, availableLanguages = [] }: LanguageSelectorProps) => { const [isOpen, setIsOpen] = useState(false); - const ref = useRef(null); + const dropdownRef = useRef(null); + + // Detectar idioma atual + let currentLang = 'pt'; + let baseSlug = currentSlug; + + if (currentSlug.endsWith('_en')) { + currentLang = 'en'; + baseSlug = currentSlug.replace('_en', ''); + } else if (currentSlug.endsWith('_es')) { + currentLang = 'es'; + baseSlug = currentSlug.replace('_es', ''); + } + + const languages = [ + { lang: 'pt', flag: '🇧🇷', label: 'Português', slug: baseSlug }, + { lang: 'en', flag: '🇬🇧', label: 'English', slug: `${baseSlug}_en` }, + { lang: 'es', flag: '🇪🇸', label: 'Español', slug: `${baseSlug}_es` }, + ]; + + const currentLanguage = languages.find(l => l.lang === currentLang); + const otherLanguages = languages.filter(l => l.lang !== currentLang); + // Fechar dropdown ao clicar fora useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (ref.current && !ref.current.contains(event.target as Node)) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { setIsOpen(false); } }; - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [ref]); - if (!links || links.length === 0) { - return null; - } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); return ( -
+
{isOpen && ( -
-
- {links.map((link: any) => ( - - {link.title} - {link.language || 'Tradução'} - - ))} -
+
+ {otherLanguages.map((lang) => ( + setIsOpen(false)} + > + {lang.flag} + {lang.label} + + ))}
)}
); }; - -export default LanguageSelector; \ No newline at end of file diff --git a/app/components/Projetos/ProjectCard.tsx b/app/components/Projetos/ProjectCard.tsx index cc90f6ea..0c1b33da 100644 --- a/app/components/Projetos/ProjectCard.tsx +++ b/app/components/Projetos/ProjectCard.tsx @@ -1,4 +1,5 @@ import { Link } from "@remix-run/react"; +import { LanguageBadge } from "./LanguageBadge"; const StatusIndicator = ({ status }: any) => { const statusMap = new Map([ @@ -26,35 +27,7 @@ const StatusIndicator = ({ status }: any) => { ); }; -export const ProjectCard = ({ project, translations }: any) => { - const translationLinks = []; - - if (translations["es"]) { - translationLinks.push( - - - - 🇪🇸 - - Traducción - - - ); - } - - if (translations["en"]) { - translationLinks.push( - - - - 🇬🇧 - - Translation - - - ); - } - +export const ProjectCard = ({ project }: any) => { return (
@@ -97,9 +70,9 @@ export const ProjectCard = ({ project, translations }: any) => { {project.description} - {translationLinks.length > 0 && ( -
- {translationLinks} + {project.slug === 'bota_pra_rodar' && ( +
+
)}
diff --git a/app/components/Projetos/ProjectSteps.tsx b/app/components/Projetos/ProjectSteps.tsx new file mode 100644 index 00000000..8af5efc2 --- /dev/null +++ b/app/components/Projetos/ProjectSteps.tsx @@ -0,0 +1,145 @@ +import { Bike } from "lucide-react"; + +interface ProjectStepsProps { + currentSlug?: string; +} + +const translations = { + pt: { + step: "PASSO", + steps: [ + { + number: 1, + title: "Campanha de Recolhimento", + description: "Campanha para recolher bicicletas sem utilização em condomínios e casas", + }, + { + number: 2, + title: "Integração Comunitária", + description: "Integração e discussão com a comunidade para a construção do sistema", + }, + { + number: 3, + title: "Oficinas de Mecânica", + description: "Realização de oficinas de mecânica com a juventude das comunidades", + }, + { + number: 4, + title: "Sistema Compartilhado", + description: "Criação de um sistema de bicicletas compartilhadas gerido pela comunidade", + }, + ], + donationButton: "Formulário de Doação", + }, + en: { + step: "STEP", + steps: [ + { + number: 1, + title: "Collection Campaign", + description: "Campaign to collect unused bicycles from condominiums and houses", + }, + { + number: 2, + title: "Community Integration", + description: "Integration and discussion with the community to build the system", + }, + { + number: 3, + title: "Mechanics Workshops", + description: "Conducting mechanics workshops with youth from the communities", + }, + { + number: 4, + title: "Shared System", + description: "Creation of a community-managed bicycle sharing system", + }, + ], + donationButton: "Donation Form", + }, + es: { + step: "PASO", + steps: [ + { + number: 1, + title: "Campaña de Recolección", + description: "Campaña para recoger bicicletas sin uso en condominios y casas", + }, + { + number: 2, + title: "Integración Comunitaria", + description: "Integración y discusión con la comunidad para la construcción del sistema", + }, + { + number: 3, + title: "Talleres de Mecánica", + description: "Realización de talleres de mecánica con la juventud de las comunidades", + }, + { + number: 4, + title: "Sistema Compartido", + description: "Creación de un sistema de bicicletas compartidas gestionado por la comunidad", + }, + ], + donationButton: "Formulario de Donación", + }, +}; + +const images = [ + "https://res.cloudinary.com/plpbs/image/upload/v1615154368/botaprarodar1_c5699a2352.png", + "https://res.cloudinary.com/plpbs/image/upload/v1615154054/botaprarodar2_c9a3b5debe.png", + "https://res.cloudinary.com/plpbs/image/upload/v1615154054/botaprarodar3_69470abc97.png", + "https://res.cloudinary.com/plpbs/image/upload/v1615154408/botaprarodar4_3652e7a970.png", +]; + +export function ProjectSteps({ currentSlug = "bota_pra_rodar" }: ProjectStepsProps) { + // Detectar idioma atual + let lang: 'pt' | 'en' | 'es' = 'pt'; + if (currentSlug.endsWith('_en')) { + lang = 'en'; + } else if (currentSlug.endsWith('_es')) { + lang = 'es'; + } + + const t = translations[lang]; + return ( +
+
+ {t.steps.map((step, index) => ( +
+
+ {step.title} +
+
+

+ {t.step} {step.number} +

+

+ {step.description} +

+
+
+ ))} +
+ + +
+ ); +} diff --git a/app/components/Projetos/ProjectsContent.tsx b/app/components/Projetos/ProjectsContent.tsx index b2be185c..947942b8 100644 --- a/app/components/Projetos/ProjectsContent.tsx +++ b/app/components/Projetos/ProjectsContent.tsx @@ -14,11 +14,6 @@ interface Project { workgroup?: { name: string }; } -interface GroupedProject { - main: Project | null; - translations: Record; -} - interface ProjectsContentProps { projectsData: { projects: Project[]; @@ -40,76 +35,48 @@ export function ProjectsContent({ projectsData }: ProjectsContentProps) { setApiDown(hasApiError); }, [hasApiError, setApiDown]); - const groupedProjects: GroupedProject[] = useMemo(() => { - const groups: Record = {}; - - projects.forEach((project: any) => { - const baseSlug = project.slug.replace(/(_es|_en)$/, ""); - let lang = "pt"; - if (project.slug.endsWith("_es")) lang = "es"; - else if (project.slug.endsWith("_en")) lang = "en"; - - if (!groups[baseSlug]) { - groups[baseSlug] = { main: null, translations: {} }; - } - - if (lang === "pt") { - groups[baseSlug].main = project; - } else { - groups[baseSlug].translations[lang] = project; - } - }); - - return Object.values(groups); - }, [projects]); - const filteredProjects = useMemo(() => { - let filtered = groupedProjects; + let filtered = projects.filter((project: Project) => { + const isTranslation = project.slug.endsWith('_es') || project.slug.endsWith('_en'); + return !isTranslation; + }); if (searchTerm) { const lowerCaseSearchTerm = searchTerm.toLowerCase(); - filtered = filtered.filter((groupedProject) => { - const project = groupedProject.main || Object.values(groupedProject.translations)[0]; - return project?.name.toLowerCase().includes(lowerCaseSearchTerm); - }); + filtered = filtered.filter((project) => + project.name.toLowerCase().includes(lowerCaseSearchTerm) + ); } if (status || group) { - filtered = filtered.filter((groupedProject) => { - const project = groupedProject.main || Object.values(groupedProject.translations)[0]; - if (project) { - if (group !== "" && status !== "") { - return project.project_status === status && project.workgroup?.name === group; - } else { - return project.project_status === status || project.workgroup?.name === group; - } + filtered = filtered.filter((project) => { + if (group !== "" && status !== "") { + return project.project_status === status && project.workgroup?.name === group; + } else { + return project.project_status === status || project.workgroup?.name === group; } - return false; }); } - const highlighted: GroupedProject[] = []; - const ongoing: GroupedProject[] = []; - const paused: GroupedProject[] = []; - const others: GroupedProject[] = []; - - filtered.forEach((groupedProject) => { - const project = groupedProject.main || Object.values(groupedProject.translations)[0]; - if (project) { - if (project.isHighlighted) { - highlighted.push(groupedProject); - } else if (project.project_status === "ongoing") { - ongoing.push(groupedProject); - } else if (project.project_status === "paused") { - paused.push(groupedProject); - } else { - others.push(groupedProject); - } + const highlighted: Project[] = []; + const ongoing: Project[] = []; + const paused: Project[] = []; + const others: Project[] = []; + + filtered.forEach((project) => { + if (project.isHighlighted) { + highlighted.push(project); + } else if (project.project_status === "ongoing") { + ongoing.push(project); + } else if (project.project_status === "paused") { + paused.push(project); + } else { + others.push(project); } }); return { highlighted, ongoing, paused, others }; - }, [status, group, searchTerm, groupedProjects]); + }, [status, group, searchTerm, projects]); const allProjectsCount = filteredProjects.highlighted.length + @@ -146,12 +113,8 @@ export function ProjectsContent({ projectsData }: ProjectsContentProps) { <> {filteredProjects.highlighted.length > 0 && (
- {filteredProjects.highlighted.map((groupedProject) => ( - + {filteredProjects.highlighted.map((project) => ( + ))}
)} @@ -160,12 +123,8 @@ export function ProjectsContent({ projectsData }: ProjectsContentProps) { <>

Projetos em Andamento

- {filteredProjects.ongoing.map((groupedProject) => ( - + {filteredProjects.ongoing.map((project) => ( + ))}
@@ -175,12 +134,8 @@ export function ProjectsContent({ projectsData }: ProjectsContentProps) { <>

Projetos Pausados

- {filteredProjects.paused.map((groupedProject) => ( - + {filteredProjects.paused.map((project) => ( + ))}
@@ -190,12 +145,8 @@ export function ProjectsContent({ projectsData }: ProjectsContentProps) { <>

Demais Projetos

- {filteredProjects.others.map((groupedProject) => ( - + {filteredProjects.others.map((project) => ( + ))}
diff --git a/app/hooks/useCountsMapData.ts b/app/hooks/useCountsMapData.ts index 7dc8d581..b79560a0 100644 --- a/app/hooks/useCountsMapData.ts +++ b/app/hooks/useCountsMapData.ts @@ -1,6 +1,7 @@ import { useMemo } from "react"; import { pointData, PcrCounting } from "typings"; import { IntlDateStr } from "~/services/utils"; +import { contagemSlug } from "~/utils/slugify"; const calculateMarkerSize = (totalCyclists: number) => { if (totalCyclists === 0) return 8; @@ -48,7 +49,7 @@ export function useCountsMapData(amecicloData: any[], pcrCounts: PcrCounting[]) name: ponto.name || "Contagem Ameciclo", total: totalCyclists, date: latestCount?.date ? IntlDateStr(latestCount.date) : "Sem data", - url: `/dados/contagens/${ponto.id}`, + url: latestCount?.date ? `/dados/contagens/${contagemSlug(latestCount.date, ponto.name || '')}` : `/dados/contagens/${ponto.id}`, obs: "As nossas contagens são registradas manualmente através da observação das pessoas voluntárias, registrando a direção do deslocamento e fatores qualitativos.", }, size: calculateMarkerSize(totalCyclists), diff --git a/app/loader/compareContagensLoader.ts b/app/loader/compareContagensLoader.ts index 11a9c264..033ec072 100644 --- a/app/loader/compareContagensLoader.ts +++ b/app/loader/compareContagensLoader.ts @@ -1,6 +1,7 @@ import { json, LoaderFunctionArgs } from "@remix-run/node"; -import { COUNTINGS_ATLAS_LOCATION } from "~/servers"; +import { COUNTINGS_ATLAS_LOCATION, COUNTINGS_ATLAS_LOCATIONS } from "~/servers"; import { fetchWithTimeout } from "~/services/fetchWithTimeout"; +import { contagemSlug } from "~/utils/slugify"; function getBoxesForCountingComparision(data: any[]) { const boxes = data.map((location) => { @@ -32,11 +33,16 @@ function getBoxesForCountingComparision(data: any[]) { return boxes; } -const fetchLocationData = async (locationId: string) => { +const fetchLocationData = async (locationId: string, countId?: string) => { try { const data = await fetchWithTimeout(COUNTINGS_ATLAS_LOCATION(locationId), { cache: "no-cache" }, 5000, null); if (!data) return null; - + + if (countId && data.counts) { + const specificCount = data.counts.find((c: any) => c.id.toString() === countId); + if (specificCount) return { ...data, selectedCount: specificCount }; + } + if (data.counts && data.counts.length > 0) { return { ...data, selectedCount: data.counts[0] }; } @@ -47,19 +53,36 @@ const fetchLocationData = async (locationId: string) => { } }; +const resolveSlug = (slug: string, locations: any[]): { locationId: string; countId?: string } | null => { + if (/^\d+$/.test(slug)) return { locationId: slug }; + for (const loc of locations) { + if (loc.counts && Array.isArray(loc.counts)) { + for (const count of loc.counts) { + if (contagemSlug(count.date, loc.name) === slug) { + return { locationId: loc.id.toString(), countId: count.id.toString() }; + } + } + } + } + return null; +}; + export const loader = async ({ params }: LoaderFunctionArgs) => { const slugParam = params.slug || ""; const compareSlugParam = params.compareSlug || ""; const toCompare = [slugParam, compareSlugParam].filter(Boolean); + const allLocations = await fetchWithTimeout(COUNTINGS_ATLAS_LOCATIONS, { cache: "no-cache" }, 5000, []); + const data = await Promise.all( - toCompare.map(async (locationId) => { - const result = await fetchLocationData(locationId); - return result; + toCompare.map(async (slug) => { + const resolved = resolveSlug(slug, allLocations || []); + if (!resolved) return null; + return fetchLocationData(resolved.locationId, resolved.countId); }) ); const boxes = getBoxesForCountingComparision(data.filter(Boolean)); return json({ boxes }); -}; \ No newline at end of file +}; diff --git a/app/loader/dados.contagens.$slug.compare.$compareSlug.ts b/app/loader/dados.contagens.$slug.compare.$compareSlug.ts index a472b291..fc4dc89f 100644 --- a/app/loader/dados.contagens.$slug.compare.$compareSlug.ts +++ b/app/loader/dados.contagens.$slug.compare.$compareSlug.ts @@ -2,59 +2,81 @@ import { defer, LoaderFunctionArgs } from "@remix-run/node"; import { loader as compareContagensLoader } from "~/loader/compareContagensLoader"; import { COUNTINGS_ATLAS_LOCATION, COUNTINGS_ATLAS_LOCATIONS, COUNTINGS_PAGE_DATA } from "~/servers"; import { fetchWithTimeout } from "~/services/fetchWithTimeout"; +import { contagemSlug } from "~/utils/slugify"; -export const loader = async ({ params }: LoaderFunctionArgs) => { - const fetchLocationData = async (locationId: string) => { - try { - const data = await fetchWithTimeout(COUNTINGS_ATLAS_LOCATION(locationId), { cache: "no-cache" }, 5000, null); - if (!data) return null; - - // Usar a contagem mais recente - if (data.counts && data.counts.length > 0) { - return { ...data, selectedCount: data.counts[0] }; - } - return data; - } catch (error) { - console.error('Error fetching location data:', error); - return null; +const fetchLocationData = async (locationId: string, countId?: string) => { + try { + const data = await fetchWithTimeout(COUNTINGS_ATLAS_LOCATION(locationId), { cache: "no-cache" }, 5000, null); + if (!data) return null; + + if (countId && data.counts) { + const specificCount = data.counts.find((c: any) => c.id.toString() === countId); + if (specificCount) return { ...data, selectedCount: specificCount }; } - }; - - const fetchData = async () => { - try { - const [pageDataRes, locationsRes] = await Promise.all([ - fetchWithTimeout(COUNTINGS_PAGE_DATA, { cache: "no-cache" }, 5000, null), - fetchWithTimeout(COUNTINGS_ATLAS_LOCATIONS, { cache: "no-cache" }, 5000, []) - ]); - - return { - pageCover: pageDataRes?.data || null, - otherCounts: locationsRes || [] - }; - } catch (error) { - console.error('Error fetching page data:', error); - return { pageCover: null, otherCounts: [] }; + + if (data.counts && data.counts.length > 0) { + return { ...data, selectedCount: data.counts[0] }; } - }; + return data; + } catch (error) { + console.error('Error fetching location data:', error); + return null; + } +}; + +const fetchPageData = async () => { + try { + const [pageDataRes, locationsRes] = await Promise.all([ + fetchWithTimeout(COUNTINGS_PAGE_DATA, { cache: "no-cache" }, 5000, null), + fetchWithTimeout(COUNTINGS_ATLAS_LOCATIONS, { cache: "no-cache" }, 5000, []) + ]); + return { + pageCover: pageDataRes?.data || null, + otherCounts: locationsRes || [] + }; + } catch (error) { + console.error('Error fetching page data:', error); + return { pageCover: null, otherCounts: [] }; + } +}; +const resolveSlug = (slug: string, locations: any[]): { locationId: string; countId?: string } | null => { + if (/^\d+$/.test(slug)) return { locationId: slug }; + for (const loc of locations) { + if (loc.counts && Array.isArray(loc.counts)) { + for (const count of loc.counts) { + if (contagemSlug(count.date, loc.name) === slug) { + return { locationId: loc.id.toString(), countId: count.id.toString() }; + } + } + } + } + return null; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { const slugParam = params.slug || ""; const compareSlugParam = params.compareSlug || ""; const toCompare = [slugParam, compareSlugParam].filter(Boolean); - + + const pageDataPromise = fetchPageData(); + const pageData = await pageDataPromise; + const locations: any[] = pageData.otherCounts || []; + const dataPromise = Promise.all( - toCompare.map(async (locationId) => { - const result = await fetchLocationData(locationId); - return result; + toCompare.map(async (slug) => { + const resolved = resolveSlug(slug, locations); + if (!resolved) return null; + return fetchLocationData(resolved.locationId, resolved.countId); }) ); - const pageDataPromise = fetchData(); const boxesPromise = compareContagensLoader({ params }).then(result => result.json()); - return defer({ - dataPromise, - pageDataPromise, - boxesPromise, - toCompare + return defer({ + dataPromise, + pageDataPromise: pageData, + boxesPromise, + toCompare }); -}; \ No newline at end of file +}; diff --git a/app/loader/dados.contagens.$slug.ts b/app/loader/dados.contagens.$slug.ts index 794dd57d..2474e3a7 100644 --- a/app/loader/dados.contagens.$slug.ts +++ b/app/loader/dados.contagens.$slug.ts @@ -1,55 +1,69 @@ import { defer, LoaderFunctionArgs } from "@remix-run/node"; import { COUNTINGS_ATLAS_LOCATION, COUNTINGS_ATLAS_LOCATIONS, COUNTINGS_PAGE_DATA } from "~/servers"; import { fetchWithTimeout } from "~/services/fetchWithTimeout"; +import { contagemSlug } from "~/utils/slugify"; + +const fetchLocationData = async (locationId: string, countId?: string) => { + try { + const data = await fetchWithTimeout(COUNTINGS_ATLAS_LOCATION(locationId), { cache: "no-cache" }, 5000, null); + if (!data) return null; + + if (countId && data.counts) { + const specificCount = data.counts.find((c: any) => c.id.toString() === countId); + if (specificCount) return { ...data, selectedCount: specificCount }; + } + + if (data.counts && data.counts.length > 0) { + return { ...data, selectedCount: data.counts[0] }; + } + + return data; + } catch (error) { + console.error('Error fetching location data:', error); + return null; + } +}; + +const fetchPageData = async () => { + try { + const [pageDataRes, locationsRes] = await Promise.all([ + fetchWithTimeout(COUNTINGS_PAGE_DATA, { cache: "no-cache" }, 5000, null), + fetchWithTimeout(COUNTINGS_ATLAS_LOCATIONS, { cache: "no-cache" }, 5000, []) + ]); + return { + pageCover: pageDataRes?.data || null, + otherCounts: locationsRes || [] + }; + } catch (error) { + console.error('Error fetching page data:', error); + return { pageCover: null, otherCounts: [] }; + } +}; export const loader = async ({ params }: LoaderFunctionArgs) => { - const fetchLocationData = async (locationId: string, countId?: string) => { - const URL = COUNTINGS_ATLAS_LOCATION(locationId); - try { - const data = await fetchWithTimeout(URL, { cache: "no-cache" }, 5000, null); - if (!data) return null; - - // Se countId foi especificado, buscar essa contagem específica - if (countId && data.counts) { - const specificCount = data.counts.find((c: any) => c.id.toString() === countId); - if (specificCount) { - return { ...data, selectedCount: specificCount }; + const slug = params.slug as string; + const isNumeric = /^\d+$/.test(slug); + + if (isNumeric) { + const dataPromise = fetchLocationData(slug); + const pageDataPromise = fetchPageData(); + return defer({ dataPromise, pageDataPromise }); + } + + // Slug textual: buscar locations para resolver o slug + const pageData = await fetchPageData(); + const locations: any[] = pageData.otherCounts || []; + + for (const loc of locations) { + if (loc.counts && Array.isArray(loc.counts)) { + for (const count of loc.counts) { + if (contagemSlug(count.date, loc.name) === slug) { + const dataPromise = fetchLocationData(loc.id.toString(), count.id.toString()); + return defer({ dataPromise, pageDataPromise: pageData }); } } - - // Caso contrário, usar a contagem mais recente (primeira do array) - if (data.counts && data.counts.length > 0) { - return { ...data, selectedCount: data.counts[0] }; - } - - return data; - } catch (error) { - console.error('Error fetching location data:', error); - return null; - } - }; - - const fetchPageData = async () => { - try { - const [pageDataRes, locationsRes] = await Promise.all([ - fetchWithTimeout(COUNTINGS_PAGE_DATA, { cache: "no-cache" }, 5000, null), - fetchWithTimeout(COUNTINGS_ATLAS_LOCATIONS, { cache: "no-cache" }, 5000, []) - ]); - - return { - pageCover: pageDataRes?.data || null, - otherCounts: locationsRes || [] - }; - } catch (error) { - console.error('Error fetching page data:', error); - return { pageCover: null, otherCounts: [] }; } - }; - - // params.slug agora é o locationId - const locationId = params.slug as string; - const dataPromise = fetchLocationData(locationId); - const pageDataPromise = fetchPageData(); + } - return defer({ dataPromise, pageDataPromise }); -}; \ No newline at end of file + return defer({ dataPromise: null, pageDataPromise: pageData }); +}; diff --git a/app/loader/projetos.ts b/app/loader/projetos.ts index 8fd0d725..ccd7ea65 100644 --- a/app/loader/projetos.ts +++ b/app/loader/projetos.ts @@ -1,6 +1,10 @@ -import { json } from "@remix-run/node"; +import { json, LoaderFunctionArgs } from "@remix-run/node"; import { fetchWithTimeout } from "~/services/fetchWithTimeout"; import { PROJECTS_LIST_DATA, WORKGROUPS_LIST_DATA, PROJECT_DETAIL_DATA } from "~/servers"; +import fs from 'fs/promises'; +import path from 'path'; + +type LoaderFunction = (args: LoaderFunctionArgs) => Promise; export const projetosLoader: LoaderFunction = async () => { const errors: Array<{url: string, error: string}> = []; @@ -35,6 +39,54 @@ export const projetoLoader: LoaderFunction = async ({ params }) => { errors.push({ url, error }); }; + // Verificar se é uma tradução + const isTranslation = projeto?.endsWith('_en') || projeto?.endsWith('_es'); + + if (isTranslation) { + try { + const filePath = path.join(process.cwd(), 'public', 'data', `${projeto}.json`); + const fileContent = await fs.readFile(filePath, 'utf-8'); + const translationData = JSON.parse(fileContent); + + if (translationData?.data && translationData.data.length > 0) { + let baseSlug = projeto || ''; + if (baseSlug.endsWith('_en')) baseSlug = baseSlug.replace('_en', ''); + if (baseSlug.endsWith('_es')) baseSlug = baseSlug.replace('_es', ''); + + const availableTranslations = []; + + // Verificar PT + try { + const ptRes = await fetchWithTimeout(PROJECT_DETAIL_DATA(baseSlug), {}, 2000, null, () => {}); + if (ptRes?.data && ptRes.data.length > 0) { + availableTranslations.push({ lang: 'pt', slug: baseSlug }); + } + } catch {} + + // Verificar EN + try { + const enPath = path.join(process.cwd(), 'public', 'data', `${baseSlug}_en.json`); + await fs.access(enPath); + availableTranslations.push({ lang: 'en', slug: `${baseSlug}_en` }); + } catch {} + + // Verificar ES + try { + const esPath = path.join(process.cwd(), 'public', 'data', `${baseSlug}_es.json`); + await fs.access(esPath); + availableTranslations.push({ lang: 'es', slug: `${baseSlug}_es` }); + } catch {} + + return json({ + project: translationData.data[0], + availableTranslations, + apiDown: false, + apiErrors: [] + }); + } + } catch {} + } + const projectUrl = PROJECT_DETAIL_DATA(projeto); const projects = await fetchWithTimeout(projectUrl, {}, 3000, null, onError(projectUrl)); @@ -42,8 +94,26 @@ export const projetoLoader: LoaderFunction = async ({ params }) => { throw new Response("Not Found", { status: 404 }); } + let baseSlug = projeto || ''; + const availableTranslations = []; + + availableTranslations.push({ lang: 'pt', slug: baseSlug }); + + try { + const enPath = path.join(process.cwd(), 'public', 'data', `${baseSlug}_en.json`); + await fs.access(enPath); + availableTranslations.push({ lang: 'en', slug: `${baseSlug}_en` }); + } catch {} + + try { + const esPath = path.join(process.cwd(), 'public', 'data', `${baseSlug}_es.json`); + await fs.access(esPath); + availableTranslations.push({ lang: 'es', slug: `${baseSlug}_es` }); + } catch {} + return json({ project: projects.data[0], + availableTranslations, apiDown: errors.length > 0, apiErrors: errors }); diff --git a/app/routes/dados.contagens.$slug._index.tsx b/app/routes/dados.contagens.$slug._index.tsx index ec36cd09..a5bc859c 100644 --- a/app/routes/dados.contagens.$slug._index.tsx +++ b/app/routes/dados.contagens.$slug._index.tsx @@ -4,7 +4,7 @@ import { StatisticsBox } from "~/components/ExecucaoCicloviaria/StatisticsBox"; import { InfoCards } from "~/components/Contagens/InfoCards"; import { AmecicloMap } from "~/components/Commom/Maps/AmecicloMap"; import { CountingComparisionTable } from "~/components/Contagens/CountingComparisionTable"; -import { useLoaderData, Await } from "@remix-run/react"; +import { useLoaderData, Await, useParams } from "@remix-run/react"; import { Suspense } from "react"; import { loader } from "~/loader/dados.contagens.$slug"; import { @@ -13,14 +13,16 @@ import { getCountingStatistics, transformOtherCountsForComparison, } from "~/services/counting-details.service"; +import { contagemSlug } from "~/utils/slugify"; export { loader }; const Contagem = () => { const { dataPromise, pageDataPromise } = useLoaderData(); + const params = useParams(); return ( -
+
}> {(pageData) => ( @@ -60,7 +62,6 @@ const Contagem = () => { {(data) => { if (!data) return null; const pointsData = getPointsData(data, data.selectedCount); - const flowData = getFlowData(data, data.selectedCount); return (
@@ -115,11 +116,14 @@ const Contagem = () => { {([data, pageData]) => { if (!data) return null; - const allCounts = transformOtherCountsForComparison(pageData.otherCounts || [], data.id); + const allCounts = transformOtherCountsForComparison(pageData.otherCounts || [], data.id, data.selectedCount?.id); + const currentSlug = data.selectedCount?.date + ? contagemSlug(data.selectedCount.date, data.name) + : data.id.toString(); return ( ); }} diff --git a/app/routes/dados.contagens.$slug.compare.$compareSlug.tsx b/app/routes/dados.contagens.$slug.compare.$compareSlug.tsx index 29e1f1d5..ff9b6a5b 100644 --- a/app/routes/dados.contagens.$slug.compare.$compareSlug.tsx +++ b/app/routes/dados.contagens.$slug.compare.$compareSlug.tsx @@ -12,6 +12,8 @@ import Breadcrumb from "~/components/Commom/Breadcrumb"; import { colors } from "~/components/Charts/FlowChart/FlowContainer"; import { VerticalStatisticsBoxes } from "~/components/Contagens/VerticalStatisticsBoxes"; import { Tooltip } from "~/components/Commom/Tooltip"; +import { contagemSlug } from "~/utils/slugify"; +import { IntlDateStr } from "~/services/utils"; interface Series { name: string | undefined; @@ -68,7 +70,11 @@ function getPointsDataForComparingCounting(data: any[]) { const lng = parseFloat(location.longitude); if (isNaN(lat) || isNaN(lng)) return null; - + + const slug = count.date + ? contagemSlug(count.date, location.name) + : String(location.id); + return { key: location.name, latitude: lat, @@ -77,8 +83,8 @@ function getPointsDataForComparingCounting(data: any[]) { popup: { name: location.name, total: count.total_cyclists || 0, - date: new Intl.DateTimeFormat("pt-BR").format(new Date(count.date)), - url: `/dados/contagens/${location.id}`, + date: count.date ? IntlDateStr(count.date) : '', + url: `/dados/contagens/${slug}`, obs: "" }, size: Math.round((count.total_cyclists || 0) / 250) + 15, @@ -128,14 +134,19 @@ export default function Compare() { - {data.map((location: any, index: number) => ( -
  • - {location?.name || 'Contagem'} - - - -
  • - ))} + {data.map((location: any, index: number) => { + const locationSlug = location?.selectedCount?.date + ? contagemSlug(location.selectedCount.date, location.name) + : String(location?.id || ''); + return ( +
  • + {location?.name || 'Contagem'} + + + +
  • + ); + })}
  • Comparação
  • @@ -166,7 +177,7 @@ export default function Compare() { text: index === 0 ? 'text-teal-700' : 'text-emerald-700', accent: index === 0 ? 'bg-teal-500' : 'bg-emerald-500' }; - + return (
    @@ -174,7 +185,7 @@ export default function Compare() {
    {data[index]?.name || `Ponto ${index + 1}`}
    - {box.date} + {box.date ? IntlDateStr(box.date) : ''}

    {box.title}

    @@ -295,12 +306,19 @@ export default function Compare() { )}
    - - Ver Detalhes Completos - + {(() => { + const detailSlug = data[index]?.selectedCount?.date + ? contagemSlug(data[index].selectedCount.date, data[index].name) + : String(data[index]?.id); + return ( + + Ver Detalhes Completos + + ); + })()}
    @@ -351,11 +369,23 @@ export default function Compare() { {([data, pageData, compareIds]) => { const excludeIds = data.map((d: any) => d?.id).filter(Boolean); - const filteredData = pageData.otherCounts.filter((d: any) => !excludeIds.includes(d.id)); + const flatData = (pageData.otherCounts || []) + .filter((loc: any) => !excludeIds.includes(loc.id)) + .flatMap((loc: any) => + (loc.counts || []).map((count: any) => ({ + id: loc.id, + name: loc.name, + date: count.date, + total_cyclists: count.total_cyclists, + })) + ); + const currentSlug = data[0]?.selectedCount?.date + ? contagemSlug(data[0].selectedCount.date, data[0].name) + : compareIds[0]; return ( ); }} @@ -364,4 +394,4 @@ export default function Compare() {
    ); -} \ No newline at end of file +} diff --git a/app/routes/projetos.$projeto.tsx b/app/routes/projetos.$projeto.tsx index c3dd8282..84e12f13 100644 --- a/app/routes/projetos.$projeto.tsx +++ b/app/routes/projetos.$projeto.tsx @@ -1,12 +1,23 @@ import { useLoaderData } from "@remix-run/react"; +import { MetaFunction } from "@remix-run/node"; import Breadcrumb from "~/components/Commom/Breadcrumb"; import ReactMarkdown from "react-markdown"; import { projetoLoader } from "~/loader/projetos"; import { useState } from "react"; import ImageGalleryWithZoom from '~/components/Commom/ImageGalleryWithZoom'; +import { LanguageSelector } from "~/components/Projetos/LanguageSelector"; +import { ProjectSteps } from "~/components/Projetos/ProjectSteps"; export const loader = projetoLoader; +export const meta: MetaFunction = ({ data }) => { + const projectName = data?.project?.name || "Projeto"; + return [ + { title: projectName }, + { name: "description", content: data?.project?.description || "" }, + ]; +}; + const ProjectDate = ({ project }: any) => { const dateOption: Intl.DateTimeFormatOptions = { year: "numeric", @@ -15,18 +26,18 @@ const ProjectDate = ({ project }: any) => { return ( project.startDate && ( -
    - +
    + - + {new Date(project.startDate) .toLocaleDateString("pt-br", dateOption) .toUpperCase()} - a - + + {project.endDate ? new Date(project.endDate) .toLocaleDateString("pt-br", dateOption) @@ -138,25 +149,6 @@ export default function Projeto() { const otherLinks = project?.Links || []; - // Lógica de tradução: detectar idioma atual e criar links para outros idiomas - const currentSlug = project?.slug || ''; - let baseSlug = currentSlug; - let currentLang = 'pt'; - - if (currentSlug.endsWith('_en')) { - baseSlug = currentSlug.replace('_en', ''); - currentLang = 'en'; - } else if (currentSlug.endsWith('_es')) { - baseSlug = currentSlug.replace('_es', ''); - currentLang = 'es'; - } - - const translations = [ - { lang: 'pt', flag: '🇧🇷', label: 'Português', slug: baseSlug }, - { lang: 'en', flag: '🇬🇧', label: 'English', slug: `${baseSlug}_en` }, - { lang: 'es', flag: '🇪🇸', label: 'Español', slug: `${baseSlug}_es` }, - ].filter(t => t.lang !== currentLang); - // Converter rich text blocks para Markdown const getLongDescription = () => { if (!project?.long_description) return null; @@ -207,7 +199,7 @@ export default function Projeto() { __html: ` .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6 { color: #1f2937; - font-weight: 600; + font-weight: 400; margin-top: 1.5em; margin-bottom: 0.5em; } @@ -216,9 +208,10 @@ export default function Projeto() { .markdown-content h3 { font-size: 1.5rem; } .markdown-content h4 { font-size: 1.25rem; } .markdown-content p { - margin-bottom: 1em; /* Adjusted for better flow */ - line-height: 1.7; /* Adjusted for better readability */ + margin-bottom: 1em; + line-height: 1.7; text-align: justify; + text-indent: 3em; } .markdown-content ul { list-style-type: disc; @@ -283,33 +276,23 @@ export default function Projeto() {
    -
    -
    - {translations.length > 0 && ( -
    - {translations.map((t) => ( - - - - ))} +
    +
    + {(project?.slug === 'bota_pra_rodar' || project?.slug?.startsWith('bota_pra_rodar_')) && ( +
    +
    )}

    {project?.goal}

    -
    +
    {(project?.startDate || project?.endDate) && ( )} @@ -318,20 +301,20 @@ export default function Projeto() { className="flex flex-col items-center justify-center w-full pt-4 mt-6 lg:pt-4 lg:flex-row" style={{ textTransform: "uppercase" }} > -
    - +
    + Cultura da Bicicleta
    -
    - +
    + Articulação Institucional
    -
    - +
    + Incidência Política @@ -341,7 +324,7 @@ export default function Projeto() { {otherLinks.map((link: any) => (
    -
    + {(project?.slug === 'bota_pra_rodar' || project?.slug?.startsWith('bota_pra_rodar_')) && ( + + )} + +
    {project?.steps && project.steps.length > 0 && (
    @@ -368,7 +355,7 @@ export default function Projeto() { )}
    -
    +
    {longDescription ? ( {longDescription} @@ -427,7 +414,7 @@ export default function Projeto() {
    )} -
    +
    {project?.partners && project.partners.length > 0 && (
    {project.partners.map((partner: any) => ( diff --git a/app/services/counting-details.service.ts b/app/services/counting-details.service.ts index 5f73de81..e53c04a9 100644 --- a/app/services/counting-details.service.ts +++ b/app/services/counting-details.service.ts @@ -66,16 +66,16 @@ export function getCountingStatistics(location: any, count: any) { ]; } -export function transformOtherCountsForComparison(otherCounts: any[], currentLocationId: number) { +export function transformOtherCountsForComparison(otherCounts: any[], currentLocationId: number, currentCountId?: number) { return (otherCounts || []) - .filter((loc: any) => loc.id !== currentLocationId) .flatMap((loc: any) => (loc.counts || []).map((count: any) => ({ id: loc.id, name: loc.name, - slug: loc.id.toString(), date: count.date, + countId: count.id, total_cyclists: count.total_cyclists, })) - ); + ) + .filter((row: any) => !(row.id === currentLocationId && row.countId === currentCountId)); } diff --git a/app/services/utils.ts b/app/services/utils.ts index 288ba412..29e9e41f 100644 --- a/app/services/utils.ts +++ b/app/services/utils.ts @@ -13,7 +13,17 @@ export const IntlPercentil = (n: number): string => { }; export const IntlDateStr = (str: string): string => { + if (!str) return ''; + const parts = str.slice(0, 10).split('-'); + if (parts.length === 3) { + const [year, month, day] = parts.map(Number); + if (!isNaN(year) && !isNaN(month) && !isNaN(day)) { + return new Intl.DateTimeFormat("pt-BR").format(new Date(year, month - 1, day)); + } + } + // Fallback para formatos não-YYYY-MM-DD const date = new Date(str); + if (isNaN(date.getTime())) return str; return new Intl.DateTimeFormat("pt-BR").format(date); }; diff --git a/app/utils/slugify.ts b/app/utils/slugify.ts index 36cfb14b..8d34b3f7 100644 --- a/app/utils/slugify.ts +++ b/app/utils/slugify.ts @@ -14,4 +14,20 @@ export function unslugify(slug: string): string { .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); +} + +/** + * Gera slug de contagem no formato: aaaa_mm_dd-nome_da_contagem + * Ex: "2022-03-30" + "Av. Rui Barbosa x R. Amélia" → "2022_03_30-av_rui_barbosa_x_r_amelia" + */ +export function contagemSlug(date: string, name: string): string { + const datePart = date.slice(0, 10).replace(/-/g, '_'); + const namePart = name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9\s]/g, '') + .trim() + .replace(/\s+/g, '_'); + return `${datePart}-${namePart}`; } \ No newline at end of file diff --git a/public/data/bota_pra_rodar_en.json b/public/data/bota_pra_rodar_en.json new file mode 100644 index 00000000..b1ac70f5 --- /dev/null +++ b/public/data/bota_pra_rodar_en.json @@ -0,0 +1,256 @@ +{ + "data": [ + { + "id": 9999, + "slug": "bota_pra_rodar_en", + "name": "Bota pra Rodar", + "goal": "Recover unused bicycles to enhance the right to the city through community shared bicycle systems.", + "description": "Bota Pra Rodar is a community integration project that, through meetings, dialogue circles on topics related to the right to the city, basic mechanics and entrepreneurship workshops, and the assembly of a community shared bicycle system, built in collaboration between Ameciclo members and residents of participating communities through the donation of used bicycles.", + "long_description": [ + { + "type": "heading", + "level": 2, + "children": [{ "text": "What is Bota Pra Rodar?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Bota Pra Rodar is a community integration project that, through meetings, dialogue circles on topics related to the right to the city, basic mechanics and entrepreneurship workshops, and the assembly of a community shared bicycle system, built in collaboration between Ameciclo members and residents of participating communities through the donation of used bicycles. Bicycles that are idle, gathering dust, usually in more vertical and privileged areas of the city, are transformed through this project into a tool for transforming the city into a more human, democratic and sustainable environment.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "The project is a tool to combat socio-spatial inequalities, a very striking characteristic in the city of Recife, which is one of the most unequal capitals in the country, where instruments of access and exercise of rights (such as leisure, education, health and mobility) are concentrated in specific areas of the city, characterizing as a privilege something that is everyone's right.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Believing that mobility is an essential right that allows people to access other fundamental rights, Bota Pra Rodar, through the repair of used and donated bicycles, allows men and women to move around the city, whether to work, their children's school or leisure and sociability spaces, using a safe, cheap and sustainable means of transport. In this way, the project strengthens the bicycle culture in the peripheries of Recife (which already has the bicycle as a daily means of transport), promotes the autonomy of peripheral women, as many do not have their own means of transport other than their own bodies, and allows the exchange of knowledge about citizenship, collectivity and technical information about bicycle mechanics, thus providing another possibility of obtaining income for people who are currently without employment options.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Currently, in the context of the health crisis, the project contributes to reducing the risks of coronavirus contagion, as the bicycle is the safest means of transport as it promotes social distancing, enables one less person on buses and allows the person to circulate in open environments throughout the journey, factors that are essential for coexistence with the new coronavirus.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "How did the project come about?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Bota pra Rodar emerged from old Ameciclo projects that aimed to collect and donate bicycles. However, the question arose: what is the best way to donate a bicycle and who would deserve one? Thus, inspired by the White Bicycles movement in Amsterdam in the 1960s, a provocation by the anarchist group PROVOS, we decided to use the donations to set up a community shared bicycle system, seeking collections from vertical areas of the city and taking these bicycles to places with low access to mobility and other rights. This is how the Bota Pra Rodar project was born.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "How does it work?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Basically, the project works with the following flowchart:", + "type": "text" + } + ] + }, + { + "type": "list", + "format": "unordered", + "children": [ + { + "type": "list-item", + "children": [{ "text": "Campaign and articulation to collect bicycles", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Donor contacts Ameciclo", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Donor arranges a place to leave the bicycle OR arranges a time when we will collect the bicycle", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Bicycle undergoes repair through basic mechanics workshops with project participants who live in the contemplated community", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Bicycle is made available for community use through a sharing system self-managed by the community itself, with the help of an app developed in partnership with ThoughtWorks, exclusive to Bota Pra Rodar", "type": "text" }] + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "However, the project also includes workshops on citizenship and entrepreneurship, where we invite groups to give talks on the subject, showing the step-by-step process to start entrepreneurship, as well as inviting companies that work with bicycles to give talks about how they were planned and executed, sharing their achievements and difficulties with the group. In addition, in one of the Bota Pra Rodar units, in Vila de Santa Luzia, a mechanical workshop is being set up, which will be made available for community use and collectively managed by participants in the basic mechanics course promoted by the project, which also included workshops on associativism and corporatism.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "Where is it currently happening?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Currently, Bota Pra Rodar takes place in two neighborhoods in the western zone of Recife: Ilha do Retiro and Torre. In Ilha do Retiro, the project is installed in the ZEIS (Special Zones of Social Interest) Caranguejo-Tabaiares, through articulation with the Caranguejo-Tabaiares Community Library. In the Torre neighborhood, the project takes place in ZEIS Vila de Santa Luzia, in partnership with CEPAS - Centro de Ensino Popular e Assistência Social Santa Paula Frassinetti. Recently, we obtained approval, through a public call, for another installation of Bota Pra Rodar, through the Recife Secretariat of Environment and Sustainability, however, the resource has not yet been made available. Our interest is that Bota Pra Rodar is increasingly present in the city, strengthening the bicycle culture and providing safe and sustainable locomotion for the population.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "What have we achieved?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Our main achievement, without a doubt, is contributing to access to the city, which is the main focus of this project. When the project manages to get rolling and has its bicycles circulating in the city, it helps people who have little access to transportation to get to their workplaces, study and leisure places, having a possibility of daily transportation. Another achievement is the fact that the project has been replicated in other cities in Brazil, such as Queimados (RJ) and Boa Vista (DF).", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "In addition to the social good, Bota Pra Rodar was the winner of the Promoting Bicycle Mobility in Brazil Award 2017, obtaining recognition from an international company. From this project, Ameciclo was invited to participate in events such as Summit Mobilidade 2020 and Mostra Laços de Iniciativas de Impacto Social Positivo, through the organization Muitos de Nós, where we received mentorship to improve our institutional communication focused on the project.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "Who has already contributed to the project?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Since the first Bota Pra Rodar, the project has had financial support from institutions such as Fundo Casa Socioambiental (Caixa Econômica Federal), Itaú and Greenpeace, all through funding calls. This funding is what allows us to buy the tools and parts for replacing parts of donated bicycles that are no longer usable, pay the project management group and spend on other material resources, such as: graphic material, paint, transportation and other expenses that arise throughout the project. In addition to the calls, Ameciclo received, in 2017, the Bicycle Promotion Award in Brazil, with Bota Pra Rodar, and in 2018 the recognition from Folkersma and the Dutch Cycle Embassy, together with Pedala Queimados (RJ).", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "We also have help from partner institutions and groups, who volunteer to contribute to the project or who help us in articulations and strengthening the bicycle culture and active and sustainable mobility in their regions, such as: Caranguejo-Tabaiares Community Library, Associação Desportiva Cultural da Capoeira Arte Pernambucana, União dos Moradores da Caranguejo-Tabaiares, Grupo Pedala Queimados (RJ), Centro de Ensino Popular e Assistência Social Santa Paula Frassinetti and La Ursa Tours.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "We have the contribution of members who are willing to help with the demands that arise in the project's pedaling, each in their own way. And, of course, we had the essential contribution of everyone who let go of their bikes and got them rolling! Donations are the key point of this project. Without them, we would not be able to set up shared bicycle systems. Our heartfelt thanks to everyone who contributed to Bota Pra Rodar! ♥", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "How can you help?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Bota Pra Rodar is possible because we count on many hands and legs to guide this ride towards our mission as an Association, which is to transform the city, through the bicycle, into more human, democratic and sustainable environments. The project, to happen, relies on the collaboration of funders, management team, volunteer members and people who believe that sharing is a way to combat inequalities and help us by donating their idle bicycles or even promoting the project with friends, family or residents of their building.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "There are several ways you can contribute to the project, directly or indirectly:", + "type": "text" + } + ] + }, + { + "type": "list", + "format": "unordered", + "children": [ + { + "type": "list-item", + "children": [{ "text": "Donate your idle bicycle! The bicycle is a means of transport with great potential to transform cities into a humanized environment and more inviting to the occupation of the streets in a safe and pleasant way, without threatening the lives of passersby due to high speed, in addition to being a non-polluting means of transport. In addition, your idle bicycle can be the tool that other people need to access basic rights, but who are not covered by accessible public transport. Get it rolling!", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Talk to your building manager! The project might be just what they needed to clear out that bike rack full of idle bicycles gathering rust. Informing your neighbors about the project can enable people who didn't know what to do with their bicycles to find a way to get their bike moving and still help a great social project in your city. All good, right? Put Bota Pra Rodar on the agenda at the condominium meeting and it will be a success!", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Volunteer! Our members contribute a lot to the project, whether by making themselves available to collect donated bicycles (we do bicycle transportation!), or by joining in for other things that may arise in the project's pedaling, such as: workshops on specific topics, where someone volunteers to promote this exchange with the communities; events that promote active mobility, where integration between people from different areas of the city is important for the exchange of experiences. Join us! Everyone has something to offer and to learn from others. You can join the project's Working Group, which is open to all members, and works on the Telegram program. Not a member yet?! It's easy, just go back to the home page and click on 'Become a Member'. It's free, there's no queue and you'll also contribute to the association's representativeness. You'll be very welcome!", "type": "text" }] + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Still have questions about the project? Contact us. Our channels are in the footer of this page ; )", + "type": "text" + } + ] + } + ], + "project_status": "ongoing", + "bikeCulture": "high", + "instArticulation": "medium", + "politicIncidence": "medium", + "startDate": "2014-08-01", + "endDate": null, + "media": { + "url": "/projetos.webp" + }, + "cover": { + "url": "/projetos.webp" + }, + "workgroup": { + "name": "Mobility" + }, + "Links": [], + "gallery": [], + "products": [], + "partners": [], + "sponsors": [] + } + ] +} diff --git a/public/data/bota_pra_rodar_es.json b/public/data/bota_pra_rodar_es.json new file mode 100644 index 00000000..f2a7d802 --- /dev/null +++ b/public/data/bota_pra_rodar_es.json @@ -0,0 +1,256 @@ +{ + "data": [ + { + "id": 9998, + "slug": "bota_pra_rodar_es", + "name": "Bota pra Rodar", + "goal": "Recuperar bicicletas en desuso para potenciar el derecho a la ciudad a través de sistemas de bicicletas comunitarias compartidas.", + "description": "Bota Pra Rodar es un proyecto de integración comunitaria que, a través de encuentros, ruedas de diálogos sobre temas relacionados con el derecho a la ciudad, talleres de mecánica básica y emprendimiento, y el montaje de un sistema de bicicletas comunitarias compartidas, construido en colaboración entre miembros de Ameciclo y residentes de las comunidades participantes a través de la donación de bicicletas usadas.", + "long_description": [ + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Qué es Bota Pra Rodar?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Bota Pra Rodar es un proyecto de integración comunitaria que, a través de encuentros, ruedas de diálogos sobre temas que atraviesan el tema del derecho a la ciudad, talleres de mecánica básica y emprendimiento y montaje de un sistema de bicicletas comunitarias compartidas, montado en colaboración entre ameciclistas y residentes de las comunidades participantes, a través de la donación de bicicletas usadas. Bicicletas que se encuentran paradas, acumulando polvo, generalmente en lugares más verticales y privilegiados de la ciudad y que se transforman, a través de este proyecto, en una herramienta de transformación de la ciudad en un ambiente más humano, democrático y sostenible.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "El proyecto es una herramienta de combate a las desigualdades socioespaciales, característica muy marcada en la ciudad de Recife, que se configura como siendo una de las capitales más desiguales del país, donde instrumentos de acceso y ejercicio de derechos (como ocio, educación, salud y movilidad) se encuentran concentrados en zonas específicas de la ciudad, caracterizando como privilegio algo que es derecho de todos y todas.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Creyendo que la movilidad es un derecho imprescindible que permite que las personas accedan a otros derechos fundamentales, el Bota Pra Rodar, a través de reformas de bicicletas usadas y donadas, permite que hombres y mujeres puedan desplazarse por la ciudad, sea con destino al trabajo, escuela de sus niños o espacios de ocio y sociabilidad, utilizando un medio de transporte seguro, barato y sostenible. De esta forma, el proyecto fortalece la cultura de la bicicleta en las periferias de Recife (que ya tiene la bicicleta como medio de transporte del día a día), promueve la autonomía de las mujeres periféricas, en la medida en que muchas no poseen medios de transporte propios además de sus propios cuerpos y permite el intercambio de conocimientos sobre ciudadanía, colectividad e informaciones técnicas sobre mecánica de bicicleta, proporcionando, así, una posibilidad más de obtención de renta para personas que se encuentran, hoy, sin opciones de empleo.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Actualmente, en contexto de crisis sanitaria, el proyecto contribuye para la disminución de los riesgos de contagio por coronavirus, pues la bicicleta es el medio de transporte más seguro en la medida en que promueve el distanciamiento social, posibilita una persona menos en autobuses y posibilita que en todo el trayecto la persona esté circulando en ambientes abiertos, factores que son esenciales para la convivencia con el nuevo coronavirus.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Cómo surgió el proyecto?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "El Bota pra Rodar surgió a partir de antiguos proyectos de Ameciclo, que buscaban la recaudación y donación de bicicletas. Sin embargo, surgió la cuestión: ¿cuál es la mejor forma de donar una bicicleta y quién sería merecedor de una? Así, inspirándonos en el movimiento Bicicletas Blancas de Ámsterdam, de la década de 60, una provocación del grupo anarquista PROVOS, resolvimos, entonces, usar las donaciones para montar un sistema de bicicletas compartidas comunitarias, buscando recaudaciones de zonas verticales de la ciudad y llevando esas bicicletas para lugares de bajo acceso a la movilidad y otros derechos. Así nació el proyecto Bota Pra Rodar.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Cómo funciona?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Básicamente, el proyecto funciona con el siguiente flujo:", + "type": "text" + } + ] + }, + { + "type": "list", + "format": "unordered", + "children": [ + { + "type": "list-item", + "children": [{ "text": "Campaña y articulación para recaudación de bicicletas", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Donante entra en contacto con Ameciclo", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Donante combina lugar para dejar la bicicleta O combina horario en que recogeremos la bicicleta", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Bicicleta pasa por reforma, a través de talleres de mecánica básica con los participantes del proyecto que viven en la comunidad contemplada", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "Bicicleta es disponibilizada para uso comunitario, a través de un sistema de compartimiento autogestionado por la propia comunidad, con la ayuda de una aplicación desarrollada en asociación con ThoughtWorks, exclusiva para el Bota Pra Rodar", "type": "text" }] + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Sin embargo, el proyecto también cuenta con talleres sobre ciudadanía y emprendimiento, donde invitamos grupos de charla sobre el tema, mostrando el paso a paso para comenzar a emprender, así como invitamos empresas que trabajan con la bicicleta para dar charlas sobre cómo fueron planeadas y ejecutadas, trayendo sus conquistas y dificultades para compartir con el grupo. Además, en una de las unidades del Bota Pra Rodar, en la Vila de Santa Luzia, está siendo montado un taller mecánico, que será disponibilizado para uso comunitario y gestionado colectivamente por participantes del curso de mecánica básica promovido por el proyecto, que contó, también, con talleres sobre asociativismo y corporativismo.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Dónde está ocurriendo actualmente?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Actualmente, el Bota Pra Rodar ocurre en dos barrios de la zona oeste de Recife: Ilha do Retiro y Torre. En Ilha do Retiro, el proyecto está instalado en la ZEIS (Zonas Especiales de Interés Social) Caranguejo-Tabaiares, a través de la articulación con la Biblioteca Comunitaria Caranguejo-Tabaiares. Ya en el barrio de Torre, el proyecto ocurre en la ZEIS Vila de Santa Luzia, en articulación con el CEPAS - Centro de Enseñanza Popular y Asistencia Social Santa Paula Frassinetti. Recientemente, conseguimos la aprobación, a través de convocatoria, para una instalación más del Bota Pra Rodar, a través de la Secretaría de Medio Ambiente y Sustentabilidad de Recife, sin embargo, el recurso aún no fue viabilizado. Nuestro interés es que el Bota Pra Rodar esté cada vez más presente en la ciudad, fortaleciendo la cultura de la bicicleta y proporcionando la locomoción segura y sostenible de la población.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Qué conquistamos?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Nuestra principal conquista, sin dudas, es contribuir con el acceso a la ciudad, que es el principal foco de este proyecto. Cuando el proyecto consigue poner para rodar y tiene sus bicicletas circulando en la ciudad, ayuda personas que tienen poco acceso al transporte a conseguir llegar a sus locales de trabajo, estudio y ocio, teniendo una posibilidad de transporte diario. Otra conquista es el hecho del proyecto haber sido replicado en otras ciudades de Brasil, a ejemplo de Queimados (RJ) y Boa Vista (DF).", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Además del bien social, el Bota Pra Rodar fue vencedor del Premio Promoviendo la Movilidad por Bicicletas en Brasil 2017, obteniendo reconocimiento de una empresa internacional. A partir de ese proyecto, Ameciclo fue invitada a participar de eventos como Summit Mobilidade 2020 y Mostra Lazos de Iniciativas de Impacto Social Positivo, a través de la organización Muitos de Nós, en que ganamos mentoría para mejorar nuestra comunicación institucional volcada para el proyecto.", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Quién ya contribuyó con el proyecto?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Desde el primer Bota Pra Rodar, el proyecto contó con apoyo financiero de instituciones como Fundo Casa Socioambiental (Caixa Econômica Federal), Itaú y Greenpeace, todos a través de convocatorias de financiamiento. Ese financiamiento es lo que permite que compremos las herramientas y piezas para los cambios de partes de las bicicletas donadas que no son más utilizables, pagamos el grupo gestor del proyecto y gasto con otros recursos materiales, como: material gráfico, tinta, traslado y otros gastos que surgen a lo largo del proyecto. Además de las convocatorias, Ameciclo recibió, en 2017, el Premio Promoción de la Bicicleta en Brasil, con el Bota Pra Rodar, y en 2018 el reconocimiento de Folkersma y de la Dutch Cycle Embassy, juntamente con el Pedala Queimados (RJ).", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "También tenemos ayuda de instituciones y grupos socios, que se voluntarizan para contribuir con el proyecto o que nos ayudan en las articulaciones y fortaleciendo la cultura de la bicicleta y la movilidad activa y sostenible en sus regiones, como: Biblioteca Comunitaria Caranguejo-Tabaiares, Asociación Deportiva Cultural de la Capoeira Arte Pernambucana, Unión de los Moradores de Caranguejo-Tabaiares, Grupo Pedala Queimados (RJ), Centro de Enseñanza Popular y Asistencia Social Santa Paula Frassinetti y La Ursa Tours.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Tenemos la contribución de asociados que se disponen a ayudar en las demandas que surgen en el pedalear del proyecto, cada uno(a) a su manera. Y, claro, tuvimos la contribución imprescindible de todos y todas que se desapegaron de sus bicicletas y las pusieron para rodar! Las donaciones son el punto clave de ese proyecto. Sin ellas, no conseguiríamos montar los sistemas de bicicletas compartidas. ¡Nuestro muchas gracias a todo el mundo que contribuyó con el Bota Pra Rodar! ♥", + "type": "text" + } + ] + }, + { + "type": "heading", + "level": 2, + "children": [{ "text": "¿Cómo puedes ayudar?", "type": "text" }] + }, + { + "type": "paragraph", + "children": [ + { + "text": "El Bota Pra Rodar es posible porque contamos con muchas manos y piernas para guiar esa pedaleada a camino de nuestra misión mientras Asociación, que es transformar la ciudad, a través de la bicicleta, en ambientes más humanos, democráticos y sostenibles. El proyecto, para ocurrir, cuenta con la colaboración de financiadores, equipo de gestión, asociados voluntarios y de personas que creen que el compartimiento es una manera de combatir desigualdades y nos ayudan donando sus bicicletas paradas o mismo divulgando el proyecto con amigos, familiares o con los moradores de su edificio.", + "type": "text" + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "Existen varias maneras en que puedes contribuir para el proyecto, directa o indirectamente:", + "type": "text" + } + ] + }, + { + "type": "list", + "format": "unordered", + "children": [ + { + "type": "list-item", + "children": [{ "text": "¡Dona tu bicicleta parada! La bicicleta es un medio de transporte con mucho potencial para transformar las ciudades en un ambiente humanizado y más invitador a la ocupación de las calles de forma segura y agradable, sin amenazar la vida de transeúntes por alta velocidad, además de ser un medio de transporte no contaminante. Además, tu bicicleta parada puede ser la herramienta que otras personas están necesitando para acceder a derechos básicos, pero que no son contempladas por un transporte público accesible. ¡Pon para rodar!", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "¡Habla con tu síndico(a)! El proyecto puede ser todo lo que estaba necesitando para desahogar aquel bicicletero lleno de bicicleta parada acumulando herrumbre. Informar a tus vecinos sobre el proyecto puede posibilitar que las personas que no sabían lo que hacer con sus bicicletas encuentren un medio de desencallar la bicicleta y aún ayudan un proyecto social genial en tu ciudad. ¡Todo de bueno, no? ¡Coloca el Bota Pra Rodar como pauta en la reunión del condominio que va a ser éxito!", "type": "text" }] + }, + { + "type": "list-item", + "children": [{ "text": "¡Voluntarízate! Nuestros asociados y asociadas contribuyen mucho con el proyecto, sea disponibilizándose para hacer la colecta de las bicicletas donadas (¡hacemos el traslado de bicicleta!), sea llegando junto para otras cosas que pueden surgir en el pedalear del proyecto, como: talleres de temas específicos, donde tiene alguien que se voluntariza para promover ese intercambio con las comunidades; eventos que promuevan la movilidad activa, en que es importante la integración entre las personas de diversas áreas de la ciudad para el intercambio de experiencias. ¡Llega junto! Todo el mundo tiene algo para ofrecer y para aprender con los otros. Puedes entrar en el Grupo de Trabajo del proyecto, que es abierto para todos que son asociados, y funciona en el programa Telegram. ¿Aún no eres asociado?! Es fácil, solo volver para la página inicial y hacer clic en 'Asóciate'. Es gratis, no tiene fila y aún contribuirás para la representatividad de la asociación. ¡Serás bien venido!", "type": "text" }] + } + ] + }, + { + "type": "paragraph", + "children": [ + { + "text": "¿Aún tienes dudas sobre el proyecto? Entra en contacto con nosotros. Nuestros canales están en el pie de esta página ; )", + "type": "text" + } + ] + } + ], + "project_status": "ongoing", + "bikeCulture": "high", + "instArticulation": "medium", + "politicIncidence": "medium", + "startDate": "2014-08-01", + "endDate": null, + "media": { + "url": "/projetos.webp" + }, + "cover": { + "url": "/projetos.webp" + }, + "workgroup": { + "name": "Movilidad" + }, + "Links": [], + "gallery": [], + "products": [], + "partners": [], + "sponsors": [] + } + ] +}