made components that create and edit item can share, started item detail changes, listings provide more views
This commit is contained in:
60
frontend/src/components/DeliveryOptions.tsx
Normal file
60
frontend/src/components/DeliveryOptions.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
|
||||
interface DeliveryOptionsProps {
|
||||
pickUpAvailable: boolean;
|
||||
inPlaceUseAvailable: boolean;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
const DeliveryOptions: React.FC<DeliveryOptionsProps> = ({
|
||||
pickUpAvailable,
|
||||
inPlaceUseAvailable,
|
||||
onChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<label className="form-label">
|
||||
How will renters receive this item?
|
||||
</label>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="pickUpAvailable"
|
||||
name="pickUpAvailable"
|
||||
checked={pickUpAvailable}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||
Pick-Up/Drop-off
|
||||
<div className="small text-muted">
|
||||
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="inPlaceUseAvailable"
|
||||
name="inPlaceUseAvailable"
|
||||
checked={inPlaceUseAvailable}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor="inPlaceUseAvailable"
|
||||
>
|
||||
In-Place Use
|
||||
<div className="small text-muted">
|
||||
They use at your location
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryOptions;
|
||||
70
frontend/src/components/ImageUpload.tsx
Normal file
70
frontend/src/components/ImageUpload.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ImageUploadProps {
|
||||
imageFiles: File[];
|
||||
imagePreviews: string[];
|
||||
onImageChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onRemoveImage: (index: number) => void;
|
||||
error: string;
|
||||
}
|
||||
|
||||
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||||
imageFiles,
|
||||
imagePreviews,
|
||||
onImageChange,
|
||||
onRemoveImage,
|
||||
error
|
||||
}) => {
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="mb-3">
|
||||
<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"
|
||||
onChange={onImageChange}
|
||||
accept="image/*"
|
||||
multiple
|
||||
disabled={imageFiles.length >= 5}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{imagePreviews.length > 0 && (
|
||||
<div className="row mt-3">
|
||||
{imagePreviews.map((preview, index) => (
|
||||
<div key={index} className="col-6 col-md-4 col-lg-3 mb-3">
|
||||
<div className="position-relative">
|
||||
<img
|
||||
src={preview}
|
||||
alt={`Preview ${index + 1}`}
|
||||
className="img-fluid rounded"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "150px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-danger position-absolute top-0 end-0 m-1"
|
||||
onClick={() => onRemoveImage(index)}
|
||||
>
|
||||
<i className="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
53
frontend/src/components/ItemInformation.tsx
Normal file
53
frontend/src/components/ItemInformation.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
|
||||
interface ItemInformationProps {
|
||||
name: string;
|
||||
description: string;
|
||||
onChange: (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => void;
|
||||
}
|
||||
|
||||
const ItemInformation: React.FC<ItemInformationProps> = ({
|
||||
name,
|
||||
description,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="mb-3">
|
||||
<label htmlFor="name" className="form-label">
|
||||
Item Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="name"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="description" className="form-label">
|
||||
Description *
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="description"
|
||||
name="description"
|
||||
rows={4}
|
||||
value={description}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemInformation;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Rental } from '../types';
|
||||
import { rentalAPI } from '../services/api';
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Rental } from "../types";
|
||||
import { rentalAPI } from "../services/api";
|
||||
|
||||
interface ItemReviewsProps {
|
||||
itemId: string;
|
||||
@@ -20,24 +20,25 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
// Fetch all rentals for this item
|
||||
const response = await rentalAPI.getMyListings();
|
||||
const allRentals: Rental[] = response.data;
|
||||
|
||||
|
||||
// Filter for completed rentals with reviews for this specific item
|
||||
const itemReviews = allRentals.filter(
|
||||
rental => rental.itemId === itemId &&
|
||||
rental.status === 'completed' &&
|
||||
rental.rating &&
|
||||
rental.review
|
||||
(rental) =>
|
||||
rental.itemId === itemId &&
|
||||
rental.status === "completed" &&
|
||||
rental.rating &&
|
||||
rental.review
|
||||
);
|
||||
|
||||
|
||||
setReviews(itemReviews);
|
||||
|
||||
|
||||
// Calculate average rating
|
||||
if (itemReviews.length > 0) {
|
||||
const sum = itemReviews.reduce((acc, r) => acc + (r.rating || 0), 0);
|
||||
setAverageRating(sum / itemReviews.length);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch reviews:', error);
|
||||
console.error("Failed to fetch reviews:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -49,7 +50,7 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<i
|
||||
key={star}
|
||||
className={`bi ${star <= rating ? 'bi-star-fill' : 'bi-star'}`}
|
||||
className={`bi ${star <= rating ? "bi-star-fill" : "bi-star"}`}
|
||||
></i>
|
||||
))}
|
||||
</span>
|
||||
@@ -72,16 +73,18 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h5>Reviews</h5>
|
||||
|
||||
|
||||
{reviews.length === 0 ? (
|
||||
<p className="text-muted">No reviews yet. Be the first to rent and review this item!</p>
|
||||
<p className="text-muted">Be the first to rent and review this item!</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{renderStars(Math.round(averageRating))}
|
||||
<span className="fw-bold">{averageRating.toFixed(1)}</span>
|
||||
<span className="text-muted">({reviews.length} {reviews.length === 1 ? 'review' : 'reviews'})</span>
|
||||
<span className="text-muted">
|
||||
({reviews.length} {reviews.length === 1 ? "review" : "reviews"})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,18 +99,27 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
src={rental.renter.profileImage}
|
||||
alt={`${rental.renter.firstName} ${rental.renter.lastName}`}
|
||||
className="rounded-circle"
|
||||
style={{ width: '32px', height: '32px', objectFit: 'cover' }}
|
||||
style={{
|
||||
width: "32px",
|
||||
height: "32px",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||
style={{ width: '32px', height: '32px' }}
|
||||
style={{ width: "32px", height: "32px" }}
|
||||
>
|
||||
<i className="bi bi-person-fill text-white" style={{ fontSize: '0.8rem' }}></i>
|
||||
<i
|
||||
className="bi bi-person-fill text-white"
|
||||
style={{ fontSize: "0.8rem" }}
|
||||
></i>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<strong>{rental.renter?.firstName} {rental.renter?.lastName}</strong>
|
||||
<strong>
|
||||
{rental.renter?.firstName} {rental.renter?.lastName}
|
||||
</strong>
|
||||
<div className="small">
|
||||
{renderStars(rental.rating || 0)}
|
||||
</div>
|
||||
@@ -128,4 +140,4 @@ const ItemReviews: React.FC<ItemReviewsProps> = ({ itemId }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemReviews;
|
||||
export default ItemReviews;
|
||||
|
||||
179
frontend/src/components/LocationForm.tsx
Normal file
179
frontend/src/components/LocationForm.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React from 'react';
|
||||
import { Address } from '../types';
|
||||
|
||||
interface LocationFormData {
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
}
|
||||
|
||||
interface LocationFormProps {
|
||||
data: LocationFormData;
|
||||
userAddresses: Address[];
|
||||
selectedAddressId: string;
|
||||
addressesLoading: boolean;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
|
||||
onAddressSelect: (addressId: string) => void;
|
||||
formatAddressDisplay: (address: Address) => string;
|
||||
}
|
||||
|
||||
const LocationForm: React.FC<LocationFormProps> = ({
|
||||
data,
|
||||
userAddresses,
|
||||
selectedAddressId,
|
||||
addressesLoading,
|
||||
onChange,
|
||||
onAddressSelect,
|
||||
formatAddressDisplay
|
||||
}) => {
|
||||
const usStates = [
|
||||
"Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut",
|
||||
"Delaware", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa",
|
||||
"Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan",
|
||||
"Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire",
|
||||
"New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio",
|
||||
"Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota",
|
||||
"Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia",
|
||||
"Wisconsin", "Wyoming"
|
||||
];
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{addressesLoading ? (
|
||||
<div className="text-center py-3">
|
||||
<div className="spinner-border spinner-border-sm" role="status">
|
||||
<span className="visually-hidden">Loading addresses...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Multiple addresses - show dropdown */}
|
||||
{userAddresses.length > 1 && (
|
||||
<div className="mb-3">
|
||||
<label className="form-label">Select Address</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedAddressId || "new"}
|
||||
onChange={(e) => onAddressSelect(e.target.value)}
|
||||
>
|
||||
<option value="new">Enter new address</option>
|
||||
{userAddresses.map(address => (
|
||||
<option key={address.id} value={address.id}>
|
||||
{formatAddressDisplay(address)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show form fields for all scenarios with addresses <= 1 or when "new" is selected */}
|
||||
{(userAddresses.length <= 1 ||
|
||||
(userAddresses.length > 1 && !selectedAddressId)) && (
|
||||
<>
|
||||
<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={data.address1}
|
||||
onChange={onChange}
|
||||
placeholder="123 Main Street"
|
||||
required
|
||||
/>
|
||||
</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={data.address2}
|
||||
onChange={onChange}
|
||||
placeholder="Apt, Suite, Unit, etc."
|
||||
/>
|
||||
</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={data.city}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="state" className="form-label">
|
||||
State *
|
||||
</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="state"
|
||||
name="state"
|
||||
value={data.state}
|
||||
onChange={onChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select State</option>
|
||||
{usStates.map(state => (
|
||||
<option key={state} value={state}>
|
||||
{state}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</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={data.zipCode}
|
||||
onChange={onChange}
|
||||
placeholder="12345"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationForm;
|
||||
102
frontend/src/components/PricingForm.tsx
Normal file
102
frontend/src/components/PricingForm.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PricingFormProps {
|
||||
priceType: "hour" | "day";
|
||||
pricePerHour: number | string;
|
||||
pricePerDay: number | string;
|
||||
replacementCost: number | string;
|
||||
minimumRentalDays: number;
|
||||
onPriceTypeChange: (type: "hour" | "day") => void;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => void;
|
||||
}
|
||||
|
||||
const PricingForm: React.FC<PricingFormProps> = ({
|
||||
priceType,
|
||||
pricePerHour,
|
||||
pricePerDay,
|
||||
replacementCost,
|
||||
minimumRentalDays,
|
||||
onPriceTypeChange,
|
||||
onChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="mb-3">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<label className="col-form-label">Price per</label>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<select
|
||||
className="form-select"
|
||||
value={priceType}
|
||||
onChange={(e) => onPriceTypeChange(e.target.value as "hour" | "day")}
|
||||
>
|
||||
<option value="hour">Hour</option>
|
||||
<option value="day">Day</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="input-group">
|
||||
<span className="input-group-text">$</span>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
||||
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
||||
value={priceType === "hour" ? pricePerHour : pricePerDay}
|
||||
onChange={onChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="minimumRentalDays" className="form-label">
|
||||
Minimum Rental {priceType === "hour" ? "Hours" : "Days"}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="minimumRentalDays"
|
||||
name="minimumRentalDays"
|
||||
value={minimumRentalDays}
|
||||
onChange={onChange}
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<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
|
||||
type="number"
|
||||
className="form-control"
|
||||
id="replacementCost"
|
||||
name="replacementCost"
|
||||
value={replacementCost}
|
||||
onChange={onChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingForm;
|
||||
47
frontend/src/components/RulesForm.tsx
Normal file
47
frontend/src/components/RulesForm.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
interface RulesFormProps {
|
||||
needsTraining: boolean;
|
||||
rules: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
}
|
||||
|
||||
const RulesForm: React.FC<RulesFormProps> = ({
|
||||
needsTraining,
|
||||
rules,
|
||||
onChange
|
||||
}) => {
|
||||
return (
|
||||
<div className="card mb-4">
|
||||
<div className="card-body">
|
||||
<div className="form-check mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id="needsTraining"
|
||||
name="needsTraining"
|
||||
checked={needsTraining}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="needsTraining">
|
||||
Requires in-person training before rental
|
||||
</label>
|
||||
</div>
|
||||
<label htmlFor="rules" className="form-label">
|
||||
Additional Rules
|
||||
</label>
|
||||
<textarea
|
||||
className="form-control"
|
||||
id="rules"
|
||||
name="rules"
|
||||
rows={3}
|
||||
value={rules || ""}
|
||||
onChange={onChange}
|
||||
placeholder="Any specific rules for renting this item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesForm;
|
||||
Reference in New Issue
Block a user