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;

View File

@@ -0,0 +1,596 @@
import React, { useState, useEffect } from 'react';
interface UnavailablePeriod {
id: string;
startDate: Date;
endDate: Date;
startTime?: string;
endTime?: string;
isRentalSelection?: boolean;
isAcceptedRental?: boolean;
}
interface AvailabilityCalendarProps {
unavailablePeriods: UnavailablePeriod[];
onPeriodsChange: (periods: UnavailablePeriod[]) => void;
priceType?: 'hour' | 'day';
isRentalMode?: boolean;
}
type ViewType = 'month' | 'week' | 'day';
const AvailabilityCalendar: React.FC<AvailabilityCalendarProps> = ({
unavailablePeriods,
onPeriodsChange,
priceType = 'hour',
isRentalMode = false
}) => {
const [currentDate, setCurrentDate] = useState(new Date());
const [viewType, setViewType] = useState<ViewType>('month');
const [selectionStart, setSelectionStart] = useState<Date | null>(null);
// Reset to month view if priceType is day and current view is week/day
useEffect(() => {
if (priceType === 'day' && (viewType === 'week' || viewType === 'day')) {
setViewType('month');
}
}, [priceType]);
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
const days: (Date | null)[] = [];
// Add empty cells for days before month starts
for (let i = 0; i < startingDayOfWeek; i++) {
days.push(null);
}
// Add all days in month
for (let i = 1; i <= daysInMonth; i++) {
days.push(new Date(year, month, i));
}
return days;
};
const getWeekDays = (date: Date) => {
const startOfWeek = new Date(date);
const day = startOfWeek.getDay();
const diff = startOfWeek.getDate() - day;
startOfWeek.setDate(diff);
const days: Date[] = [];
for (let i = 0; i < 7; i++) {
const day = new Date(startOfWeek);
day.setDate(startOfWeek.getDate() + i);
days.push(day);
}
return days;
};
const isDateInPeriod = (date: Date, period: UnavailablePeriod) => {
const start = new Date(period.startDate);
const end = new Date(period.endDate);
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
return checkDate >= start && checkDate <= end;
};
const isDateUnavailable = (date: Date) => {
return unavailablePeriods.some(period => {
const start = new Date(period.startDate);
const end = new Date(period.endDate);
start.setHours(0, 0, 0, 0);
end.setHours(23, 59, 59, 999);
const checkDate = new Date(date);
checkDate.setHours(0, 0, 0, 0);
return checkDate >= start && checkDate <= end;
});
};
const isDateFullyUnavailable = (date: Date) => {
// Check if there's a period that covers the entire day without specific times
const hasFullDayPeriod = unavailablePeriods.some(period => {
const start = new Date(period.startDate);
const end = new Date(period.endDate);
const checkDate = new Date(date);
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
checkDate.setHours(0, 0, 0, 0);
return checkDate >= start && checkDate <= end && !period.startTime && !period.endTime;
});
if (hasFullDayPeriod) return true;
// Check if all 24 hours are covered by hour-specific periods
const dateStr = date.toISOString().split('T')[0];
const hoursWithPeriods = new Set<number>();
unavailablePeriods.forEach(period => {
const periodDateStr = new Date(period.startDate).toISOString().split('T')[0];
if (periodDateStr === dateStr && period.startTime && period.endTime) {
const startHour = parseInt(period.startTime.split(':')[0]);
const endHour = parseInt(period.endTime.split(':')[0]);
for (let h = startHour; h <= endHour; h++) {
hoursWithPeriods.add(h);
}
}
});
return hoursWithPeriods.size === 24;
};
const isDatePartiallyUnavailable = (date: Date) => {
return isDateUnavailable(date) && !isDateFullyUnavailable(date);
};
const isHourUnavailable = (date: Date, hour: number) => {
return unavailablePeriods.some(period => {
const start = new Date(period.startDate);
const end = new Date(period.endDate);
// Check if date is within period
const dateOnly = new Date(date);
dateOnly.setHours(0, 0, 0, 0);
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
if (dateOnly < start || dateOnly > end) return false;
// If no specific times, entire day is unavailable
if (!period.startTime || !period.endTime) return true;
// Check specific hour
const startHour = parseInt(period.startTime.split(':')[0]);
const endHour = parseInt(period.endTime.split(':')[0]);
return hour >= startHour && hour <= endHour;
});
};
const handleDateClick = (date: Date) => {
// Check if this date has an accepted rental
const hasAcceptedRental = unavailablePeriods.some(p =>
p.isAcceptedRental && isDateInPeriod(date, p)
);
if (hasAcceptedRental) {
// Don't allow clicking on accepted rental dates
return;
}
if (!isRentalMode) {
toggleDateAvailability(date);
return;
}
// Check if clicking on an existing rental selection to clear it
const existingRental = unavailablePeriods.find(p =>
p.isRentalSelection && isDateInPeriod(date, p)
);
if (existingRental) {
// Clear the rental selection
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRental.id));
setSelectionStart(null);
return;
}
// Two-click selection in rental mode
if (!selectionStart) {
// First click - set start date
setSelectionStart(date);
} else {
// Second click - create rental period
const start = new Date(Math.min(selectionStart.getTime(), date.getTime()));
const end = new Date(Math.max(selectionStart.getTime(), date.getTime()));
// Check if any date in range is unavailable
let currentDate = new Date(start);
let hasUnavailable = false;
while (currentDate <= end) {
if (unavailablePeriods.some(p => !p.isRentalSelection && isDateInPeriod(currentDate, p))) {
hasUnavailable = true;
break;
}
currentDate.setDate(currentDate.getDate() + 1);
}
if (hasUnavailable) {
// Range contains unavailable dates, reset selection
setSelectionStart(null);
return;
}
// Clear existing rental selections and add new one
const nonRentalPeriods = unavailablePeriods.filter(p => !p.isRentalSelection);
const newPeriod: UnavailablePeriod = {
id: Date.now().toString(),
startDate: start,
endDate: end,
isRentalSelection: true
};
onPeriodsChange([...nonRentalPeriods, newPeriod]);
// Reset selection
setSelectionStart(null);
}
};
const toggleDateAvailability = (date: Date) => {
const dateStr = date.toISOString().split('T')[0];
if (isRentalMode) {
// In rental mode, only handle rental selections (green), not unavailable periods (red)
const existingRentalPeriod = unavailablePeriods.find(period => {
const periodStart = new Date(period.startDate).toISOString().split('T')[0];
const periodEnd = new Date(period.endDate).toISOString().split('T')[0];
return period.isRentalSelection && periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime;
});
// Check if date is already unavailable (not a rental selection)
const isUnavailable = unavailablePeriods.some(p =>
!p.isRentalSelection && isDateInPeriod(date, p)
);
if (isUnavailable) {
// Can't select unavailable dates
return;
}
if (existingRentalPeriod) {
// Remove the rental selection
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingRentalPeriod.id));
} else {
// Add new rental selection
const newPeriod: UnavailablePeriod = {
id: Date.now().toString(),
startDate: date,
endDate: date,
isRentalSelection: true
};
onPeriodsChange([...unavailablePeriods, newPeriod]);
}
} else {
// Original behavior for marking unavailable
const existingPeriod = unavailablePeriods.find(period => {
const periodStart = new Date(period.startDate).toISOString().split('T')[0];
const periodEnd = new Date(period.endDate).toISOString().split('T')[0];
return periodStart === dateStr && periodEnd === dateStr && !period.startTime && !period.endTime;
});
if (existingPeriod) {
// Remove the period to make it available
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id));
} else {
// Add new unavailable period for this date
const newPeriod: UnavailablePeriod = {
id: Date.now().toString(),
startDate: date,
endDate: date
};
onPeriodsChange([...unavailablePeriods, newPeriod]);
}
}
};
const toggleHourAvailability = (date: Date, hour: number) => {
const startTime = `${hour.toString().padStart(2, '0')}:00`;
const endTime = `${hour.toString().padStart(2, '0')}:59`;
const existingPeriod = unavailablePeriods.find(period => {
const periodDate = new Date(period.startDate).toISOString().split('T')[0];
const checkDate = date.toISOString().split('T')[0];
return periodDate === checkDate &&
period.startTime === startTime &&
period.endTime === endTime;
});
if (existingPeriod) {
// Remove the period to make it available
onPeriodsChange(unavailablePeriods.filter(p => p.id !== existingPeriod.id));
} else {
// Add new unavailable period for this hour
const newPeriod: UnavailablePeriod = {
id: Date.now().toString(),
startDate: date,
endDate: date,
startTime: startTime,
endTime: endTime
};
onPeriodsChange([...unavailablePeriods, newPeriod]);
}
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
};
const renderMonthView = () => {
const days = getDaysInMonth(currentDate);
const monthName = currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
return (
<>
<h6 className="text-center mb-3">{monthName}</h6>
<div className="d-grid" style={{ gridTemplateColumns: 'repeat(7, 1fr)', gap: '2px' }}>
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="text-center fw-bold p-2">
{day}
</div>
))}
{days.map((date, index) => {
let className = 'p-2 text-center';
let title = '';
let backgroundColor = undefined;
if (date) {
className += ' border cursor-pointer';
const rentalPeriod = unavailablePeriods.find(p =>
p.isRentalSelection && isDateInPeriod(date, p)
);
const acceptedRental = unavailablePeriods.find(p =>
p.isAcceptedRental && isDateInPeriod(date, p)
);
// Check if date is the selection start
const isSelectionStart = selectionStart && date.getTime() === selectionStart.getTime();
// Check if date would be in the range if this was the end date
const wouldBeInRange = selectionStart && date >=
new Date(Math.min(selectionStart.getTime(), date.getTime())) &&
date <= new Date(Math.max(selectionStart.getTime(), date.getTime()));
if (acceptedRental) {
className += ' text-white';
title = 'Booked - This date has an accepted rental';
backgroundColor = '#6f42c1';
} else if (rentalPeriod) {
className += ' bg-success text-white';
title = isRentalMode ? 'Selected for rental - Click to clear' : 'Selected';
} else if (isSelectionStart) {
className += ' bg-primary text-white';
title = 'Start date selected - Click another date to complete selection';
} else if (wouldBeInRange && !isDateFullyUnavailable(date) && !isDatePartiallyUnavailable(date)) {
className += ' bg-info bg-opacity-25';
title = 'Click to set as end date';
} else if (isDateFullyUnavailable(date)) {
className += ' bg-danger text-white';
title = isRentalMode ? 'Unavailable' : 'Fully unavailable - Click to make available';
} else if (isDatePartiallyUnavailable(date)) {
className += ' text-dark';
title = isRentalMode ? 'Partially unavailable' : 'Partially unavailable - Click to view details';
backgroundColor = '#ffeb3b';
} else {
className += ' bg-light';
title = isRentalMode ? 'Available - Click to select' : 'Available - Click to make unavailable';
}
}
return (
<div
key={index}
className={className}
onClick={() => date && handleDateClick(date)}
style={{
minHeight: '40px',
cursor: date ? 'pointer' : 'default',
backgroundColor: backgroundColor
}}
title={date ? title : ''}
>
{date?.getDate()}
</div>
);
})}
</div>
</>
);
};
const renderWeekView = () => {
const days = getWeekDays(currentDate);
const weekRange = `${formatDate(days[0])} - ${formatDate(days[6])}`;
const hours = Array.from({ length: 24 }, (_, i) => i);
return (
<>
<h6 className="text-center mb-3">{weekRange}</h6>
<div style={{ overflowX: 'auto' }}>
<table className="table table-bordered table-sm">
<thead>
<tr>
<th style={{ width: '60px' }}>Time</th>
{days.map((date, index) => (
<th key={index} className="text-center" style={{ minWidth: '100px' }}>
<div>{date.toLocaleDateString('en-US', { weekday: 'short' })}</div>
<div>{date.getDate()}</div>
</th>
))}
</tr>
</thead>
<tbody>
{hours.map(hour => (
<tr key={hour}>
<td className="text-center small">
{hour.toString().padStart(2, '0')}:00
</td>
{days.map((date, dayIndex) => {
const isUnavailable = isHourUnavailable(date, hour);
return (
<td
key={dayIndex}
className={`text-center cursor-pointer p-1
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
onClick={() => toggleHourAvailability(date, hour)}
style={{ cursor: 'pointer', height: '30px' }}
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
>
{isUnavailable && '×'}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</>
);
};
const renderDayView = () => {
const dayName = currentDate.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric'
});
const hours = Array.from({ length: 24 }, (_, i) => i);
return (
<>
<h6 className="text-center mb-3">{dayName}</h6>
<div style={{ maxHeight: '500px', overflowY: 'auto' }}>
<table className="table table-bordered">
<tbody>
{hours.map(hour => {
const isUnavailable = isHourUnavailable(currentDate, hour);
return (
<tr key={hour}>
<td className="text-center" style={{ width: '100px' }}>
{hour.toString().padStart(2, '0')}:00
</td>
<td
className={`text-center cursor-pointer p-3
${isUnavailable ? 'bg-danger text-white' : 'bg-light'}`}
onClick={() => toggleHourAvailability(currentDate, hour)}
style={{ cursor: 'pointer' }}
title={isUnavailable ? 'Click to make available' : 'Click to make unavailable'}
>
{isUnavailable ? 'Unavailable' : 'Available'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</>
);
};
const navigateDate = (direction: 'prev' | 'next') => {
const newDate = new Date(currentDate);
switch (viewType) {
case 'month':
newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));
break;
case 'week':
newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));
break;
case 'day':
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
break;
}
setCurrentDate(newDate);
};
return (
<div className="availability-calendar">
<div className="d-flex justify-content-between align-items-center mb-3">
<div className="btn-group" role="group">
<button
type="button"
className={`btn btn-sm ${viewType === 'month' ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setViewType('month')}
>
Month
</button>
{priceType === 'hour' && (
<>
<button
type="button"
className={`btn btn-sm ${viewType === 'week' ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setViewType('week')}
>
Week
</button>
<button
type="button"
className={`btn btn-sm ${viewType === 'day' ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setViewType('day')}
>
Day
</button>
</>
)}
</div>
<div>
<button
type="button"
className="btn btn-sm btn-outline-secondary me-2"
onClick={() => navigateDate('prev')}
>
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary"
onClick={() => navigateDate('next')}
>
</button>
</div>
</div>
<div className="calendar-view mb-3">
{viewType === 'month' && renderMonthView()}
{viewType === 'week' && renderWeekView()}
{viewType === 'day' && renderDayView()}
</div>
<div className="text-muted small">
<div className="mb-2">
<i className="bi bi-info-circle"></i> {isRentalMode ? 'Click start date, then click end date to select rental period' : 'Click on any date or time slot to toggle availability'}
</div>
{viewType === 'month' && (
<div className="d-flex gap-3 justify-content-center flex-wrap">
<span><span className="badge bg-light text-dark"></span> Available</span>
{isRentalMode && (
<span><span className="badge bg-success"></span> Selected</span>
)}
{!isRentalMode && (
<span><span className="badge text-white" style={{ backgroundColor: '#6f42c1' }}></span> Booked</span>
)}
<span><span className="badge text-dark" style={{ backgroundColor: '#ffeb3b' }}></span> Partially Unavailable</span>
<span><span className="badge bg-danger"></span> Fully Unavailable</span>
</div>
)}
</div>
</div>
);
};
export default AvailabilityCalendar;

View File

@@ -0,0 +1,81 @@
import React, { useState, useRef, useEffect } from 'react';
interface InfoTooltipProps {
text: string;
}
const InfoTooltip: React.FC<InfoTooltipProps> = ({ text }) => {
const [showTooltip, setShowTooltip] = useState(false);
const tooltipRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
tooltipRef.current &&
!tooltipRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setShowTooltip(false);
}
};
if (showTooltip) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [showTooltip]);
return (
<span className="position-relative">
<span
ref={buttonRef}
className="text-muted ms-1"
style={{ cursor: 'pointer' }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowTooltip(!showTooltip);
}}
>
<i className="bi bi-info-circle"></i>
</span>
{showTooltip && (
<div
ref={tooltipRef}
className="position-absolute bg-dark text-white p-2 rounded"
style={{
bottom: '100%',
left: '50%',
transform: 'translateX(-50%)',
marginBottom: '5px',
whiteSpace: 'nowrap',
fontSize: '0.875rem',
zIndex: 1000,
}}
>
{text}
<div
className="position-absolute"
style={{
top: '100%',
left: '50%',
transform: 'translateX(-50%)',
width: 0,
height: 0,
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderTop: '5px solid var(--bs-dark)',
}}
/>
</div>
)}
</span>
);
};
export default InfoTooltip;

View File

@@ -0,0 +1,110 @@
import React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const Navbar: React.FC = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/');
};
return (
<nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div className="container">
<Link className="navbar-brand fw-bold" to="/">
<i className="bi bi-box-seam me-2"></i>
Rentall
</Link>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav me-auto">
<li className="nav-item">
<Link className="nav-link" to="/items">
Browse Items
</Link>
</li>
{user && (
<li className="nav-item">
<Link className="nav-link" to="/create-item">
List an Item
</Link>
</li>
)}
</ul>
<ul className="navbar-nav">
{user ? (
<>
<li className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="#"
id="navbarDropdown"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i className="bi bi-person-circle me-1"></i>
{user.firstName}
</a>
<ul className="dropdown-menu" aria-labelledby="navbarDropdown">
<li>
<Link className="dropdown-item" to="/profile">
<i className="bi bi-person me-2"></i>Profile
</Link>
</li>
<li>
<Link className="dropdown-item" to="/my-rentals">
<i className="bi bi-calendar-check me-2"></i>My Rentals
</Link>
</li>
<li>
<Link className="dropdown-item" to="/my-listings">
<i className="bi bi-list-ul me-2"></i>My Listings
</Link>
</li>
<li>
<hr className="dropdown-divider" />
</li>
<li>
<button className="dropdown-item" onClick={handleLogout}>
<i className="bi bi-box-arrow-right me-2"></i>Logout
</button>
</li>
</ul>
</li>
</>
) : (
<>
<li className="nav-item">
<Link className="nav-link" to="/login">
Login
</Link>
</li>
<li className="nav-item">
<Link className="btn btn-primary btn-sm ms-2" to="/register">
Sign Up
</Link>
</li>
</>
)}
</ul>
</div>
</div>
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
interface PrivateRouteProps {
children: React.ReactNode;
}
const PrivateRoute: React.FC<PrivateRouteProps> = ({ children }) => {
const { user, loading } = useAuth();
if (loading) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ minHeight: '80vh' }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
return user ? <>{children}</> : <Navigate to="/login" />;
};
export default PrivateRoute;