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

@@ -22,6 +22,7 @@ import ItemRequests from './pages/ItemRequests';
import ItemRequestDetail from './pages/ItemRequestDetail';
import CreateItemRequest from './pages/CreateItemRequest';
import MyRequests from './pages/MyRequests';
import EarningsDashboard from './pages/EarningsDashboard';
import CheckoutReturn from './components/CheckoutReturn';
import PrivateRoute from './components/PrivateRoute';
import './App.css';
@@ -123,6 +124,14 @@ function App() {
</PrivateRoute>
}
/>
<Route
path="/earnings"
element={
<PrivateRoute>
<EarningsDashboard />
</PrivateRoute>
}
/>
<Route
path="/checkout/return"
element={

View File

@@ -38,12 +38,11 @@ const CheckoutReturn: React.FC = () => {
// Convert metadata back to proper types
const rentalData = {
itemId: metadata.itemId,
startDate: metadata.startDate,
endDate: metadata.endDate,
startTime: metadata.startTime,
endTime: metadata.endTime,
startDateTime: metadata.startDateTime,
endDateTime: metadata.endDateTime,
totalAmount: parseFloat(metadata.totalAmount),
deliveryMethod: metadata.deliveryMethod,
paymentStatus: "paid", // Set since payment already succeeded
};
const response = await rentalAPI.createRental(rentalData);

View File

@@ -0,0 +1,76 @@
import React from "react";
interface EarningsStatusProps {
hasStripeAccount: boolean;
onSetupClick: () => void;
}
const EarningsStatus: React.FC<EarningsStatusProps> = ({
hasStripeAccount,
onSetupClick,
}) => {
// No Stripe account exists
if (!hasStripeAccount) {
return (
<div className="text-center">
<div className="mb-3">
<i
className="bi bi-exclamation-circle text-warning"
style={{ fontSize: "2.5rem" }}
></i>
</div>
<h6>Earnings Not Set Up</h6>
<p className="text-muted small mb-3">
Set up earnings to automatically receive payments when rentals are
completed.
</p>
<button className="btn btn-primary" onClick={onSetupClick}>
Set Up Earnings
</button>
</div>
);
}
// Account exists and is set up
return (
<div className="text-center">
<div className="mb-3">
<i
className="bi bi-check-circle text-success"
style={{ fontSize: "2.5rem" }}
></i>
</div>
<h6 className="text-success">Earnings Active</h6>
<p className="text-muted small mb-3">
Your earnings are set up and working. You'll receive payments
automatically.
</p>
<div className="small text-start">
<div className="d-flex justify-content-between mb-1">
<span>Earnings Enabled:</span>
<span className="text-success">
<i className="bi bi-check"></i> Yes
</span>
</div>
<div className="d-flex justify-content-between">
<span>Status:</span>
<span className="text-success">
<i className="bi bi-check"></i> Active
</span>
</div>
</div>
<hr />
<button
className="btn btn-outline-primary btn-sm"
onClick={() => window.open("https://dashboard.stripe.com", "_blank")}
>
<i className="bi bi-box-arrow-up-right"></i> Stripe Dashboard
</button>
</div>
);
};
export default EarningsStatus;

View File

@@ -162,6 +162,12 @@ const Navbar: React.FC = () => {
Looking For
</Link>
</li>
<li>
<Link className="dropdown-item" to="/earnings">
<i className="bi bi-cash-coin me-2"></i>
Earnings
</Link>
</li>
<li>
<Link className="dropdown-item" to="/messages">
<i className="bi bi-envelope me-2"></i>Messages

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { itemRequestAPI, itemAPI } from '../services/api';
import { ItemRequest, Item } from '../types';
import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext";
import { itemRequestAPI, itemAPI } from "../services/api";
import { ItemRequest, Item } from "../types";
interface RequestResponseModalProps {
show: boolean;
@@ -14,21 +14,21 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
show,
onHide,
request,
onResponseSubmitted
onResponseSubmitted,
}) => {
const { user } = useAuth();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [userItems, setUserItems] = useState<Item[]>([]);
const [formData, setFormData] = useState({
message: '',
offerPricePerHour: '',
offerPricePerDay: '',
availableStartDate: '',
availableEndDate: '',
existingItemId: '',
contactInfo: ''
message: "",
offerPricePerHour: "",
offerPricePerDay: "",
availableStartDate: "",
availableEndDate: "",
existingItemId: "",
contactInfo: "",
});
useEffect(() => {
@@ -43,26 +43,30 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
const response = await itemAPI.getItems({ owner: user?.id });
setUserItems(response.data.items || []);
} catch (err) {
console.error('Failed to fetch user items:', err);
console.error("Failed to fetch user items:", err);
}
};
const resetForm = () => {
setFormData({
message: '',
offerPricePerHour: '',
offerPricePerDay: '',
availableStartDate: '',
availableEndDate: '',
existingItemId: '',
contactInfo: ''
message: "",
offerPricePerHour: "",
offerPricePerDay: "",
availableStartDate: "",
availableEndDate: "",
existingItemId: "",
contactInfo: "",
});
setError(null);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const handleChange = (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
@@ -75,18 +79,22 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
try {
const responseData = {
...formData,
offerPricePerHour: formData.offerPricePerHour ? parseFloat(formData.offerPricePerHour) : null,
offerPricePerDay: formData.offerPricePerDay ? parseFloat(formData.offerPricePerDay) : null,
offerPricePerHour: formData.offerPricePerHour
? parseFloat(formData.offerPricePerHour)
: null,
offerPricePerDay: formData.offerPricePerDay
? parseFloat(formData.offerPricePerDay)
: null,
existingItemId: formData.existingItemId || null,
availableStartDate: formData.availableStartDate || null,
availableEndDate: formData.availableEndDate || null
availableEndDate: formData.availableEndDate || null,
};
await itemRequestAPI.respondToRequest(request.id, responseData);
onResponseSubmitted();
onHide();
} catch (err: any) {
setError(err.response?.data?.error || 'Failed to submit response');
setError(err.response?.data?.error || "Failed to submit response");
} finally {
setLoading(false);
}
@@ -95,157 +103,187 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
if (!request) return null;
return (
<div className={`modal fade ${show ? 'show d-block' : ''}`} tabIndex={-1} style={{ backgroundColor: show ? 'rgba(0,0,0,0.5)' : 'transparent' }}>
<div
className={`modal fade ${show ? "show d-block" : ""}`}
tabIndex={-1}
style={{ backgroundColor: show ? "rgba(0,0,0,0.5)" : "transparent" }}
>
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Respond to Request</h5>
<button type="button" className="btn-close" onClick={onHide}></button>
<button
type="button"
className="btn-close"
onClick={onHide}
></button>
</div>
<div className="modal-body">
<div className="mb-3 p-3 bg-light rounded">
<h6>{request.title}</h6>
<p className="text-muted small mb-0">{request.description}</p>
</div>
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="message" className="form-label">Your Message *</label>
<textarea
className="form-control"
id="message"
name="message"
rows={4}
value={formData.message}
onChange={handleChange}
placeholder="Explain how you can help, availability, condition of the item, etc."
required
/>
</div>
{userItems.length > 0 && (
<div className="mb-3">
<label htmlFor="existingItemId" className="form-label">Do you have an existing listing for this item?</label>
<select
className="form-select"
id="existingItemId"
name="existingItemId"
value={formData.existingItemId}
onChange={handleChange}
>
<option value="">No existing listing</option>
{userItems.map((item) => (
<option key={item.id} value={item.id}>
{item.name} - ${item.pricePerDay}/day
</option>
))}
</select>
<div className="form-text">
If you have an existing listing that matches this request, select it here.
</div>
<div className="mb-3 p-3 bg-light rounded">
<h6>{request.title}</h6>
<p className="text-muted small mb-0">{request.description}</p>
</div>
)}
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="offerPricePerDay" className="form-label">Your Price per Day</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
{error && (
<div className="alert alert-danger" role="alert">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-3">
<label htmlFor="message" className="form-label">
Your Message *
</label>
<textarea
className="form-control"
id="offerPricePerDay"
name="offerPricePerDay"
value={formData.offerPricePerDay}
id="message"
name="message"
rows={4}
value={formData.message}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
placeholder="Explain how you can help, availability, condition of the item, etc."
required
/>
</div>
</div>
<div className="col-md-6">
<label htmlFor="offerPricePerHour" className="form-label">Your Price per Hour</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="offerPricePerHour"
name="offerPricePerHour"
value={formData.offerPricePerHour}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
/>
{userItems.length > 0 && (
<div className="mb-3">
<label htmlFor="existingItemId" className="form-label">
Do you have an existing listing for this item?
</label>
<select
className="form-select"
id="existingItemId"
name="existingItemId"
value={formData.existingItemId}
onChange={handleChange}
>
<option value="">No existing listing</option>
{userItems.map((item) => (
<option key={item.id} value={item.id}>
{item.name} - ${item.pricePerDay}/day
</option>
))}
</select>
<div className="form-text">
If you have an existing listing that matches this request,
select it here.
</div>
</div>
)}
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="offerPricePerDay" className="form-label">
Your Price per Day
</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="offerPricePerDay"
name="offerPricePerDay"
value={formData.offerPricePerDay}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
/>
</div>
</div>
<div className="col-md-6">
<label htmlFor="offerPricePerHour" className="form-label">
Your Price per Hour
</label>
<div className="input-group">
<span className="input-group-text">$</span>
<input
type="number"
className="form-control"
id="offerPricePerHour"
name="offerPricePerHour"
value={formData.offerPricePerHour}
onChange={handleChange}
step="0.01"
min="0"
placeholder="0.00"
/>
</div>
</div>
</div>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="availableStartDate" className="form-label">Available From</label>
<input
type="date"
className="form-control"
id="availableStartDate"
name="availableStartDate"
value={formData.availableStartDate}
onChange={handleChange}
min={new Date().toISOString().split('T')[0]}
/>
</div>
<div className="col-md-6">
<label htmlFor="availableEndDate" className="form-label">Available Until</label>
<input
type="date"
className="form-control"
id="availableEndDate"
name="availableEndDate"
value={formData.availableEndDate}
onChange={handleChange}
min={formData.availableStartDate || new Date().toISOString().split('T')[0]}
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label htmlFor="availableStartDate" className="form-label">
Available From
</label>
<input
type="date"
className="form-control"
id="availableStartDate"
name="availableStartDate"
value={formData.availableStartDate}
onChange={handleChange}
min={new Date().toLocaleDateString()}
/>
</div>
<div className="col-md-6">
<label htmlFor="availableEndDate" className="form-label">
Available Until
</label>
<input
type="date"
className="form-control"
id="availableEndDate"
name="availableEndDate"
value={formData.availableEndDate}
onChange={handleChange}
min={
formData.availableStartDate ||
new Date().toLocaleDateString()
}
/>
</div>
</div>
<div className="mb-3">
<label htmlFor="contactInfo" className="form-label">Contact Information</label>
<input
type="text"
className="form-control"
id="contactInfo"
name="contactInfo"
value={formData.contactInfo}
onChange={handleChange}
placeholder="Phone number, email, or preferred contact method"
/>
<div className="form-text">
How should the requester contact you if they're interested?
</div>
</div>
</form>
<div className="mb-3">
<label htmlFor="contactInfo" className="form-label">
Contact Information
</label>
<input
type="text"
className="form-control"
id="contactInfo"
name="contactInfo"
value={formData.contactInfo}
onChange={handleChange}
placeholder="Phone number, email, or preferred contact method"
/>
<div className="form-text">
How should the requester contact you if they're interested?
</div>
</div>
</form>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onHide}>
<button
type="button"
className="btn btn-secondary"
onClick={onHide}
>
Cancel
</button>
<button
<button
type="button"
className="btn btn-primary"
onClick={handleSubmit}
disabled={loading || !formData.message.trim()}
>
{loading ? 'Submitting...' : 'Submit Response'}
{loading ? "Submitting..." : "Submit Response"}
</button>
</div>
</div>
@@ -254,4 +292,4 @@ const RequestResponseModal: React.FC<RequestResponseModalProps> = ({
);
};
export default RequestResponseModal;
export default RequestResponseModal;

View File

@@ -17,23 +17,9 @@ const ReviewDetailsModal: React.FC<ReviewDetailsModalProps> = ({
}) => {
if (!show) return null;
const formatDateTime = (dateString: string, timeString?: string) => {
const formatDateTime = (dateString: string) => {
const date = new Date(dateString).toLocaleDateString();
const formattedTime = timeString
? (() => {
try {
const [hour, minute] = timeString.split(":");
const hourNum = parseInt(hour);
const hour12 =
hourNum === 0 ? 12 : hourNum > 12 ? hourNum - 12 : hourNum;
const period = hourNum < 12 ? "AM" : "PM";
return `${hour12}:${minute} ${period}`;
} catch {
return "";
}
})()
: "";
return formattedTime ? `${date} at ${formattedTime}` : date;
return date;
};
const isRenter = userType === "renter";
@@ -60,8 +46,8 @@ const ReviewDetailsModal: React.FC<ReviewDetailsModalProps> = ({
<div className="mb-4 text-center">
<h6 className="mb-1">{rental.item.name}</h6>
<small className="text-muted">
{formatDateTime(rental.startDate, rental.startTime)} to{" "}
{formatDateTime(rental.endDate, rental.endTime)}
{formatDateTime(rental.startDateTime)} to{" "}
{formatDateTime(rental.endDateTime)}
</small>
</div>
)}

View File

@@ -40,12 +40,14 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
setRating(5);
setReview("");
setPrivateMessage("");
// Show success modal with appropriate message
if (response.data.reviewVisible) {
setSuccessMessage("Review published successfully!");
} else {
setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes.");
setSuccessMessage(
"Review submitted! It will be published when both parties have reviewed or after 10 minutes."
);
}
setShowSuccessModal(true);
} catch (err: any) {
@@ -114,8 +116,8 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
</h6>
<p className="mb-1 text-muted small">{rental.item.name}</p>
<small className="text-muted">
{new Date(rental.startDate).toLocaleDateString()} to{" "}
{new Date(rental.endDate).toLocaleDateString()}
{new Date(rental.startDateTime).toLocaleDateString()} to{" "}
{new Date(rental.endDateTime).toLocaleDateString()}
</small>
</div>
)}
@@ -210,7 +212,7 @@ const ReviewItemModal: React.FC<ReviewItemModalProps> = ({
</form>
</div>
</div>
<SuccessModal
show={showSuccessModal}
onClose={handleSuccessModalClose}

View File

@@ -40,12 +40,14 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
setRating(5);
setReview("");
setPrivateMessage("");
// Show success modal with appropriate message
if (response.data.reviewVisible) {
setSuccessMessage("Review published successfully!");
} else {
setSuccessMessage("Review submitted! It will be published when both parties have reviewed or after 10 minutes.");
setSuccessMessage(
"Review submitted! It will be published when both parties have reviewed or after 10 minutes."
);
}
setShowSuccessModal(true);
} catch (err: any) {
@@ -114,8 +116,8 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
</h6>
<p className="mb-1 text-muted small">{rental.item.name}</p>
<small className="text-muted">
{new Date(rental.startDate).toLocaleDateString()} to{" "}
{new Date(rental.endDate).toLocaleDateString()}
{new Date(rental.startDateTime).toLocaleDateString()} to{" "}
{new Date(rental.endDateTime).toLocaleDateString()}
</small>
</div>
)}
@@ -210,7 +212,7 @@ const ReviewRenterModal: React.FC<ReviewRenterModalProps> = ({
</form>
</div>
</div>
<SuccessModal
show={showSuccessModal}
onClose={handleSuccessModalClose}

View File

@@ -0,0 +1,211 @@
import React, { useState } from "react";
import { stripeAPI } from "../services/api";
interface StripeConnectOnboardingProps {
onComplete: () => void;
onCancel: () => void;
}
const StripeConnectOnboarding: React.FC<StripeConnectOnboardingProps> = ({
onComplete,
onCancel,
}) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [step, setStep] = useState<"start" | "creating" | "redirecting">(
"start"
);
const createStripeAccount = async () => {
setLoading(true);
setError(null);
setStep("creating");
try {
// First, create the Stripe Connected Account
const accountResponse = await stripeAPI.createConnectedAccount();
setStep("redirecting");
// Generate onboarding link
const refreshUrl = `${window.location.origin}/earnings?refresh=true`;
const returnUrl = `${window.location.origin}/earnings?setup=complete`;
const linkResponse = await stripeAPI.createAccountLink({
refreshUrl,
returnUrl,
});
const { url } = linkResponse.data;
// Redirect to Stripe onboarding
window.location.href = url;
} catch (err: any) {
setError(
err.response?.data?.error || err.message || "Failed to set up earnings"
);
setStep("start");
setLoading(false);
}
};
const handleStartSetup = () => {
createStripeAccount();
};
return (
<div
className="modal fade show d-block"
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
tabIndex={-1}
>
<div className="modal-dialog modal-lg">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Set Up Earnings</h5>
<button
type="button"
className="btn-close"
onClick={onCancel}
disabled={loading}
></button>
</div>
<div className="modal-body">
{step === "start" && (
<>
<div className="text-center mb-4">
<div className="text-primary mb-3">
<i
className="bi bi-cash-coin"
style={{ fontSize: "3rem" }}
></i>
</div>
<h4>Start Receiving Earnings</h4>
<p className="text-muted">
Set up your earnings account to automatically receive
payments
</p>
</div>
<div className="row text-center mb-4">
<div className="col-md-4">
<div className="mb-3">
<i
className="bi bi-shield-check text-success"
style={{ fontSize: "2rem" }}
></i>
</div>
<h6>Secure</h6>
<small className="text-muted">
Powered by Stripe, trusted by millions
</small>
</div>
<div className="col-md-4">
<div className="mb-3">
<i
className="bi bi-clock text-primary"
style={{ fontSize: "2rem" }}
></i>
</div>
<h6>Automatic</h6>
<small className="text-muted">
Earnings are processed automatically
</small>
</div>
<div className="col-md-4">
<div className="mb-3">
<i
className="bi bi-bank text-info"
style={{ fontSize: "2rem" }}
></i>
</div>
<h6>Direct Deposit</h6>
<small className="text-muted">
Funds go directly to your bank
</small>
</div>
</div>
<div className="alert alert-info">
<h6>
<i className="bi bi-info-circle"></i> What to expect:
</h6>
<ul className="mb-0">
<li>
You'll be redirected to Stripe to verify your identity
</li>
<li>Provide bank account details for deposits</li>
<li>The setup process takes about 5 minutes</li>
<li>Start earning immediately after setup</li>
</ul>
</div>
{error && (
<div className="alert alert-danger">
<i className="bi bi-exclamation-triangle"></i> {error}
</div>
)}
</>
)}
{step === "creating" && (
<div className="text-center">
<div className="spinner-border text-primary mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<h5>Creating your earnings account...</h5>
<p className="text-muted">
Please wait while we set up your account
</p>
</div>
)}
{step === "redirecting" && (
<div className="text-center">
<div className="spinner-border text-success mb-3" role="status">
<span className="visually-hidden">Loading...</span>
</div>
<h5>Redirecting to Stripe...</h5>
<p className="text-muted">
You'll be redirected to complete the setup process. This may
take a moment.
</p>
</div>
)}
</div>
<div className="modal-footer">
{step === "start" && (
<>
<button
type="button"
className="btn btn-secondary"
onClick={onCancel}
disabled={loading}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
onClick={handleStartSetup}
disabled={loading}
>
Set Up Earnings
</button>
</>
)}
{(step === "creating" || step === "redirecting") && (
<div className="w-100 text-center">
<small className="text-muted">
Please don't close this window...
</small>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default StripeConnectOnboarding;

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",
}}

View File

@@ -84,7 +84,6 @@ export const rentalAPI = {
api.post(`/rentals/${id}/review-renter`, data),
reviewItem: (id: string, data: any) =>
api.post(`/rentals/${id}/review-item`, data),
addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data), // Legacy
};
export const messageAPI = {
@@ -119,14 +118,10 @@ export const stripeAPI = {
}) => api.post("/stripe/create-checkout-session", data),
getCheckoutSession: (sessionId: string) =>
api.get(`/stripe/checkout-session/${sessionId}`),
// createConnectedAccount: () =>
// api.post("/stripe/accounts"),
// createAccountLink: (data: { refreshUrl: string; returnUrl: string }) =>
// api.post("/stripe/account-links", data),
// getAccountStatus: () =>
// api.get("/stripe/account-status"),
// createPaymentIntent: (data: { rentalId: string; amount: number }) =>
// api.post("/stripe/payment-intents", data),
createConnectedAccount: () => api.post("/stripe/accounts"),
createAccountLink: (data: { refreshUrl: string; returnUrl: string }) =>
api.post("/stripe/account-links", data),
getAccountStatus: () => api.get("/stripe/account-status"),
};
export default api;

View File

@@ -29,6 +29,7 @@ export interface User {
country?: string;
profileImage?: string;
isVerified: boolean;
stripeConnectedAccountId?: string;
addresses?: Address[];
}
@@ -101,13 +102,20 @@ export interface Rental {
itemId: string;
renterId: string;
ownerId: string;
startDate: string;
endDate: string;
startTime?: string;
endTime?: string;
startDateTime: string;
endDateTime: string;
totalAmount: number;
// Fee tracking fields
baseRentalAmount?: number;
platformFee?: number;
processingFee?: number;
payoutAmount?: number;
status: "pending" | "confirmed" | "active" | "completed" | "cancelled";
paymentStatus: "pending" | "paid" | "refunded";
// Payout status tracking
payoutStatus?: "pending" | "processing" | "completed" | "failed";
payoutProcessedAt?: string;
stripeTransferId?: string;
deliveryMethod: "pickup" | "delivery";
deliveryAddress?: string;
notes?: string;