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

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