simplified create item. Restructured profile. Simplified availability
This commit is contained in:
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;
|
||||
|
||||
Reference in New Issue
Block a user