Initial commit - Rentall App

- Full-stack rental marketplace application
- React frontend with TypeScript
- Node.js/Express backend with JWT authentication
- Features: item listings, rental requests, calendar availability, user profiles
This commit is contained in:
jackiettran
2025-07-15 21:21:09 -04:00
commit c09384e3ea
53 changed files with 24425 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
import React, { useState, useEffect, useRef } from 'react';
interface AddressSuggestion {
place_id: string;
display_name: string;
lat: string;
lon: string;
}
interface AddressAutocompleteProps {
value: string;
onChange: (value: string, lat?: number, lon?: number) => void;
placeholder?: string;
required?: boolean;
className?: string;
id?: string;
name?: string;
}
const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
value,
onChange,
placeholder = "Address",
required = false,
className = "form-control",
id,
name
}) => {
const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [loading, setLoading] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
const debounceTimer = useRef<number | undefined>(undefined);
// Handle clicking outside to close suggestions
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setShowSuggestions(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Cleanup timer on unmount
useEffect(() => {
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
};
}, []);
const fetchAddressSuggestions = async (query: string) => {
if (query.length < 3) {
setSuggestions([]);
return;
}
setLoading(true);
try {
// Using Nominatim API (OpenStreetMap) for free geocoding
// In production, you might want to use Google Places API or another service
const response = await fetch(
`https://nominatim.openstreetmap.org/search?` +
`q=${encodeURIComponent(query)}&` +
`format=json&` +
`limit=5&` +
`countrycodes=us`
);
if (response.ok) {
const data = await response.json();
setSuggestions(data);
}
} catch (error) {
console.error('Error fetching address suggestions:', error);
setSuggestions([]);
} finally {
setLoading(false);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
onChange(newValue);
setShowSuggestions(true);
// Debounce the API call
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = window.setTimeout(() => {
fetchAddressSuggestions(newValue);
}, 300);
};
const handleSuggestionClick = (suggestion: AddressSuggestion) => {
onChange(
suggestion.display_name,
parseFloat(suggestion.lat),
parseFloat(suggestion.lon)
);
setShowSuggestions(false);
setSuggestions([]);
};
return (
<div ref={wrapperRef} className="position-relative">
<input
type="text"
className={className}
id={id}
name={name}
value={value}
onChange={handleInputChange}
onFocus={() => setShowSuggestions(true)}
placeholder={placeholder}
required={required}
autoComplete="off"
/>
{showSuggestions && (suggestions.length > 0 || loading) && (
<div
className="position-absolute w-100 bg-white border rounded-bottom shadow-sm"
style={{ top: '100%', zIndex: 1000, maxHeight: '300px', overflowY: 'auto' }}
>
{loading ? (
<div className="p-2 text-center text-muted">
<small>Searching addresses...</small>
</div>
) : (
suggestions.map((suggestion) => (
<div
key={suggestion.place_id}
className="p-2 border-bottom cursor-pointer"
style={{ cursor: 'pointer' }}
onClick={() => handleSuggestionClick(suggestion)}
onMouseEnter={(e) => e.currentTarget.classList.add('bg-light')}
onMouseLeave={(e) => e.currentTarget.classList.remove('bg-light')}
>
<small className="d-block text-truncate">
{suggestion.display_name}
</small>
</div>
))
)}
</div>
)}
</div>
);
};
export default AddressAutocomplete;