made components that create and edit item can share, started item detail changes, listings provide more views

This commit is contained in:
jackiettran
2025-08-20 17:06:47 -04:00
parent ddd27a59f9
commit b624841350
13 changed files with 1008 additions and 982 deletions

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;