-
Notifications
You must be signed in to change notification settings - Fork 227
Description
I'm building a SaaS with the following stack:
- Ruby 3.4.1
- Ruby on Rails 8.0.0
- React 19.0.0
- Vite 5.0.0
- Typescript 5.3.2
In my application, I was using react-material-ui-carousel (version 3.4.2) on a page without any issues. However, after adding react-leaflet (version 5.0.0-rc.2), the carousel I had and another one I tried to add stopped working. When I remove react-leaflet and its dependencies, the carousel functions correctly again.
The issue I observe is that the first image appears in the carousel, but all the other images render as blank divs.
This problem is known, and I found it mentioned in a few places(i.e.: here), but none of the solutions work when I have leaflet (version 1.9.4) installed. Since leaflet works when having the react-material-ui-carousel installed, I'm creating the issue here, but I'm unsure where I should report it first.
// package.json
{
"name": "marteleiro",
"private": true,
"type": "module",
"license": "MIT",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.1.1",
"@fortawesome/fontawesome-free": "6.1.1",
"@mui/icons-material": "^7.0.2",
"@mui/material": "^6.4.6",
"@mui/styles": "^6.4.8",
"@mui/x-date-pickers": "^7.27.1",
"@popperjs/core": "2.11.8",
"@rails/webpacker": "5.4.4",
"@react-oauth/google": "^0.12.1",
"@stripe/stripe-js": "^5.7.0",
"@types/leaflet": "^1.9.17",
"@vitejs/plugin-react-swc": "^3.8.0",
"axios": "^1.8.1",
"bootstrap": "5.3.2",
"dayjs": "^1.11.13",
"jquery": "3.7.1",
"js-cookie": "^3.0.5",
"leaflet": "^1.9.4",
"react": "^19.0.0",
"react-currency-mask": "^1.3.3",
"react-dom": "^19.0.0",
"react-facebook": "^9.0.12",
"react-hook-form": "^7.54.2",
"react-leaflet": "^5.0.0-rc.2",
"react-material-ui-carousel": "^3.4.2",
"react-router-dom": "^7.2.0",
"react-toastify": "^11.0.5",
"toastr": "2.1.4"
},
"version": "0.1.0",
"devDependencies": {
"@eslint/js": "^8.52.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^8.52.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.0.3",
"ts-jest": "^29.2.6",
"typescript": "^5.3.2",
"vite": "^5.0.0",
"vite-plugin-ruby": "^5.1.0",
"vite-tsconfig-paths": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"scripts": {
"test": "jest",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"format:check": "prettier --check .",
"format:fix": "prettier --write .",
"lint-format": "yarn format && yarn lint"
}
}
// Component with both leaflet and react-material-ui-carousel
import React, { useEffect, useState } from 'react';
import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet'
import { useParams, useNavigate } from 'react-router-dom';
import {
Box,
Typography,
Grid,
Paper,
Chip,
Divider,
Button,
Card,
CardContent,
Table,
TableBody,
TableCell,
TableContainer,
TableRow,
useTheme,
useMediaQuery,
IconButton,
Tabs,
Tab,
List,
ListItem,
ListItemIcon,
ListItemText,
Tooltip,
} from '@mui/material';
import {
Home,
BedOutlined,
ShowerOutlined,
DirectionsCar,
SquareFoot,
Favorite,
FavoriteBorder,
CalendarToday,
Gavel,
Description,
LocationOn,
DocumentScanner,
ArrowBack,
Balance,
LockPerson,
} from '@mui/icons-material';
import { formatCurrency } from '@utils/utils';
import { propertyService } from '@services/PropertyService';
import Loading from '@shared/Loading';
import Carousel from 'react-material-ui-carousel';
import '@styles/property_detail.scss';
// Interface para TabPanel
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
...
function a11yProps(index: number) {
return {
id: `property-tab-${index}`,
'aria-controls': `property-tabpanel-${index}`,
};
}
export default function PropertyDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const [property, setProperty] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [isFavorite, setIsFavorite] = useState(false);
const [tabValue, setTabValue] = useState(0);
const [opportunityId, setOpportunityId] = useState<string | null>(null);
const [currentImageIndex, setCurrentImageIndex] = useState(0);
useEffect(() => {
const fetchPropertyDetails = async () => {
try {
setLoading(true);
const propertyData = await propertyService.getPropertyById(id as string);
setProperty(propertyData);
// // Verificar se já é favorito
// const userOpportunities = await opportunityService.getUserOpportunities();
// const existingOpportunity = userOpportunities.find(
// (opp) => opp.property.id === id
// );
// if (existingOpportunity) {
// setIsFavorite(true);
// setOpportunityId(existingOpportunity.id);
// }
} catch (error) {
console.error('Erro ao buscar detalhes da propriedade:', error);
} finally {
setLoading(false);
}
};
if (id) {
fetchPropertyDetails();
}
}, [id]);
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
// const handleToggleFavorite = async () => {
// try {
// if (isFavorite && opportunityId) {
// // Remover dos favoritos
// await opportunityService.deleteOpportunity(opportunityId);
// setIsFavorite(false);
// setOpportunityId(null);
// } else {
// // Adicionar aos favoritos
// const response = await opportunityService.createOpportunity(id as string);
// setIsFavorite(true);
// setOpportunityId(response.id);
// }
// } catch (error) {
// console.error('Erro ao atualizar favoritos:', error);
// }
// };
const handleBack = () => {
navigate(-1);
};
...
<Grid container spacing={3}>
{/* Coluna da esquerda com imagens e informações principais */}
<Grid item xs={12} md={8}>
{/* Carrossel de imagens */}
{property.images && !!property.images.length ? (
<Paper elevation={0} sx={{ mb: 3, borderRadius: 2, overflow: 'hidden', position: 'relative' }}>
<Carousel
autoPlay={false}
animation="slide"
indicators={true}
navButtonsAlwaysVisible={true}
cycleNavigation={true}
index={currentImageIndex}
onChange={(index) => setCurrentImageIndex(index)}
navButtonsProps={{
style: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
padding: '10px',
margin: '0 20px',
zIndex: 2,
}
}}
indicatorContainerProps={{
style: {
position: 'absolute',
bottom: '16px',
zIndex: 999999,
}
}}
className="property-carousel"
>
{property.images.map((img_link: string, index: number) => (
<Box
key={index}
sx={{
height: isMobile ? '250px' : '400px',
width: '100%',
backgroundImage: `url(${img_link})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
}}
>
{/* <img
src={image}
alt={`Imagem ${index + 1} do imóvel ${property.name}`}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/> */}
</Box>
))}
</Carousel>
</Paper>
) : (
<Paper elevation={0} sx={{ mb: 3, borderRadius: 2, p: 5, textAlign: 'center', bgcolor: 'grey.100' }}>
<Typography variant="body1" color="text.secondary">
Nenhuma imagem disponível para este imóvel.
</Typography>
</Paper>
)}
{/* Miniaturas de imagens adicionais */}
{property.images && property.images.length > 1 && (
<Box sx={{ display: 'flex', gap: 1, mb: 3, overflowX: 'auto', pb: 1 }}>
{property.images.map((img: string, index: number) => (
<Box
key={index}
component="img"
src={img}
alt={`Imagem ${index + 1}`}
sx={{
height: 80,
width: 120,
objectFit: 'cover',
borderRadius: 1,
cursor: 'pointer',
border: currentImageIndex === index ? '2px solid #1976d2' : 'none',
opacity: currentImageIndex === index ? 1 : 0.7,
transition: 'all 0.2s ease-in-out',
'&:hover': {
opacity: 1,
transform: 'scale(1.05)',
}
}}
onClick={() => setCurrentImageIndex(index)}
/>
))}
</Box>
)}
{/* Informações principais */}
...
<Grid container spacing={2}>
{property.numBedrooms !== null && (
<Grid item xs={6} sm={3}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<BedOutlined color="primary" />
<Typography variant="body2" color="text.secondary">Quartos</Typography>
<Typography variant="h6">{property.numBedrooms}</Typography>
</Box>
</Grid>
)}
{property.numBathrooms !== null && (
<Grid item xs={6} sm={3}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<ShowerOutlined color="primary" />
<Typography variant="body2" color="text.secondary">Banheiros</Typography>
<Typography variant="h6">{property.numBathrooms}</Typography>
</Box>
</Grid>
)}
{property.numParkingSpots !== null && (
<Grid item xs={6} sm={3}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<DirectionsCar color="primary" />
<Typography variant="body2" color="text.secondary">Vagas</Typography>
<Typography variant="h6">{property.numParkingSpots}</Typography>
</Box>
</Grid>
)}
{property.area !== null && (
<Grid item xs={6} sm={3}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<SquareFoot color="primary" />
<Typography variant="body2" color="text.secondary">Área</Typography>
<Typography variant="h6">{property.area} m²</Typography>
</Box>
</Grid>
)}
</Grid>
</Paper>
{/* Tabs com detalhes, leilão, documentos, e mapa */}
<Paper sx={{ borderRadius: 2 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
variant={isMobile ? "scrollable" : "fullWidth"}
scrollButtons={isMobile ? "auto" : undefined}
sx={{ borderBottom: 1, borderColor: 'divider' }}
>
<Tab label="Detalhes" {...a11yProps(0)} />
<Tab label="Leilão" {...a11yProps(1)} />
<Tab label="Documentos" {...a11yProps(2)} />
<Tab label="Processos" {...a11yProps(3)} />
</Tabs>
<TabPanel value={tabValue} index={0}>
<Typography variant="h6" gutterBottom>Descrição</Typography>
<Typography variant="body1" paragraph>
{property.description || 'Descrição não disponível'}
</Typography>
<Typography variant="h6" gutterBottom>Características</Typography>
...
<TabPanel value={tabValue} index={2}>
<Typography variant="h6" gutterBottom>Documentos</Typography>
{property.documents && property.documents.length > 0 ? (
<List>
{property.documents.map((doc: any, index: number) => (
<ListItem
key={index}
button
component="a"
href={doc.url}
target="_blank"
>
<ListItemIcon>
<DocumentScanner />
</ListItemIcon>
<ListItemText
primary={doc.name || `Documento ${index + 1}`}
secondary={doc.description}
/>
</ListItem>
))}
</List>
) : (
<Typography variant="body1">
Nenhum documento disponível
</Typography>
)}
{property.propertyRegistrationUrl && (
<Button
variant="outlined"
startIcon={<DocumentScanner />}
href={property.propertyRegistrationUrl}
target="_blank"
sx={{ mt: 2, mr: 2 }}
>
Matrícula do imóvel
</Button>
)}
{property.propertyReportUrl && (
<Button
variant="outlined"
startIcon={<Description />}
href={property.propertyReportUrl}
target="_blank"
sx={{ mt: 2 }}
>
Laudo do imóvel
</Button>
)}
</TabPanel>
...
{(property.address?.latitude && property.address?.longitude) || (property.latitude && property.longitude) && (
<Paper sx={{ p: 3, mb: 3, borderRadius: 2 }}>
<Typography variant="h6" gutterBottom>
Localização
</Typography>
<MapContainer center={[property.address?.latitude || property.latitude, property.address?.longitude || property.longitude]} zoom={13} scrollWheelZoom={false} style={{ height: "200px" }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[property.address?.latitude || property.latitude, property.address?.longitude || property.longitude]}>
{/* <Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup> */}
</Marker>
</MapContainer>
</Paper>
)}
{
property.marketValue && (
<Paper sx={{ p: 3, mb: 3, borderRadius: 2, bgcolor: 'grey.100' }}>
<Typography variant="h6" gutterBottom color="text.secondary">
Valor de mercado estimado
</Typography>
<Typography variant="body2" color="text.secondary">
Esta funcionalidade estará disponível em breve. O valor será calculado com
base na média de diversos portais imobiliários.
</Typography>
</Paper>
)
}
</Grid>
</Grid>
</Box>
);
}