simplified create item. Restructured profile. Simplified availability
This commit is contained in:
@@ -12,13 +12,6 @@ main {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
|
||||
165
frontend/src/components/AvailabilitySettings.tsx
Normal file
165
frontend/src/components/AvailabilitySettings.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
|
||||
interface AvailabilityData {
|
||||
generalAvailableAfter: string;
|
||||
generalAvailableBefore: string;
|
||||
specifyTimesPerDay: boolean;
|
||||
weeklyTimes: {
|
||||
sunday: { availableAfter: string; availableBefore: string };
|
||||
monday: { availableAfter: string; availableBefore: string };
|
||||
tuesday: { availableAfter: string; availableBefore: string };
|
||||
wednesday: { availableAfter: string; availableBefore: string };
|
||||
thursday: { availableAfter: string; availableBefore: string };
|
||||
friday: { availableAfter: string; availableBefore: string };
|
||||
saturday: { availableAfter: string; availableBefore: string };
|
||||
};
|
||||
}
|
||||
|
||||
interface AvailabilitySettingsProps {
|
||||
data: AvailabilityData;
|
||||
onChange: (field: string, value: string | boolean) => void;
|
||||
onWeeklyTimeChange: (day: string, field: 'availableAfter' | 'availableBefore', value: string) => void;
|
||||
showTitle?: boolean;
|
||||
}
|
||||
|
||||
const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
||||
data,
|
||||
onChange,
|
||||
onWeeklyTimeChange,
|
||||
showTitle = true
|
||||
}) => {
|
||||
const generateTimeOptions = () => {
|
||||
const options = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
const time24 = `${hour.toString().padStart(2, '0')}:00`;
|
||||
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
|
||||
const period = hour < 12 ? 'AM' : 'PM';
|
||||
const time12 = `${hour12}:00 ${period}`;
|
||||
options.push({ value: time24, label: time12 });
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
const handleGeneralChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
if (type === 'checkbox') {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
onChange(name, checked);
|
||||
} else {
|
||||
onChange(name, value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showTitle && <h5 className="card-title">Availability</h5>}
|
||||
|
||||
{/* General Times */}
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="generalAvailableAfter" className="form-label">
|
||||
Available After *
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="generalAvailableAfter"
|
||||
name="generalAvailableAfter"
|
||||
value={data.generalAvailableAfter}
|
||||
onChange={handleGeneralChange}
|
||||
disabled={data.specifyTimesPerDay}
|
||||
required
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="generalAvailableBefore" className="form-label">
|
||||
Available Before *
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="generalAvailableBefore"
|
||||
name="generalAvailableBefore"
|
||||
value={data.generalAvailableBefore}
|
||||
onChange={handleGeneralChange}
|
||||
disabled={data.specifyTimesPerDay}
|
||||
required
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkbox for day-specific times */}
|
||||
<div className="form-check mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="specifyTimesPerDay"
|
||||
name="specifyTimesPerDay"
|
||||
checked={data.specifyTimesPerDay}
|
||||
onChange={handleGeneralChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="specifyTimesPerDay">
|
||||
Specify times for each day of the week
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Weekly Times */}
|
||||
{data.specifyTimesPerDay && (
|
||||
<div className="mt-4">
|
||||
<h6>Weekly Schedule</h6>
|
||||
{Object.entries(data.weeklyTimes).map(([day, times]) => (
|
||||
<div key={day} className="row mb-2">
|
||||
<div className="col-md-2">
|
||||
<label className="form-label">
|
||||
{day.charAt(0).toUpperCase() + day.slice(1)}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={times.availableAfter}
|
||||
onChange={(e) =>
|
||||
onWeeklyTimeChange(day, 'availableAfter', e.target.value)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-5">
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
value={times.availableBefore}
|
||||
onChange={(e) =>
|
||||
onWeeklyTimeChange(day, 'availableBefore', e.target.value)
|
||||
}
|
||||
>
|
||||
{generateTimeOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvailabilitySettings;
|
||||
@@ -1,138 +1,211 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import AuthModal from './AuthModal';
|
||||
import React, { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import AuthModal from "./AuthModal";
|
||||
|
||||
const Navbar: React.FC = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||
const [authModalMode, setAuthModalMode] = useState<'login' | 'signup'>('login');
|
||||
const [authModalMode, setAuthModalMode] = useState<"login" | "signup">(
|
||||
"login"
|
||||
);
|
||||
const [searchFilters, setSearchFilters] = useState({
|
||||
search: "",
|
||||
location: "",
|
||||
});
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const openAuthModal = (mode: 'login' | 'signup') => {
|
||||
const openAuthModal = (mode: "login" | "signup") => {
|
||||
setAuthModalMode(mode);
|
||||
setShowAuthModal(true);
|
||||
};
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (searchFilters.search.trim()) {
|
||||
params.append("search", searchFilters.search.trim());
|
||||
}
|
||||
if (searchFilters.location.trim()) {
|
||||
// Check if location looks like a zip code (5 digits) or city name
|
||||
const location = searchFilters.location.trim();
|
||||
if (/^\d{5}(-\d{4})?$/.test(location)) {
|
||||
params.append("zipCode", location);
|
||||
} else {
|
||||
params.append("city", location);
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
navigate(`/items${queryString ? `?${queryString}` : ""}`);
|
||||
|
||||
// Clear search after navigating
|
||||
setSearchFilters({ search: "", location: "" });
|
||||
};
|
||||
|
||||
const handleSearchInputChange = (
|
||||
field: "search" | "location",
|
||||
value: string
|
||||
) => {
|
||||
setSearchFilters((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||
<div className="container-fluid" style={{ maxWidth: '1800px' }}>
|
||||
<Link className="navbar-brand fw-bold" to="/">
|
||||
<i className="bi bi-box-seam me-2"></i>
|
||||
CommunityRentals.App
|
||||
</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">
|
||||
<div className="d-flex align-items-center w-100">
|
||||
<div className="position-absolute start-50 translate-middle-x">
|
||||
<div className="input-group" style={{ width: '400px' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search for items to rent..."
|
||||
aria-label="Search"
|
||||
/>
|
||||
<button className="btn btn-outline-secondary" type="button">
|
||||
<i className="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ms-auto d-flex align-items-center">
|
||||
<Link className="btn btn-outline-primary btn-sm me-3 text-nowrap" to="/create-item">
|
||||
Start Earning
|
||||
</Link>
|
||||
<ul className="navbar-nav flex-row">
|
||||
{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>
|
||||
<Link className="dropdown-item" to="/my-requests">
|
||||
<i className="bi bi-clipboard-check me-2"></i>My Requests
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/messages">
|
||||
<i className="bi bi-envelope me-2"></i>Messages
|
||||
</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">
|
||||
<button
|
||||
className="btn btn-primary btn-sm text-nowrap"
|
||||
onClick={() => openAuthModal('login')}
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||
<div className="container-fluid" style={{ maxWidth: "1800px" }}>
|
||||
<Link className="navbar-brand fw-bold" to="/">
|
||||
<i className="bi bi-box-seam me-2"></i>
|
||||
CommunityRentals.App
|
||||
</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">
|
||||
<div className="d-flex align-items-center w-100">
|
||||
<div className="position-absolute start-50 translate-middle-x">
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="input-group" style={{ width: "520px" }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search items..."
|
||||
value={searchFilters.search}
|
||||
onChange={(e) =>
|
||||
handleSearchInputChange("search", e.target.value)
|
||||
}
|
||||
/>
|
||||
<span
|
||||
className="input-group-text bg-white text-muted"
|
||||
style={{
|
||||
borderLeft: "0",
|
||||
borderRight: "1px solid #dee2e6",
|
||||
}}
|
||||
>
|
||||
Login or Sign Up
|
||||
in
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="City or ZIP"
|
||||
value={searchFilters.location}
|
||||
onChange={(e) =>
|
||||
handleSearchInputChange("location", e.target.value)
|
||||
}
|
||||
style={{ borderLeft: "0" }}
|
||||
/>
|
||||
<button className="btn btn-outline-secondary" type="submit">
|
||||
<i className="bi bi-search"></i>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="ms-auto d-flex align-items-center">
|
||||
<Link
|
||||
className="btn btn-outline-primary btn-sm me-3 text-nowrap"
|
||||
to="/create-item"
|
||||
>
|
||||
Start Earning
|
||||
</Link>
|
||||
<ul className="navbar-nav flex-row">
|
||||
{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>
|
||||
Rentals
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/my-listings">
|
||||
<i className="bi bi-list-ul me-2"></i>Listings
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/my-requests">
|
||||
<i className="bi bi-clipboard-check me-2"></i>
|
||||
Requests
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="dropdown-item" to="/messages">
|
||||
<i className="bi bi-envelope me-2"></i>Messages
|
||||
</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">
|
||||
<button
|
||||
className="btn btn-primary btn-sm text-nowrap"
|
||||
onClick={() => openAuthModal("login")}
|
||||
>
|
||||
Login or Sign Up
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<AuthModal
|
||||
show={showAuthModal}
|
||||
onHide={() => setShowAuthModal(false)}
|
||||
initialMode={authModalMode}
|
||||
/>
|
||||
</>
|
||||
</nav>
|
||||
|
||||
<AuthModal
|
||||
show={showAuthModal}
|
||||
onHide={() => setShowAuthModal(false)}
|
||||
initialMode={authModalMode}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
export default Navbar;
|
||||
|
||||
@@ -2,15 +2,12 @@ import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import api from "../services/api";
|
||||
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
||||
import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
|
||||
interface ItemFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
pickUpAvailable: boolean;
|
||||
localDeliveryAvailable: boolean;
|
||||
localDeliveryRadius?: number;
|
||||
shippingAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
pricePerHour?: number;
|
||||
pricePerDay?: number;
|
||||
@@ -27,13 +24,18 @@ interface ItemFormData {
|
||||
rules?: string;
|
||||
minimumRentalDays: number;
|
||||
needsTraining: boolean;
|
||||
unavailablePeriods?: Array<{
|
||||
id: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}>;
|
||||
generalAvailableAfter: string;
|
||||
generalAvailableBefore: string;
|
||||
specifyTimesPerDay: boolean;
|
||||
weeklyTimes: {
|
||||
sunday: { availableAfter: string; availableBefore: string };
|
||||
monday: { availableAfter: string; availableBefore: string };
|
||||
tuesday: { availableAfter: string; availableBefore: string };
|
||||
wednesday: { availableAfter: string; availableBefore: string };
|
||||
thursday: { availableAfter: string; availableBefore: string };
|
||||
friday: { availableAfter: string; availableBefore: string };
|
||||
saturday: { availableAfter: string; availableBefore: string };
|
||||
};
|
||||
}
|
||||
|
||||
const CreateItem: React.FC = () => {
|
||||
@@ -45,9 +47,6 @@ const CreateItem: React.FC = () => {
|
||||
name: "",
|
||||
description: "",
|
||||
pickUpAvailable: false,
|
||||
localDeliveryAvailable: false,
|
||||
localDeliveryRadius: 25,
|
||||
shippingAvailable: false,
|
||||
inPlaceUseAvailable: false,
|
||||
pricePerDay: undefined,
|
||||
replacementCost: 0,
|
||||
@@ -57,10 +56,21 @@ const CreateItem: React.FC = () => {
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "",
|
||||
country: "US",
|
||||
minimumRentalDays: 1,
|
||||
needsTraining: false,
|
||||
unavailablePeriods: [],
|
||||
generalAvailableAfter: "09:00",
|
||||
generalAvailableBefore: "17:00",
|
||||
specifyTimesPerDay: false,
|
||||
weeklyTimes: {
|
||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
},
|
||||
});
|
||||
const [imageFiles, setImageFiles] = useState<File[]>([]);
|
||||
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
|
||||
@@ -126,6 +136,24 @@ const CreateItem: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleWeeklyTimeChange = (
|
||||
day: string,
|
||||
field: "availableAfter" | "availableBefore",
|
||||
value: string
|
||||
) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
weeklyTimes: {
|
||||
...prev.weeklyTimes,
|
||||
[day]: {
|
||||
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
|
||||
@@ -170,7 +198,12 @@ const CreateItem: React.FC = () => {
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Upload Images (Max 5)</label>
|
||||
<label className="form-label mb-0">
|
||||
Upload Images (Max 5)
|
||||
</label>
|
||||
<div className="form-text mb-2">
|
||||
Have pictures of everything that's included
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
className="form-control"
|
||||
@@ -179,9 +212,6 @@ const CreateItem: React.FC = () => {
|
||||
multiple
|
||||
disabled={imageFiles.length >= 5}
|
||||
/>
|
||||
<div className="form-text">
|
||||
Upload up to 5 images of your item
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{imagePreviews.length > 0 && (
|
||||
@@ -252,6 +282,13 @@ const CreateItem: React.FC = () => {
|
||||
{/* Location Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="mb-3">
|
||||
<small className="text-muted">
|
||||
<i className="bi bi-info-circle me-2"></i>
|
||||
Your address is private. This will only be used to show
|
||||
renters a general area.
|
||||
</small>
|
||||
</div>
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="address1" className="form-label">
|
||||
@@ -365,70 +402,12 @@ const CreateItem: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||
Pick-Up
|
||||
Pick-Up/Drop-off
|
||||
<div className="small text-muted">
|
||||
They pick-up the item from your location and they return
|
||||
the item to your location
|
||||
You and the renter coordinate pick-up and drop-off
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="localDeliveryAvailable"
|
||||
name="localDeliveryAvailable"
|
||||
checked={formData.localDeliveryAvailable}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label d-flex align-items-center"
|
||||
htmlFor="localDeliveryAvailable"
|
||||
>
|
||||
<div>
|
||||
Local Delivery
|
||||
{formData.localDeliveryAvailable && (
|
||||
<span className="ms-2">
|
||||
(Delivery Radius:
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm d-inline-block mx-1"
|
||||
id="localDeliveryRadius"
|
||||
name="localDeliveryRadius"
|
||||
value={formData.localDeliveryRadius || ""}
|
||||
onChange={handleChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="25"
|
||||
min="1"
|
||||
max="100"
|
||||
style={{ width: "60px" }}
|
||||
/>
|
||||
miles)
|
||||
</span>
|
||||
)}
|
||||
<div className="small text-muted">
|
||||
You deliver and then pick-up the item when the rental
|
||||
period ends
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="shippingAvailable"
|
||||
name="shippingAvailable"
|
||||
checked={formData.shippingAvailable}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="shippingAvailable"
|
||||
>
|
||||
Shipping
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -451,6 +430,24 @@ const CreateItem: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Availability Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<AvailabilitySettings
|
||||
data={{
|
||||
generalAvailableAfter: formData.generalAvailableAfter,
|
||||
generalAvailableBefore: formData.generalAvailableBefore,
|
||||
specifyTimesPerDay: formData.specifyTimesPerDay,
|
||||
weeklyTimes: formData.weeklyTimes
|
||||
}}
|
||||
onChange={(field, value) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
}}
|
||||
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
@@ -518,9 +515,12 @@ const CreateItem: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="replacementCost" className="form-label">
|
||||
<label htmlFor="replacementCost" className="form-label mb-0">
|
||||
Replacement Cost *
|
||||
</label>
|
||||
<div className="form-text mb-2">
|
||||
The cost to replace the item if lost
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
@@ -535,32 +535,10 @@ const CreateItem: React.FC = () => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
The cost to replace the item if damaged or lost
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Availability Schedule Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">
|
||||
Select dates when the item is NOT available for rent
|
||||
</p>
|
||||
<AvailabilityCalendar
|
||||
unavailablePeriods={formData.unavailablePeriods || []}
|
||||
onPeriodsChange={(periods) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
unavailablePeriods: periods,
|
||||
}))
|
||||
}
|
||||
mode="owner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rules & Guidelines Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
@@ -578,7 +556,7 @@ const CreateItem: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
<label htmlFor="rules" className="form-label">
|
||||
Additional Rules or Guidelines
|
||||
Additional Rules
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
@@ -587,12 +565,12 @@ const CreateItem: React.FC = () => {
|
||||
rows={3}
|
||||
value={formData.rules || ""}
|
||||
onChange={handleChange}
|
||||
placeholder="Any specific rules or guidelines for renting this item"
|
||||
placeholder="Any specific rules for renting this item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="d-grid gap-2">
|
||||
<div className="d-grid gap-2 mb-5">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
|
||||
@@ -164,7 +164,6 @@ const EditItem: React.FC = () => {
|
||||
await itemAPI.updateItem(id!, {
|
||||
...formData,
|
||||
images: imageUrls,
|
||||
isPortable: formData.pickUpAvailable || formData.shippingAvailable,
|
||||
});
|
||||
|
||||
setSuccess(true);
|
||||
@@ -257,7 +256,7 @@ const EditItem: React.FC = () => {
|
||||
disabled={imagePreviews.length >= 5}
|
||||
/>
|
||||
<div className="form-text">
|
||||
Upload up to 5 images of your item
|
||||
Have pictures of everything that's included
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -368,10 +367,9 @@ const EditItem: React.FC = () => {
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||
Pick-Up
|
||||
Pick-Up/Drop-off
|
||||
<div className="small text-muted">
|
||||
They pick-up the item from your location and they return
|
||||
the item to your location
|
||||
You and the renter coordinate pick-up and drop-off
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -539,7 +537,7 @@ const EditItem: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
The cost to replace the item if damaged or lost
|
||||
The cost to replace the item if lost
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -608,7 +606,7 @@ const EditItem: React.FC = () => {
|
||||
</label>
|
||||
</div>
|
||||
<label htmlFor="rules" className="form-label">
|
||||
Additional Rules or Guidelines
|
||||
Additional Rules
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
@@ -617,7 +615,7 @@ const EditItem: React.FC = () => {
|
||||
rows={3}
|
||||
value={formData.rules || ""}
|
||||
onChange={handleChange}
|
||||
placeholder="Any specific rules or guidelines for renting this item"
|
||||
placeholder="Any specific rules for renting this item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,46 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Item } from '../types';
|
||||
import { itemAPI } from '../services/api';
|
||||
|
||||
const ItemList: React.FC = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filters, setFilters] = useState({
|
||||
search: searchParams.get('search') || '',
|
||||
city: searchParams.get('city') || '',
|
||||
zipCode: searchParams.get('zipCode') || ''
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, []);
|
||||
}, [filters]);
|
||||
|
||||
// Update filters when URL params change (e.g., from navbar search)
|
||||
useEffect(() => {
|
||||
setFilters({
|
||||
search: searchParams.get('search') || '',
|
||||
city: searchParams.get('city') || '',
|
||||
zipCode: searchParams.get('zipCode') || ''
|
||||
});
|
||||
}, [searchParams]);
|
||||
|
||||
const fetchItems = async () => {
|
||||
try {
|
||||
const response = await itemAPI.getItems();
|
||||
setLoading(true);
|
||||
const params = {
|
||||
...filters
|
||||
};
|
||||
// Remove empty filters
|
||||
Object.keys(params).forEach(key => {
|
||||
if (!params[key as keyof typeof params]) {
|
||||
delete params[key as keyof typeof params];
|
||||
}
|
||||
});
|
||||
|
||||
const response = await itemAPI.getItems(params);
|
||||
console.log('API Response:', response);
|
||||
// Access the items array from response.data.items
|
||||
const allItems = response.data.items || response.data || [];
|
||||
@@ -32,14 +57,6 @@ const ItemList: React.FC = () => {
|
||||
};
|
||||
|
||||
|
||||
// Filter items based on search term
|
||||
const filteredItems = items.filter(item => {
|
||||
const matchesSearch = searchTerm === '' ||
|
||||
item.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
item.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
return matchesSearch;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -67,26 +84,15 @@ const ItemList: React.FC = () => {
|
||||
<div className="container mt-4">
|
||||
<h1>Browse Items</h1>
|
||||
|
||||
<div className="row mb-4">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Search items..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<span className="text-muted">{filteredItems.length} items found</span>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<span className="text-muted">{items.length} items found</span>
|
||||
</div>
|
||||
|
||||
{filteredItems.length === 0 ? (
|
||||
{items.length === 0 ? (
|
||||
<p className="text-center text-muted">No items available for rent.</p>
|
||||
) : (
|
||||
<div className="row">
|
||||
{filteredItems.map((item) => (
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<Link
|
||||
to={`/items/${item.id}`}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import api from '../services/api';
|
||||
import { Item, Rental } from '../types';
|
||||
import { rentalAPI } from '../services/api';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import api from "../services/api";
|
||||
import { Item, Rental } from "../types";
|
||||
import { rentalAPI } from "../services/api";
|
||||
|
||||
const MyListings: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [listings, setListings] = useState<Item[]>([]);
|
||||
const [rentals, setRentals] = useState<Rental[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
fetchMyListings();
|
||||
@@ -22,17 +22,19 @@ const MyListings: React.FC = () => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(''); // Clear any previous errors
|
||||
const response = await api.get('/items');
|
||||
|
||||
setError(""); // Clear any previous errors
|
||||
const response = await api.get("/items");
|
||||
|
||||
// Filter items to only show ones owned by current user
|
||||
const myItems = response.data.items.filter((item: Item) => item.ownerId === user.id);
|
||||
const myItems = response.data.items.filter(
|
||||
(item: Item) => item.ownerId === user.id
|
||||
);
|
||||
setListings(myItems);
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching listings:', err);
|
||||
console.error("Error fetching listings:", err);
|
||||
// Only show error for actual API failures
|
||||
if (err.response && err.response.status >= 500) {
|
||||
setError('Failed to get your listings. Please try again later.');
|
||||
setError("Failed to get your listings. Please try again later.");
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -40,13 +42,14 @@ const MyListings: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleDelete = async (itemId: string) => {
|
||||
if (!window.confirm('Are you sure you want to delete this listing?')) return;
|
||||
if (!window.confirm("Are you sure you want to delete this listing?"))
|
||||
return;
|
||||
|
||||
try {
|
||||
await api.delete(`/items/${itemId}`);
|
||||
setListings(listings.filter(item => item.id !== itemId));
|
||||
setListings(listings.filter((item) => item.id !== itemId));
|
||||
} catch (err: any) {
|
||||
alert('Failed to delete listing');
|
||||
alert("Failed to delete listing");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,13 +57,15 @@ const MyListings: React.FC = () => {
|
||||
try {
|
||||
await api.put(`/items/${item.id}`, {
|
||||
...item,
|
||||
availability: !item.availability
|
||||
availability: !item.availability,
|
||||
});
|
||||
setListings(listings.map(i =>
|
||||
i.id === item.id ? { ...i, availability: !i.availability } : i
|
||||
));
|
||||
setListings(
|
||||
listings.map((i) =>
|
||||
i.id === item.id ? { ...i, availability: !i.availability } : i
|
||||
)
|
||||
);
|
||||
} catch (err: any) {
|
||||
alert('Failed to update availability');
|
||||
alert("Failed to update availability");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,30 +76,30 @@ const MyListings: React.FC = () => {
|
||||
const response = await rentalAPI.getMyListings();
|
||||
setRentals(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching rental requests:', err);
|
||||
console.error("Error fetching rental requests:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAcceptRental = async (rentalId: string) => {
|
||||
try {
|
||||
await rentalAPI.updateRentalStatus(rentalId, 'confirmed');
|
||||
await rentalAPI.updateRentalStatus(rentalId, "confirmed");
|
||||
// Refresh the rentals list
|
||||
fetchRentalRequests();
|
||||
} catch (err) {
|
||||
console.error('Failed to accept rental request:', err);
|
||||
console.error("Failed to accept rental request:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectRental = async (rentalId: string) => {
|
||||
try {
|
||||
await api.put(`/rentals/${rentalId}/status`, {
|
||||
status: 'cancelled',
|
||||
rejectionReason: 'Request declined by owner'
|
||||
await api.put(`/rentals/${rentalId}/status`, {
|
||||
status: "cancelled",
|
||||
rejectionReason: "Request declined by owner",
|
||||
});
|
||||
// Refresh the rentals list
|
||||
fetchRentalRequests();
|
||||
} catch (err) {
|
||||
console.error('Failed to reject rental request:', err);
|
||||
console.error("Failed to reject rental request:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,13 +131,19 @@ const MyListings: React.FC = () => {
|
||||
)}
|
||||
|
||||
{(() => {
|
||||
const pendingCount = rentals.filter(r => r.status === 'pending').length;
|
||||
|
||||
const pendingCount = rentals.filter(
|
||||
(r) => r.status === "pending"
|
||||
).length;
|
||||
|
||||
if (pendingCount > 0) {
|
||||
return (
|
||||
<div className="alert alert-info d-flex align-items-center" role="alert">
|
||||
<div
|
||||
className="alert alert-info d-flex align-items-center"
|
||||
role="alert"
|
||||
>
|
||||
<i className="bi bi-bell-fill me-2"></i>
|
||||
You have {pendingCount} pending rental request{pendingCount > 1 ? 's' : ''} to review.
|
||||
You have {pendingCount} pending rental request
|
||||
{pendingCount > 1 ? "s" : ""} to review.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -150,137 +161,197 @@ const MyListings: React.FC = () => {
|
||||
<div className="row">
|
||||
{listings.map((item) => (
|
||||
<div key={item.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<Link
|
||||
to={`/items/${item.id}/edit`}
|
||||
<Link
|
||||
to={`/items/${item.id}/edit`}
|
||||
className="text-decoration-none"
|
||||
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button') || target.closest('.rental-requests')) {
|
||||
if (
|
||||
target.closest("button") ||
|
||||
target.closest(".rental-requests")
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="card h-100" style={{ cursor: 'pointer' }}>
|
||||
<div className="card h-100" style={{ cursor: "pointer" }}>
|
||||
{item.images && item.images[0] && (
|
||||
<img
|
||||
src={item.images[0]}
|
||||
className="card-img-top"
|
||||
alt={item.name}
|
||||
style={{ height: '200px', objectFit: 'cover' }}
|
||||
style={{ height: "200px", objectFit: "cover" }}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-dark">
|
||||
{item.name}
|
||||
</h5>
|
||||
<p className="card-text text-truncate text-dark">{item.description}</p>
|
||||
|
||||
<div className="mb-2">
|
||||
<span className={`badge ${item.availability ? 'bg-success' : 'bg-secondary'}`}>
|
||||
{item.availability ? 'Available' : 'Not Available'}
|
||||
</span>
|
||||
</div>
|
||||
<h5 className="card-title text-dark">{item.name}</h5>
|
||||
<p className="card-text text-truncate text-dark">
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
<div className="mb-3">
|
||||
{item.pricePerDay && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerDay}/day
|
||||
</div>
|
||||
)}
|
||||
{item.pricePerHour && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerHour}/hour
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<span
|
||||
className={`badge ${
|
||||
item.availability ? "bg-success" : "bg-secondary"
|
||||
}`}
|
||||
>
|
||||
{item.availability ? "Available" : "Not Available"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
onClick={() => toggleAvailability(item)}
|
||||
className="btn btn-sm btn-outline-info"
|
||||
>
|
||||
{item.availability ? 'Mark Unavailable' : 'Mark Available'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const pendingRentals = rentals.filter(r =>
|
||||
r.itemId === item.id && r.status === 'pending'
|
||||
);
|
||||
const acceptedRentals = rentals.filter(r =>
|
||||
r.itemId === item.id && ['confirmed', 'active'].includes(r.status)
|
||||
);
|
||||
|
||||
if (pendingRentals.length > 0 || acceptedRentals.length > 0) {
|
||||
return (
|
||||
<div className="mt-3 border-top pt-3 rental-requests">
|
||||
{pendingRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-primary mb-2">
|
||||
<i className="bi bi-bell-fill"></i> Pending Requests ({pendingRentals.length})
|
||||
</h6>
|
||||
{pendingRentals.map(rental => (
|
||||
<div key={rental.id} className="border rounded p-2 mb-2 bg-light">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div className="small">
|
||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong><br/>
|
||||
{new Date(rental.startDate).toLocaleDateString()} - {new Date(rental.endDate).toLocaleDateString()}<br/>
|
||||
<span className="text-muted">${rental.totalAmount}</span>
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
onClick={() => handleAcceptRental(rental.id)}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() => handleRejectRental(rental.id)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{acceptedRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-success mb-2 mt-3">
|
||||
<i className="bi bi-check-circle-fill"></i> Accepted Rentals ({acceptedRentals.length})
|
||||
</h6>
|
||||
{acceptedRentals.map(rental => (
|
||||
<div key={rental.id} className="border rounded p-2 mb-2 bg-light">
|
||||
<div className="small">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong><br/>
|
||||
{new Date(rental.startDate).toLocaleDateString()} - {new Date(rental.endDate).toLocaleDateString()}<br/>
|
||||
<span className="text-muted">${rental.totalAmount}</span>
|
||||
</div>
|
||||
<span className={`badge ${rental.status === 'active' ? 'bg-success' : 'bg-info'}`}>
|
||||
{rental.status === 'active' ? 'Active' : 'Confirmed'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
{item.pricePerDay && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerDay}/day
|
||||
</div>
|
||||
)}
|
||||
{item.pricePerHour && (
|
||||
<div className="text-muted small">
|
||||
${item.pricePerHour}/hour
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
onClick={() => toggleAvailability(item)}
|
||||
className="btn btn-sm btn-outline-info"
|
||||
>
|
||||
{item.availability
|
||||
? "Mark Unavailable"
|
||||
: "Mark Available"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(item.id)}
|
||||
className="btn btn-sm btn-outline-danger"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const pendingRentals = rentals.filter(
|
||||
(r) => r.itemId === item.id && r.status === "pending"
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
const acceptedRentals = rentals.filter(
|
||||
(r) =>
|
||||
r.itemId === item.id &&
|
||||
["confirmed", "active"].includes(r.status)
|
||||
);
|
||||
|
||||
if (
|
||||
pendingRentals.length > 0 ||
|
||||
acceptedRentals.length > 0
|
||||
) {
|
||||
return (
|
||||
<div className="mt-3 border-top pt-3 rental-requests">
|
||||
{pendingRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-primary mb-2">
|
||||
<i className="bi bi-bell-fill"></i> Pending
|
||||
Requests ({pendingRentals.length})
|
||||
</h6>
|
||||
{pendingRentals.map((rental) => (
|
||||
<div
|
||||
key={rental.id}
|
||||
className="border rounded p-2 mb-2 bg-light"
|
||||
>
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div className="small">
|
||||
<strong>
|
||||
{rental.renter?.firstName}{" "}
|
||||
{rental.renter?.lastName}
|
||||
</strong>
|
||||
<br />
|
||||
{new Date(
|
||||
rental.startDate
|
||||
).toLocaleDateString()}{" "}
|
||||
-{" "}
|
||||
{new Date(
|
||||
rental.endDate
|
||||
).toLocaleDateString()}
|
||||
<br />
|
||||
<span className="text-muted">
|
||||
${rental.totalAmount}
|
||||
</span>
|
||||
</div>
|
||||
<div className="d-flex gap-1">
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
onClick={() =>
|
||||
handleAcceptRental(rental.id)
|
||||
}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger btn-sm"
|
||||
onClick={() =>
|
||||
handleRejectRental(rental.id)
|
||||
}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{acceptedRentals.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-success mb-2 mt-3">
|
||||
<i className="bi bi-check-circle-fill"></i>{" "}
|
||||
Accepted Rentals ({acceptedRentals.length})
|
||||
</h6>
|
||||
{acceptedRentals.map((rental) => (
|
||||
<div
|
||||
key={rental.id}
|
||||
className="border rounded p-2 mb-2 bg-light"
|
||||
>
|
||||
<div className="small">
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>
|
||||
{rental.renter?.firstName}{" "}
|
||||
{rental.renter?.lastName}
|
||||
</strong>
|
||||
<br />
|
||||
{new Date(
|
||||
rental.startDate
|
||||
).toLocaleDateString()}{" "}
|
||||
-{" "}
|
||||
{new Date(
|
||||
rental.endDate
|
||||
).toLocaleDateString()}
|
||||
<br />
|
||||
<span className="text-muted">
|
||||
${rental.totalAmount}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`badge ${
|
||||
rental.status === "active"
|
||||
? "bg-success"
|
||||
: "bg-info"
|
||||
}`}
|
||||
>
|
||||
{rental.status === "active"
|
||||
? "Active"
|
||||
: "Confirmed"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -292,4 +363,4 @@ const MyListings: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MyListings;
|
||||
export default MyListings;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { rentalAPI } from '../services/api';
|
||||
import { Rental } from '../types';
|
||||
import ReviewModal from '../components/ReviewModal';
|
||||
import ConfirmationModal from '../components/ConfirmationModal';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { rentalAPI } from "../services/api";
|
||||
import { Rental } from "../types";
|
||||
import ReviewModal from "../components/ReviewModal";
|
||||
import ConfirmationModal from "../components/ConfirmationModal";
|
||||
|
||||
const MyRentals: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [rentals, setRentals] = useState<Rental[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'active' | 'past'>('active');
|
||||
const [activeTab, setActiveTab] = useState<"active" | "past">("active");
|
||||
const [showReviewModal, setShowReviewModal] = useState(false);
|
||||
const [selectedRental, setSelectedRental] = useState<Rental | null>(null);
|
||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||
@@ -27,7 +27,7 @@ const MyRentals: React.FC = () => {
|
||||
const response = await rentalAPI.getMyRentals();
|
||||
setRentals(response.data);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to fetch rentals');
|
||||
setError(err.response?.data?.message || "Failed to fetch rentals");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -40,15 +40,15 @@ const MyRentals: React.FC = () => {
|
||||
|
||||
const confirmCancelRental = async () => {
|
||||
if (!rentalToCancel) return;
|
||||
|
||||
|
||||
setCancelling(true);
|
||||
try {
|
||||
await rentalAPI.updateRentalStatus(rentalToCancel, 'cancelled');
|
||||
await rentalAPI.updateRentalStatus(rentalToCancel, "cancelled");
|
||||
fetchRentals(); // Refresh the list
|
||||
setShowCancelModal(false);
|
||||
setRentalToCancel(null);
|
||||
} catch (err: any) {
|
||||
alert('Failed to cancel rental');
|
||||
alert("Failed to cancel rental");
|
||||
} finally {
|
||||
setCancelling(false);
|
||||
}
|
||||
@@ -61,18 +61,18 @@ const MyRentals: React.FC = () => {
|
||||
|
||||
const handleReviewSuccess = () => {
|
||||
fetchRentals(); // Refresh to show the review has been added
|
||||
alert('Thank you for your review!');
|
||||
alert("Thank you for your review!");
|
||||
};
|
||||
|
||||
// Filter rentals based on status
|
||||
const activeRentals = rentals.filter(r =>
|
||||
['pending', 'confirmed', 'active'].includes(r.status)
|
||||
const activeRentals = rentals.filter((r) =>
|
||||
["pending", "confirmed", "active"].includes(r.status)
|
||||
);
|
||||
const pastRentals = rentals.filter(r =>
|
||||
['completed', 'cancelled'].includes(r.status)
|
||||
const pastRentals = rentals.filter((r) =>
|
||||
["completed", "cancelled"].includes(r.status)
|
||||
);
|
||||
|
||||
const displayedRentals = activeTab === 'active' ? activeRentals : pastRentals;
|
||||
const displayedRentals = activeTab === "active" ? activeRentals : pastRentals;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -99,20 +99,20 @@ const MyRentals: React.FC = () => {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h1>My Rentals</h1>
|
||||
|
||||
|
||||
<ul className="nav nav-tabs mb-4">
|
||||
<li className="nav-item">
|
||||
<button
|
||||
className={`nav-link ${activeTab === 'active' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('active')}
|
||||
className={`nav-link ${activeTab === "active" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("active")}
|
||||
>
|
||||
Active Rentals ({activeRentals.length})
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<button
|
||||
className={`nav-link ${activeTab === 'past' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('past')}
|
||||
className={`nav-link ${activeTab === "past" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("past")}
|
||||
>
|
||||
Past Rentals ({pastRentals.length})
|
||||
</button>
|
||||
@@ -122,8 +122,8 @@ const MyRentals: React.FC = () => {
|
||||
{displayedRentals.length === 0 ? (
|
||||
<div className="text-center py-5">
|
||||
<p className="text-muted">
|
||||
{activeTab === 'active'
|
||||
? "You don't have any active rentals."
|
||||
{activeTab === "active"
|
||||
? "You don't have any active rentals."
|
||||
: "You don't have any past rentals."}
|
||||
</p>
|
||||
<Link to="/items" className="btn btn-primary mt-3">
|
||||
@@ -134,98 +134,116 @@ const MyRentals: React.FC = () => {
|
||||
<div className="row">
|
||||
{displayedRentals.map((rental) => (
|
||||
<div key={rental.id} className="col-md-6 col-lg-4 mb-4">
|
||||
<Link
|
||||
to={rental.item ? `/items/${rental.item.id}` : '#'}
|
||||
<Link
|
||||
to={rental.item ? `/items/${rental.item.id}` : "#"}
|
||||
className="text-decoration-none"
|
||||
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!rental.item || target.closest('button')) {
|
||||
if (!rental.item || target.closest("button")) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="card h-100" style={{ cursor: rental.item ? 'pointer' : 'default' }}>
|
||||
<div
|
||||
className="card h-100"
|
||||
style={{ cursor: rental.item ? "pointer" : "default" }}
|
||||
>
|
||||
{rental.item?.images && rental.item.images[0] && (
|
||||
<img
|
||||
src={rental.item.images[0]}
|
||||
className="card-img-top"
|
||||
alt={rental.item.name}
|
||||
style={{ height: '200px', objectFit: 'cover' }}
|
||||
style={{ height: "200px", objectFit: "cover" }}
|
||||
/>
|
||||
)}
|
||||
<div className="card-body">
|
||||
<h5 className="card-title text-dark">
|
||||
{rental.item ? rental.item.name : 'Item Unavailable'}
|
||||
{rental.item ? rental.item.name : "Item Unavailable"}
|
||||
</h5>
|
||||
|
||||
<div className="mb-2">
|
||||
<span className={`badge ${
|
||||
rental.status === 'active' ? 'bg-success' :
|
||||
rental.status === 'pending' ? 'bg-warning' :
|
||||
rental.status === 'confirmed' ? 'bg-info' :
|
||||
rental.status === 'completed' ? 'bg-secondary' :
|
||||
'bg-danger'
|
||||
}`}>
|
||||
{rental.status.charAt(0).toUpperCase() + rental.status.slice(1)}
|
||||
</span>
|
||||
{rental.paymentStatus === 'paid' && (
|
||||
<span className="badge bg-success ms-2">Paid</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Rental Period:</strong><br />
|
||||
{new Date(rental.startDate).toLocaleDateString()} - {new Date(rental.endDate).toLocaleDateString()}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Total:</strong> ${rental.totalAmount}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Delivery:</strong> {rental.deliveryMethod === 'pickup' ? 'Pick-up' : 'Delivery'}
|
||||
</p>
|
||||
|
||||
{rental.owner && (
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Owner:</strong> {rental.owner.firstName} {rental.owner.lastName}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{rental.status === 'cancelled' && rental.rejectionReason && (
|
||||
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
||||
<strong>Rejection reason:</strong> {rental.rejectionReason}
|
||||
<div className="mb-2">
|
||||
<span
|
||||
className={`badge ${
|
||||
rental.status === "active"
|
||||
? "bg-success"
|
||||
: rental.status === "pending"
|
||||
? "bg-warning"
|
||||
: rental.status === "confirmed"
|
||||
? "bg-info"
|
||||
: rental.status === "completed"
|
||||
? "bg-secondary"
|
||||
: "bg-danger"
|
||||
}`}
|
||||
>
|
||||
{rental.status.charAt(0).toUpperCase() +
|
||||
rental.status.slice(1)}
|
||||
</span>
|
||||
{rental.paymentStatus === "paid" && (
|
||||
<span className="badge bg-success ms-2">Paid</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex gap-2 mt-3">
|
||||
{rental.status === 'pending' && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCancelClick(rental.id)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{rental.status === 'completed' && !rental.rating && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => handleReviewClick(rental)}
|
||||
>
|
||||
Leave Review
|
||||
</button>
|
||||
)}
|
||||
{rental.status === 'completed' && rental.rating && (
|
||||
<div className="text-success small">
|
||||
<i className="bi bi-check-circle-fill me-1"></i>
|
||||
Reviewed ({rental.rating}/5)
|
||||
</div>
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Rental Period:</strong>
|
||||
<br />
|
||||
{new Date(rental.startDate).toLocaleDateString()} -{" "}
|
||||
{new Date(rental.endDate).toLocaleDateString()}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Total:</strong> ${rental.totalAmount}
|
||||
</p>
|
||||
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Delivery:</strong>{" "}
|
||||
{rental.deliveryMethod === "pickup"
|
||||
? "Pick-up"
|
||||
: "Delivery"}
|
||||
</p>
|
||||
|
||||
{rental.owner && (
|
||||
<p className="mb-1 text-dark">
|
||||
<strong>Owner:</strong> {rental.owner.firstName}{" "}
|
||||
{rental.owner.lastName}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{rental.status === "cancelled" &&
|
||||
rental.rejectionReason && (
|
||||
<div className="alert alert-warning mt-2 mb-1 p-2 small">
|
||||
<strong>Rejection reason:</strong>{" "}
|
||||
{rental.rejectionReason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex gap-2 mt-3">
|
||||
{rental.status === "pending" && (
|
||||
<button
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => handleCancelClick(rental.id)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
{rental.status === "completed" && !rental.rating && (
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={() => handleReviewClick(rental)}
|
||||
>
|
||||
Leave Review
|
||||
</button>
|
||||
)}
|
||||
{rental.status === "completed" && rental.rating && (
|
||||
<div className="text-success small">
|
||||
<i className="bi bi-check-circle-fill me-1"></i>
|
||||
Reviewed ({rental.rating}/5)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -260,4 +278,4 @@ const MyRentals: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MyRentals;
|
||||
export default MyRentals;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { userAPI, itemAPI, rentalAPI } from '../services/api';
|
||||
import { User, Item, Rental } from '../types';
|
||||
import { getImageUrl } from '../utils/imageUrl';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { userAPI, itemAPI, rentalAPI } from "../services/api";
|
||||
import { User, Item, Rental } from "../types";
|
||||
import { getImageUrl } from "../utils/imageUrl";
|
||||
import AvailabilitySettings from "../components/AvailabilitySettings";
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const { user, updateUser, logout } = useAuth();
|
||||
@@ -10,26 +11,41 @@ const Profile: React.FC = () => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [activeSection, setActiveSection] = useState<string>('overview');
|
||||
const [profileData, setProfileData] = useState<User | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
profileImage: ''
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "",
|
||||
profileImage: "",
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null);
|
||||
const [stats, setStats] = useState({
|
||||
itemsListed: 0,
|
||||
acceptedRentals: 0,
|
||||
totalRentals: 0
|
||||
totalRentals: 0,
|
||||
});
|
||||
const [availabilityData, setAvailabilityData] = useState({
|
||||
generalAvailableAfter: "09:00",
|
||||
generalAvailableBefore: "17:00",
|
||||
specifyTimesPerDay: false,
|
||||
weeklyTimes: {
|
||||
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
monday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
friday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
saturday: { availableAfter: "09:00", availableBefore: "17:00" },
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -42,23 +58,23 @@ const Profile: React.FC = () => {
|
||||
const response = await userAPI.getProfile();
|
||||
setProfileData(response.data);
|
||||
setFormData({
|
||||
firstName: response.data.firstName || '',
|
||||
lastName: response.data.lastName || '',
|
||||
email: response.data.email || '',
|
||||
phone: response.data.phone || '',
|
||||
address1: response.data.address1 || '',
|
||||
address2: response.data.address2 || '',
|
||||
city: response.data.city || '',
|
||||
state: response.data.state || '',
|
||||
zipCode: response.data.zipCode || '',
|
||||
country: response.data.country || '',
|
||||
profileImage: response.data.profileImage || ''
|
||||
firstName: response.data.firstName || "",
|
||||
lastName: response.data.lastName || "",
|
||||
email: response.data.email || "",
|
||||
phone: response.data.phone || "",
|
||||
address1: response.data.address1 || "",
|
||||
address2: response.data.address2 || "",
|
||||
city: response.data.city || "",
|
||||
state: response.data.state || "",
|
||||
zipCode: response.data.zipCode || "",
|
||||
country: response.data.country || "",
|
||||
profileImage: response.data.profileImage || "",
|
||||
});
|
||||
if (response.data.profileImage) {
|
||||
setImagePreview(getImageUrl(response.data.profileImage));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to fetch profile');
|
||||
setError(err.response?.data?.message || "Failed to fetch profile");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -69,64 +85,71 @@ const Profile: React.FC = () => {
|
||||
// Fetch user's items
|
||||
const itemsResponse = await itemAPI.getItems();
|
||||
const allItems = itemsResponse.data.items || itemsResponse.data || [];
|
||||
const myItems = allItems.filter((item: Item) => item.ownerId === user?.id);
|
||||
|
||||
const myItems = allItems.filter(
|
||||
(item: Item) => item.ownerId === user?.id
|
||||
);
|
||||
|
||||
// Fetch rentals where user is the owner (rentals on user's items)
|
||||
const ownerRentalsResponse = await rentalAPI.getMyListings();
|
||||
const ownerRentals: Rental[] = ownerRentalsResponse.data;
|
||||
|
||||
const acceptedRentals = ownerRentals.filter(r => ['confirmed', 'active'].includes(r.status));
|
||||
|
||||
|
||||
const acceptedRentals = ownerRentals.filter((r) =>
|
||||
["confirmed", "active"].includes(r.status)
|
||||
);
|
||||
|
||||
setStats({
|
||||
itemsListed: myItems.length,
|
||||
acceptedRentals: acceptedRentals.length,
|
||||
totalRentals: ownerRentals.length
|
||||
totalRentals: ownerRentals.length,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch stats:', err);
|
||||
console.error("Failed to fetch stats:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
|
||||
|
||||
// Show preview
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
|
||||
// Upload image immediately
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('profileImage', file);
|
||||
|
||||
formData.append("profileImage", file);
|
||||
|
||||
const response = await userAPI.uploadProfileImage(formData);
|
||||
|
||||
|
||||
// Update the profileImage in formData with the new filename
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
profileImage: response.data.filename
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
profileImage: response.data.filename,
|
||||
}));
|
||||
|
||||
|
||||
// Update preview to use the uploaded image URL
|
||||
setImagePreview(getImageUrl(response.data.imageUrl));
|
||||
} catch (err: any) {
|
||||
console.error('Image upload error:', err);
|
||||
setError(err.response?.data?.error || 'Failed to upload image');
|
||||
console.error("Image upload error:", err);
|
||||
setError(err.response?.data?.error || "Failed to upload image");
|
||||
// Reset on error
|
||||
setImageFile(null);
|
||||
setImagePreview(profileData?.profileImage ?
|
||||
getImageUrl(profileData.profileImage) :
|
||||
null
|
||||
setImagePreview(
|
||||
profileData?.profileImage
|
||||
? getImageUrl(profileData.profileImage)
|
||||
: null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -146,12 +169,17 @@ const Profile: React.FC = () => {
|
||||
updateUser(response.data); // Update the auth context
|
||||
setEditing(false);
|
||||
} catch (err: any) {
|
||||
console.error('Profile update error:', err.response?.data);
|
||||
const errorMessage = err.response?.data?.error || err.response?.data?.message || 'Failed to update profile';
|
||||
console.error("Profile update error:", err.response?.data);
|
||||
const errorMessage =
|
||||
err.response?.data?.error ||
|
||||
err.response?.data?.message ||
|
||||
"Failed to update profile";
|
||||
const errorDetails = err.response?.data?.details;
|
||||
|
||||
|
||||
if (errorDetails && Array.isArray(errorDetails)) {
|
||||
const detailMessages = errorDetails.map((d: any) => `${d.field}: ${d.message}`).join(', ');
|
||||
const detailMessages = errorDetails
|
||||
.map((d: any) => `${d.field}: ${d.message}`)
|
||||
.join(", ");
|
||||
setError(`${errorMessage} - ${detailMessages}`);
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
@@ -166,25 +194,41 @@ const Profile: React.FC = () => {
|
||||
// Reset form to original data
|
||||
if (profileData) {
|
||||
setFormData({
|
||||
firstName: profileData.firstName || '',
|
||||
lastName: profileData.lastName || '',
|
||||
email: profileData.email || '',
|
||||
phone: profileData.phone || '',
|
||||
address1: profileData.address1 || '',
|
||||
address2: profileData.address2 || '',
|
||||
city: profileData.city || '',
|
||||
state: profileData.state || '',
|
||||
zipCode: profileData.zipCode || '',
|
||||
country: profileData.country || '',
|
||||
profileImage: profileData.profileImage || ''
|
||||
firstName: profileData.firstName || "",
|
||||
lastName: profileData.lastName || "",
|
||||
email: profileData.email || "",
|
||||
phone: profileData.phone || "",
|
||||
address1: profileData.address1 || "",
|
||||
address2: profileData.address2 || "",
|
||||
city: profileData.city || "",
|
||||
state: profileData.state || "",
|
||||
zipCode: profileData.zipCode || "",
|
||||
country: profileData.country || "",
|
||||
profileImage: profileData.profileImage || "",
|
||||
});
|
||||
setImagePreview(profileData.profileImage ?
|
||||
getImageUrl(profileData.profileImage) :
|
||||
null
|
||||
setImagePreview(
|
||||
profileData.profileImage ? getImageUrl(profileData.profileImage) : null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvailabilityChange = (field: string, value: string | boolean) => {
|
||||
setAvailabilityData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleWeeklyTimeChange = (day: string, field: 'availableAfter' | 'availableBefore', value: string) => {
|
||||
setAvailabilityData(prev => ({
|
||||
...prev,
|
||||
weeklyTimes: {
|
||||
...prev.weeklyTimes,
|
||||
[day]: {
|
||||
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
|
||||
[field]: value
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mt-5">
|
||||
@@ -199,292 +243,366 @@ const Profile: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8">
|
||||
<h1 className="mb-4">My Profile</h1>
|
||||
<h1 className="mb-4">Profile</h1>
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="alert alert-success" role="alert">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="alert alert-success" role="alert">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="row">
|
||||
{/* Left Sidebar Menu */}
|
||||
<div className="col-md-3">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="text-center mb-4">
|
||||
<div className="position-relative d-inline-block">
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile"
|
||||
className="rounded-circle"
|
||||
style={{ width: '150px', height: '150px', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||
style={{ width: '150px', height: '150px' }}
|
||||
>
|
||||
<i className="bi bi-person-fill text-white" style={{ fontSize: '3rem' }}></i>
|
||||
<div className="list-group list-group-flush">
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'overview' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('overview')}
|
||||
>
|
||||
<i className="bi bi-person-circle me-2"></i>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'owner-settings' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('owner-settings')}
|
||||
>
|
||||
<i className="bi bi-gear me-2"></i>
|
||||
Owner Settings
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'personal-info' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('personal-info')}
|
||||
>
|
||||
<i className="bi bi-person me-2"></i>
|
||||
Personal Information
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'notifications' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('notifications')}
|
||||
>
|
||||
<i className="bi bi-bell me-2"></i>
|
||||
Notification Settings
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'privacy' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('privacy')}
|
||||
>
|
||||
<i className="bi bi-shield-lock me-2"></i>
|
||||
Privacy & Security
|
||||
</button>
|
||||
<button
|
||||
className={`list-group-item list-group-item-action ${activeSection === 'payment' ? 'active' : ''}`}
|
||||
onClick={() => setActiveSection('payment')}
|
||||
>
|
||||
<i className="bi bi-credit-card me-2"></i>
|
||||
Payment Methods
|
||||
</button>
|
||||
<button
|
||||
className="list-group-item list-group-item-action text-danger"
|
||||
onClick={logout}
|
||||
>
|
||||
<i className="bi bi-box-arrow-right me-2"></i>
|
||||
Log Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Content Area */}
|
||||
<div className="col-md-9">
|
||||
{/* Overview Section */}
|
||||
{activeSection === 'overview' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Overview</h4>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="text-center">
|
||||
<div className="position-relative d-inline-block mb-3">
|
||||
{imagePreview ? (
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile"
|
||||
className="rounded-circle"
|
||||
style={{
|
||||
width: "120px",
|
||||
height: "120px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||
style={{ width: "120px", height: "120px" }}
|
||||
>
|
||||
<i
|
||||
className="bi bi-person-fill text-white"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<label
|
||||
htmlFor="profileImageOverview"
|
||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||
style={{ width: "35px", height: "35px", padding: "0" }}
|
||||
>
|
||||
<i className="bi bi-camera-fill"></i>
|
||||
<input
|
||||
type="file"
|
||||
id="profileImageOverview"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="d-none"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div>
|
||||
<div className="row justify-content-center mb-3">
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
placeholder="First Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mb-2"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
placeholder="Last Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex gap-2 justify-content-center">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<h5>
|
||||
{profileData?.firstName} {profileData?.lastName}
|
||||
</h5>
|
||||
<p className="text-muted">@{profileData?.username}</p>
|
||||
{profileData?.isVerified && (
|
||||
<span className="badge bg-success mb-3">
|
||||
<i className="bi bi-check-circle-fill"></i> Verified
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<i className="bi bi-pencil me-2"></i>
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Card */}
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Account Statistics</h5>
|
||||
<div className="row text-center">
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-primary mb-1">{stats.itemsListed}</h4>
|
||||
<h6 className="text-muted">Items Listed</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-success mb-1">{stats.acceptedRentals}</h4>
|
||||
<h6 className="text-muted">Active Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="p-3">
|
||||
<h4 className="text-info mb-1">{stats.totalRentals}</h4>
|
||||
<h6 className="text-muted">Total Rentals</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{editing && (
|
||||
<label
|
||||
htmlFor="profileImage"
|
||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||
style={{ width: '40px', height: '40px', padding: '0' }}
|
||||
>
|
||||
<i className="bi bi-camera-fill"></i>
|
||||
<input
|
||||
type="file"
|
||||
id="profileImage"
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
className="d-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Personal Information Section */}
|
||||
{activeSection === 'personal-info' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Personal Information</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="form-label">
|
||||
Phone Number
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="form-control"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="d-flex gap-2">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Edit Information
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h5 className="mt-3">{profileData?.firstName} {profileData?.lastName}</h5>
|
||||
<p className="text-muted">@{profileData?.username}</p>
|
||||
{profileData?.isVerified && (
|
||||
<span className="badge bg-success">
|
||||
<i className="bi bi-check-circle-fill"></i> Verified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="firstName" className="form-label">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="lastName" className="form-label">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="form-label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="form-label">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="form-control"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
placeholder="+1 (555) 123-4567"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="address1" className="form-label">Address Line 1</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="address1"
|
||||
name="address1"
|
||||
value={formData.address1}
|
||||
onChange={handleChange}
|
||||
placeholder="123 Main Street"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="address2" className="form-label">Address Line 2</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="address2"
|
||||
name="address2"
|
||||
value={formData.address2}
|
||||
onChange={handleChange}
|
||||
placeholder="Apt, Suite, Unit, etc."
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row mb-3">
|
||||
<div className="col-md-6">
|
||||
<label htmlFor="city" className="form-label">City</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="city"
|
||||
name="city"
|
||||
value={formData.city}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="state" className="form-label">State</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="state"
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
placeholder="CA"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="zipCode"
|
||||
name="zipCode"
|
||||
value={formData.zipCode}
|
||||
onChange={handleChange}
|
||||
placeholder="12345"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="country" className="form-label">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="country"
|
||||
name="country"
|
||||
value={formData.country}
|
||||
onChange={handleChange}
|
||||
placeholder="United States"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<div className="d-flex gap-2">
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Save Changes
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card mt-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Account Statistics</h5>
|
||||
<div className="row text-center">
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-primary">{stats.itemsListed}</h3>
|
||||
<p className="text-muted">Items Listed</p>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-success">{stats.acceptedRentals}</h3>
|
||||
<p className="text-muted">Accepted Rentals</p>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<h3 className="text-info">{stats.totalRentals}</h3>
|
||||
<p className="text-muted">Total Rentals</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card mt-4">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">Account Settings</h5>
|
||||
<div className="list-group list-group-flush">
|
||||
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i className="bi bi-bell me-2"></i>
|
||||
Notification Settings
|
||||
{/* Owner Settings Section */}
|
||||
{activeSection === 'owner-settings' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Owner Settings</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{/* Addresses Section */}
|
||||
<div className="mb-5">
|
||||
<h5 className="mb-3">Saved Addresses</h5>
|
||||
<p className="text-muted small mb-3">Manage addresses for your rental locations</p>
|
||||
<button className="btn btn-outline-primary">
|
||||
<i className="bi bi-plus-circle me-2"></i>
|
||||
Add New Address
|
||||
</button>
|
||||
</div>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
|
||||
{/* Availability Section */}
|
||||
<div>
|
||||
<i className="bi bi-shield-lock me-2"></i>
|
||||
Privacy & Security
|
||||
<h5 className="mb-3">Default Availability</h5>
|
||||
<p className="text-muted small mb-3">Set your general availability for all items</p>
|
||||
<AvailabilitySettings
|
||||
data={availabilityData}
|
||||
onChange={handleAvailabilityChange}
|
||||
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||
showTitle={false}
|
||||
/>
|
||||
<button className="btn btn-outline-success mt-3">
|
||||
<i className="bi bi-check2 me-2"></i>
|
||||
Save Availability
|
||||
</button>
|
||||
</div>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
<a href="#" className="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i className="bi bi-credit-card me-2"></i>
|
||||
Payment Methods
|
||||
</div>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
<button
|
||||
onClick={logout}
|
||||
className="list-group-item list-group-item-action d-flex justify-content-between align-items-center text-danger border-0 w-100 text-start"
|
||||
>
|
||||
<div>
|
||||
<i className="bi bi-box-arrow-right me-2"></i>
|
||||
Log Out
|
||||
</div>
|
||||
<i className="bi bi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Placeholder sections for other menu items */}
|
||||
{activeSection === 'notifications' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Notification Settings</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Notification preferences coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'privacy' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Privacy & Security</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Privacy and security settings coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'payment' && (
|
||||
<div>
|
||||
<h4 className="mb-4">Payment Methods</h4>
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<p className="text-muted">Payment method management coming soon...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
export default Profile;
|
||||
|
||||
@@ -34,7 +34,6 @@ export interface Item {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
isPortable: boolean;
|
||||
pickUpAvailable?: boolean;
|
||||
localDeliveryAvailable?: boolean;
|
||||
localDeliveryRadius?: number;
|
||||
|
||||
Reference in New Issue
Block a user