This commit is contained in:
jackiettran
2025-09-02 16:15:09 -04:00
parent b52104c3fa
commit b59fc07fc3
23 changed files with 1080 additions and 417 deletions

View File

@@ -1,45 +1,49 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI } from '../services/api';
import AddressAutocomplete from '../components/AddressAutocomplete';
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { itemRequestAPI } from "../services/api";
import AddressAutocomplete from "../components/AddressAutocomplete";
const CreateItemRequest: React.FC = () => {
const navigate = useNavigate();
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [formData, setFormData] = useState({
title: '',
description: '',
address1: '',
address2: '',
city: '',
state: '',
zipCode: '',
country: 'US',
title: "",
description: "",
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
latitude: undefined as number | undefined,
longitude: undefined as number | undefined,
maxPricePerHour: '',
maxPricePerDay: '',
preferredStartDate: '',
preferredEndDate: '',
isFlexibleDates: true
maxPricePerHour: "",
maxPricePerDay: "",
preferredStartDate: "",
preferredEndDate: "",
isFlexibleDates: true,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
if (type === "checkbox") {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({ ...prev, [name]: checked }));
setFormData((prev) => ({ ...prev, [name]: checked }));
} else {
setFormData(prev => ({ ...prev, [name]: value }));
setFormData((prev) => ({ ...prev, [name]: value }));
}
};
const handleAddressChange = (value: string, lat?: number, lon?: number) => {
setFormData(prev => ({
setFormData((prev) => ({
...prev,
address1: value,
latitude: lat,
@@ -47,7 +51,7 @@ const CreateItemRequest: React.FC = () => {
city: prev.city,
state: prev.state,
zipCode: prev.zipCode,
country: prev.country
country: prev.country,
}));
};
@@ -61,16 +65,20 @@ const CreateItemRequest: React.FC = () => {
try {
const requestData = {
...formData,
maxPricePerHour: formData.maxPricePerHour ? parseFloat(formData.maxPricePerHour) : null,
maxPricePerDay: formData.maxPricePerDay ? parseFloat(formData.maxPricePerDay) : null,
maxPricePerHour: formData.maxPricePerHour
? parseFloat(formData.maxPricePerHour)
: null,
maxPricePerDay: formData.maxPricePerDay
? parseFloat(formData.maxPricePerDay)
: null,
preferredStartDate: formData.preferredStartDate || null,
preferredEndDate: formData.preferredEndDate || null
preferredEndDate: formData.preferredEndDate || null,
};
await itemRequestAPI.createItemRequest(requestData);
navigate('/my-requests');
navigate("/my-requests");
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to create item request');
setError(err.response?.data?.error || "Failed to create item request");
setLoading(false);
}
};
@@ -92,7 +100,9 @@ const CreateItemRequest: React.FC = () => {
<div className="card">
<div className="card-header">
<h2 className="mb-0">Request an Item</h2>
<p className="text-muted mb-0">Can't find what you need? Request it and let others know!</p>
<p className="text-muted mb-0">
Can't find what you need? Request it and let others know!
</p>
</div>
<div className="card-body">
{error && (
@@ -103,7 +113,9 @@ const CreateItemRequest: React.FC = () => {
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="title" className="form-label">What are you looking for? *</label>
<label htmlFor="title" className="form-label">
What are you looking for? *
</label>
<input
type="text"
className="form-control"
@@ -117,7 +129,9 @@ const CreateItemRequest: React.FC = () => {
</div>
<div className="mb-3">
<label htmlFor="description" className="form-label">Description *</label>
<label htmlFor="description" className="form-label">
Description *
</label>
<textarea
className="form-control"
id="description"
@@ -132,7 +146,9 @@ const CreateItemRequest: React.FC = () => {
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="maxPricePerDay" className="form-label">Max Price per Day</label>
<label htmlFor="maxPricePerDay" className="form-label">
Max Price per Day
</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
@@ -149,7 +165,9 @@ const CreateItemRequest: React.FC = () => {
</div>
</div>
<div className="col-md-6">
<label htmlFor="maxPricePerHour" className="form-label">Max Price per Hour</label>
<label htmlFor="maxPricePerHour" className="form-label">
Max Price per Hour
</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
@@ -178,7 +196,9 @@ const CreateItemRequest: React.FC = () => {
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="address2" className="form-label">Apartment, suite, etc.</label>
<label htmlFor="address2" className="form-label">
Apartment, suite, etc.
</label>
<input
type="text"
className="form-control"
@@ -190,7 +210,9 @@ const CreateItemRequest: React.FC = () => {
/>
</div>
<div className="col-md-6">
<label htmlFor="city" className="form-label">City</label>
<label htmlFor="city" className="form-label">
City
</label>
<input
type="text"
className="form-control"
@@ -205,7 +227,9 @@ const CreateItemRequest: React.FC = () => {
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="state" className="form-label">State</label>
<label htmlFor="state" className="form-label">
State
</label>
<input
type="text"
className="form-control"
@@ -217,7 +241,9 @@ const CreateItemRequest: React.FC = () => {
/>
</div>
<div className="col-md-6">
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
<label htmlFor="zipCode" className="form-label">
ZIP Code
</label>
<input
type="text"
className="form-control"
@@ -240,7 +266,10 @@ const CreateItemRequest: React.FC = () => {
checked={formData.isFlexibleDates}
onChange={handleChange}
/>
<label className="form-check-label" htmlFor="isFlexibleDates">
<label
className="form-check-label"
htmlFor="isFlexibleDates"
>
I'm flexible with dates
</label>
</div>
@@ -249,7 +278,12 @@ const CreateItemRequest: React.FC = () => {
{!formData.isFlexibleDates && (
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="preferredStartDate" className="form-label">Preferred Start Date</label>
<label
htmlFor="preferredStartDate"
className="form-label"
>
Preferred Start Date
</label>
<input
type="date"
className="form-control"
@@ -257,11 +291,13 @@ const CreateItemRequest: React.FC = () => {
name="preferredStartDate"
value={formData.preferredStartDate}
onChange={handleChange}
min={new Date().toISOString().split('T')[0]}
min={new Date().toLocaleDateString()}
/>
</div>
<div className="col-md-6">
<label htmlFor="preferredEndDate" className="form-label">Preferred End Date</label>
<label htmlFor="preferredEndDate" className="form-label">
Preferred End Date
</label>
<input
type="date"
className="form-control"
@@ -269,7 +305,10 @@ const CreateItemRequest: React.FC = () => {
name="preferredEndDate"
value={formData.preferredEndDate}
onChange={handleChange}
min={formData.preferredStartDate || new Date().toISOString().split('T')[0]}
min={
formData.preferredStartDate ||
new Date().toLocaleDateString()
}
/>
</div>
</div>
@@ -281,7 +320,7 @@ const CreateItemRequest: React.FC = () => {
className="btn btn-primary"
disabled={loading}
>
{loading ? 'Creating Request...' : 'Create Request'}
{loading ? "Creating Request..." : "Create Request"}
</button>
<button
type="button"
@@ -300,4 +339,4 @@ const CreateItemRequest: React.FC = () => {
);
};
export default CreateItemRequest;
export default CreateItemRequest;

View File

@@ -0,0 +1,288 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { rentalAPI, userAPI } from "../services/api";
import { Rental, User } from "../types";
import StripeConnectOnboarding from "../components/StripeConnectOnboarding";
import EarningsStatus from "../components/EarningsStatus";
interface EarningsData {
totalEarnings: number;
pendingEarnings: number;
completedEarnings: number;
rentalsWithEarnings: Rental[];
}
const EarningsDashboard: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [earningsData, setEarningsData] = useState<EarningsData | null>(null);
const [userProfile, setUserProfile] = useState<User | null>(null);
const [showOnboarding, setShowOnboarding] = useState(false);
useEffect(() => {
fetchUserProfile();
fetchEarningsData();
}, []);
const fetchUserProfile = async () => {
try {
const response = await userAPI.getProfile();
setUserProfile(response.data);
} catch (err) {
console.error("Failed to fetch user profile:", err);
}
};
const fetchEarningsData = async () => {
try {
// Get completed rentals where user is the owner
const response = await rentalAPI.getMyListings();
const rentals = response.data || [];
// Filter for completed rentals with earnings data
const completedRentals = rentals.filter(
(rental: Rental) => rental.status === "completed" && rental.payoutAmount
);
// Calculate earnings - convert string values to numbers
const totalEarnings = completedRentals.reduce(
(sum: number, rental: Rental) =>
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
0
);
const pendingEarnings = completedRentals
.filter((rental: Rental) => rental.payoutStatus === "pending")
.reduce(
(sum: number, rental: Rental) =>
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
0
);
const completedEarnings = completedRentals
.filter((rental: Rental) => rental.payoutStatus === "completed")
.reduce(
(sum: number, rental: Rental) =>
sum + parseFloat(rental.payoutAmount?.toString() || "0"),
0
);
setEarningsData({
totalEarnings,
pendingEarnings,
completedEarnings,
rentalsWithEarnings: completedRentals,
});
} catch (err: any) {
setError(err.response?.data?.message || "Failed to fetch earnings data");
} finally {
setLoading(false);
}
};
const handleSetupComplete = () => {
setShowOnboarding(false);
fetchUserProfile(); // Refresh user profile after setup
fetchEarningsData();
};
if (loading) {
return (
<div className="container mt-5">
<div className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
</div>
);
}
const hasStripeAccount = !!userProfile?.stripeConnectedAccountId;
return (
<div className="container mt-4">
<div className="row">
<div className="col-12">
<h1>My Earnings</h1>
<p className="text-muted">
Manage your rental earnings and payment setup
</p>
</div>
</div>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<div className="row">
<div className="col-md-8">
{/* Earnings Overview */}
<div className="card mb-4">
<div className="card-header">
<h5 className="mb-0">Earnings Overview</h5>
</div>
<div className="card-body">
<div className="row text-center">
<div className="col-md-4">
<h3 className="text-primary">
${(earningsData?.totalEarnings || 0).toFixed(2)}
</h3>
<p className="text-muted">Total Earnings</p>
</div>
<div className="col-md-4">
<h3 className="text-warning">
${(earningsData?.pendingEarnings || 0).toFixed(2)}
</h3>
<p className="text-muted">Pending Earnings</p>
</div>
<div className="col-md-4">
<h3 className="text-success">
${(earningsData?.completedEarnings || 0).toFixed(2)}
</h3>
<p className="text-muted">Paid Out</p>
</div>
</div>
</div>
</div>
{/* Earnings History */}
<div className="card mb-4">
<div className="card-header">
<h5 className="mb-0">Earnings History</h5>
</div>
<div className="card-body">
{earningsData?.rentalsWithEarnings.length === 0 ? (
<p className="text-muted text-center">
No completed rentals yet
</p>
) : (
<div className="table-responsive">
<table className="table">
<thead>
<tr>
<th>Item</th>
<th>Rental Period</th>
<th>Total Amount</th>
<th>Your Earnings</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{earningsData?.rentalsWithEarnings.map((rental) => (
<tr key={rental.id}>
<td>
<Link to={`/items/${rental.itemId}`}>
{rental.item?.name || "Item"}
</Link>
</td>
<td>
<small>
{new Date(rental.startDateTime).toLocaleString()}
<br />
to
<br />
{new Date(rental.endDateTime).toLocaleString()}
</small>
</td>
<td>
$
{parseFloat(
rental.totalAmount?.toString() || "0"
).toFixed(2)}
</td>
<td className="text-success">
<strong>
$
{parseFloat(
rental.payoutAmount?.toString() || "0"
).toFixed(2)}
</strong>
</td>
<td>
<span
className={`badge ${
rental.payoutStatus === "completed"
? "bg-success"
: rental.payoutStatus === "processing"
? "bg-warning"
: "bg-secondary"
}`}
>
{rental.payoutStatus === "completed"
? "Paid"
: rental.payoutStatus === "processing"
? "Processing"
: "Pending"}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
{/* Sidebar */}
<div className="col-md-4">
{/* Earnings Setup Status */}
<div className="card mb-4">
<div className="card-header">
<h5 className="mb-0">Earnings Setup</h5>
</div>
<div className="card-body">
<EarningsStatus
hasStripeAccount={hasStripeAccount}
onSetupClick={() => setShowOnboarding(true)}
/>
</div>
</div>
{/* Quick Stats */}
<div className="card">
<div className="card-header">
<h5 className="mb-0">Quick Stats</h5>
</div>
<div className="card-body">
<div className="d-flex justify-content-between mb-2">
<span>Completed Rentals:</span>
<strong>{earningsData?.rentalsWithEarnings.length || 0}</strong>
</div>
<div className="d-flex justify-content-between mb-2">
<span>Average Earning:</span>
<strong>
$
{earningsData?.rentalsWithEarnings.length
? (
(earningsData.totalEarnings || 0) /
earningsData.rentalsWithEarnings.length
).toFixed(2)
: "0.00"}
</strong>
</div>
<div className="d-flex justify-content-between">
<span>Platform Fee:</span>
<strong>20%</strong>
</div>
</div>
</div>
</div>
</div>
{/* Stripe Connect Onboarding Modal */}
{showOnboarding && (
<StripeConnectOnboarding
onComplete={handleSetupComplete}
onCancel={() => setShowOnboarding(false)}
/>
)}
</div>
);
};
export default EarningsDashboard;

View File

@@ -488,7 +488,7 @@ const ItemDetail: React.FC = () => {
e.target.value
)
}
min={new Date().toISOString().split("T")[0]}
min={new Date().toLocaleDateString()}
style={{ flex: "1 1 50%" }}
/>
<select
@@ -537,7 +537,7 @@ const ItemDetail: React.FC = () => {
}
min={
rentalDates.startDate ||
new Date().toISOString().split("T")[0]
new Date().toLocaleDateString()
}
style={{ flex: "1 1 50%" }}
/>

View File

@@ -22,10 +22,9 @@ const MyListings: React.FC = () => {
};
// Helper function to format date and time together
const formatDateTime = (dateString: string, timeString?: string) => {
const date = new Date(dateString).toLocaleDateString();
const formattedTime = formatTime(timeString);
return formattedTime ? `${date} at ${formattedTime}` : date;
const formatDateTime = (dateTimeString: string) => {
const date = new Date(dateTimeString).toLocaleDateString();
return date;
};
const { user } = useAuth();
@@ -98,7 +97,6 @@ const MyListings: React.FC = () => {
const fetchOwnerRentals = async () => {
try {
const response = await rentalAPI.getMyListings();
console.log("Owner rentals data from backend:", response.data);
setOwnerRentals(response.data);
} catch (err: any) {
console.error("Failed to fetch owner rentals:", err);
@@ -128,7 +126,6 @@ const MyListings: React.FC = () => {
const handleCompleteClick = async (rental: Rental) => {
try {
console.log("Marking rental as completed:", rental.id);
await rentalAPI.markAsCompleted(rental.id);
setSelectedRentalForReview(rental);
@@ -237,10 +234,8 @@ const MyListings: React.FC = () => {
<p className="mb-1 text-dark small">
<strong>Period:</strong>
<br />
{formatDateTime(
rental.startDate,
rental.startTime
)} - {formatDateTime(rental.endDate, rental.endTime)}
{formatDateTime(rental.startDateTime)} -{" "}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 text-dark small">

View File

@@ -21,13 +21,6 @@ const MyRentals: React.FC = () => {
}
};
// Helper function to format date and time together
const formatDateTime = (dateString: string, timeString?: string) => {
const date = new Date(dateString).toLocaleDateString();
const formattedTime = formatTime(timeString);
return formattedTime ? `${date} at ${formattedTime}` : date;
};
const { user } = useAuth();
const [rentals, setRentals] = useState<Rental[]>([]);
const [loading, setLoading] = useState(true);
@@ -171,19 +164,16 @@ const MyRentals: React.FC = () => {
{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 />
<strong>Start:</strong>{" "}
{formatDateTime(rental.startDate, rental.startTime)}
{new Date(rental.startDateTime).toLocaleString()}
<br />
<strong>End:</strong>{" "}
{formatDateTime(rental.endDate, rental.endTime)}
{new Date(rental.endDateTime).toLocaleString()}
</p>
<p className="mb-1 text-dark">

View File

@@ -176,10 +176,9 @@ const Profile: React.FC = () => {
}
};
const formatDateTime = (dateString: string, timeString?: string) => {
const date = new Date(dateString).toLocaleDateString();
const formattedTime = formatTime(timeString);
return formattedTime ? `${date} at ${formattedTime}` : date;
const formatDateTime = (dateTimeString: string) => {
const date = new Date(dateTimeString).toLocaleDateString();
return date;
};
const fetchRentalHistory = async () => {
@@ -853,16 +852,10 @@ const Profile: React.FC = () => {
<strong>Period:</strong>
<br />
<strong>Start:</strong>{" "}
{formatDateTime(
rental.startDate,
rental.startTime
)}
{formatDateTime(rental.startDateTime)}
<br />
<strong>End:</strong>{" "}
{formatDateTime(
rental.endDate,
rental.endTime
)}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 small">
@@ -994,15 +987,8 @@ const Profile: React.FC = () => {
<p className="mb-1 small">
<strong>Period:</strong>
<br />
{formatDateTime(
rental.startDate,
rental.startTime
)}{" "}
-{" "}
{formatDateTime(
rental.endDate,
rental.endTime
)}
{formatDateTime(rental.startDateTime)} -{" "}
{formatDateTime(rental.endDateTime)}
</p>
<p className="mb-1 small">

View File

@@ -28,9 +28,24 @@ const RentItem: React.FC = () => {
const [totalCost, setTotalCost] = useState(0);
const convertToUTC = (dateString: string, timeString: string): string => {
if (!dateString || !timeString) {
throw new Error("Date and time are required");
}
// Create date in user's local timezone
const localDateTime = new Date(`${dateString}T${timeString}`);
// Return UTC ISO string
return localDateTime.toISOString();
};
const formatDate = (dateString: string) => {
if (!dateString) return "";
return new Date(dateString).toLocaleDateString();
// Use safe date parsing to avoid timezone issues
const [year, month, day] = dateString.split("-");
const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return date.toLocaleDateString();
};
const formatTime = (timeString: string) => {
@@ -167,10 +182,14 @@ const RentItem: React.FC = () => {
itemName={item.name}
rentalData={{
itemId: item.id,
startDate: manualSelection.startDate,
endDate: manualSelection.endDate,
startTime: manualSelection.startTime,
endTime: manualSelection.endTime,
startDateTime: convertToUTC(
manualSelection.startDate,
manualSelection.startTime
),
endDateTime: convertToUTC(
manualSelection.endDate,
manualSelection.endTime
),
totalAmount: totalCost,
deliveryMethod: "pickup",
}}