| x |
| |
| import React, { useState, useEffect } from 'react'; |
| import ReactDOM from 'react-dom'; |
| import styled from 'styled-components'; |
|
|
| |
| const theme = { |
| colors: { |
| primary: '#3B82F6', |
| accent: '#10B981', |
| background: '#F9FAFB', |
| text: '#111827', |
| secondary: '#6366F1', |
| white: '#FFFFFF', |
| gray100: '#F3F4F6', |
| gray200: '#E5E7EB', |
| gray500: '#6B7280', |
| gray700: '#374151', |
| }, |
| shadows: { |
| sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', |
| md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', |
| lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', |
| }, |
| radii: { |
| sm: '4px', |
| md: '8px', |
| lg: '12px', |
| full: '9999px', |
| }, |
| }; |
|
|
| |
| const Container = styled.div` |
| width: 100%; |
| height: 100%; |
| position: relative; |
| overflow: hidden; |
| `; |
|
|
| const MapContainer = styled.div` |
| width: 100%; |
| height: 100%; |
| position: absolute; |
| top: 0; |
| left: 0; |
| z-index: 0; |
| `; |
|
|
| const BottomSheet = styled.div` |
| position: fixed; |
| bottom: 0; |
| left: 0; |
| right: 0; |
| background: ${theme.colors.white}; |
| border-top-left-radius: ${theme.radii.lg}; |
| border-top-right-radius: ${theme.radii.lg}; |
| box-shadow: ${theme.shadows.lg}; |
| padding: 16px; |
| padding-bottom: env(safe-area-inset-bottom, 16px); |
| transform: ${props => props.open ? 'translateY(0)' : 'translateY(80%)'}; |
| transition: transform 0.3s ease-out; |
| z-index: 10; |
| max-height: 70vh; |
| overflow-y: auto; |
| `; |
|
|
| const ListingCard = styled.div` |
| background: ${theme.colors.white}; |
| border-radius: ${theme.radii.md}; |
| box-shadow: ${theme.shadows.sm}; |
| padding: 12px; |
| margin-bottom: 12px; |
| transition: all 0.2s ease; |
| |
| &:active { |
| transform: scale(0.98); |
| } |
| `; |
| |
| function App() { |
| const [map, setMap] = useState(null); |
| const [userLocation, setUserLocation] = useState(null); |
| const [showForm, setShowForm] = useState(false); |
| const [listings, setListings] = useState([]); |
| const [bottomSheetOpen, setBottomSheetOpen] = useState(true); |
| |
| useEffect(() => { |
| |
| const mapInstance = L.map('map').setView([41.2995, 69.2401], 13); |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
| }).addTo(mapInstance); |
| setMap(mapInstance); |
| |
| |
| if (navigator.geolocation) { |
| navigator.geolocation.getCurrentPosition( |
| position => { |
| const { latitude, longitude } = position.coords; |
| setUserLocation({ lat: latitude, lng: longitude }); |
| mapInstance.setView([latitude, longitude], 15); |
| |
| |
| L.marker([latitude, longitude], { |
| icon: L.divIcon({ |
| className: 'user-location-marker', |
| html: `<div style="background: ${theme.colors.primary}; width: 24px; height: 24px; border-radius: 50%; border: 3px solid white;"></div>` |
| }) |
| }).addTo(mapInstance); |
| |
| |
| fetchListings(latitude, longitude); |
| }, |
| error => { |
| console.error('Geolocation error:', error); |
| |
| setUserLocation({ lat: 41.2995, lng: 69.2401 }); |
| fetchListings(41.2995, 69.2401); |
| } |
| ); |
| } |
| |
| return () => { |
| mapInstance.remove(); |
| }; |
| }, []); |
| |
| const fetchListings = async (lat, lng) => { |
| try { |
| |
| const mockListings = [ |
| { |
| id: '1', |
| title: 'Cozy Apartment for Students', |
| price: 300, |
| category: 'house', |
| tags: ['For students'], |
| description: 'Nice apartment near university', |
| lat: lat + 0.005, |
| lng: lng + 0.005, |
| images: ['http://static.photos/house/640x360/1'] |
| }, |
| { |
| id: '2', |
| title: 'Office Space for Rent', |
| price: 500, |
| category: 'office', |
| tags: ['For office staff'], |
| description: 'Modern office in business district', |
| lat: lat - 0.003, |
| lng: lng + 0.007, |
| images: ['http://static.photos/office/640x360/1'] |
| } |
| ]; |
| setListings(mockListings); |
| |
| |
| mockListings.forEach(listing => { |
| L.marker([listing.lat, listing.lng], { |
| icon: L.divIcon({ |
| className: 'listing-marker', |
| html: `<div style="background: ${theme.colors.accent}; width: 20px; height: 20px; border-radius: 50%; border: 2px solid white;"></div>` |
| }) |
| }).addTo(map).on('click', () => { |
| |
| map.setView([listing.lat, listing.lng], 15); |
| }); |
| }); |
| } catch (error) { |
| console.error('Error fetching listings:', error); |
| } |
| }; |
| |
| return ( |
| <Container> |
| <MapContainer id="map" /> |
| |
| <BottomSheet open={bottomSheetOpen}> |
| <div className="flex justify-between items-center mb-4"> |
| <h2 className="text-lg font-semibold">Nearby Listings</h2> |
| <button |
| onClick={() => setBottomSheetOpen(!bottomSheetOpen)} |
| className="p-2 rounded-full hover:bg-gray-100" |
| > |
| <i data-feather={bottomSheetOpen ? "chevron-down" : "chevron-up"}></i> |
| </button> |
| </div> |
| |
| <div className="space-y-3"> |
| {listings.map(listing => ( |
| <ListingCard key={listing.id}> |
| <div className="flex"> |
| <div className="w-24 h-24 bg-gray-200 rounded-md overflow-hidden"> |
| <img |
| src={listing.images[0]} |
| alt={listing.title} |
| className="w-full h-full object-cover" |
| /> |
| </div> |
| <div className="ml-3 flex-1"> |
| <h3 className="font-medium">{listing.title}</h3> |
| <p className="text-sm text-gray-500 mt-1">${listing.price}/month</p> |
| <div className="flex items-center mt-2 text-sm text-gray-500"> |
| <i data-feather="map-pin" className="w-3 h-3 mr-1"></i> |
| <span>1.2 km away</span> |
| </div> |
| </div> |
| </div> |
| </ListingCard> |
| ))} |
| </div> |
| </BottomSheet> |
| {/* Floating action button for new listing */} |
| <button |
| className="fixed bottom-20 right-4 bg-blue-500 text-white p-3 rounded-full shadow-lg hover:bg-blue-600 transition-colors" |
| style={{ zIndex: 20 }} |
| onClick={() => setShowForm(true)} |
| > |
| <i data-feather="plus"></i> |
| </button> |
| |
| {showForm && <listing-form></listing-form>} |
| </Container> |
| ); |
| } |
|
|
| |
| ReactDOM.render(<App />, document.getElementById('root')); |