streamlined address and availability

This commit is contained in:
jackiettran
2025-08-20 14:56:16 -04:00
parent 66dc187295
commit ddd27a59f9
10 changed files with 1173 additions and 314 deletions

View File

@@ -74,6 +74,30 @@ const User = sequelize.define('User', {
isVerified: { isVerified: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
defaultValue: false defaultValue: false
},
defaultAvailableAfter: {
type: DataTypes.STRING,
defaultValue: '09:00'
},
defaultAvailableBefore: {
type: DataTypes.STRING,
defaultValue: '17:00'
},
defaultSpecifyTimesPerDay: {
type: DataTypes.BOOLEAN,
defaultValue: false
},
defaultWeeklyTimes: {
type: DataTypes.JSONB,
defaultValue: {
sunday: { availableAfter: "09:00", availableBefore: "17:00" },
monday: { availableAfter: "09:00", availableBefore: "17:00" },
tuesday: { availableAfter: "09:00", availableBefore: "17:00" },
wednesday: { availableAfter: "09:00", availableBefore: "17:00" },
thursday: { availableAfter: "09:00", availableBefore: "17:00" },
friday: { availableAfter: "09:00", availableBefore: "17:00" },
saturday: { availableAfter: "09:00", availableBefore: "17:00" }
}
} }
}, { }, {
hooks: { hooks: {

View File

@@ -0,0 +1,54 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const UserAddress = sequelize.define('UserAddress', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
userId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'Users',
key: 'id'
}
},
address1: {
type: DataTypes.STRING,
allowNull: false
},
address2: {
type: DataTypes.STRING
},
city: {
type: DataTypes.STRING,
allowNull: false
},
state: {
type: DataTypes.STRING,
allowNull: false
},
zipCode: {
type: DataTypes.STRING,
allowNull: false
},
country: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'US'
},
latitude: {
type: DataTypes.DECIMAL(10, 8)
},
longitude: {
type: DataTypes.DECIMAL(11, 8)
},
isPrimary: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
});
module.exports = UserAddress;

View File

@@ -5,6 +5,7 @@ const Rental = require('./Rental');
const Message = require('./Message'); const Message = require('./Message');
const ItemRequest = require('./ItemRequest'); const ItemRequest = require('./ItemRequest');
const ItemRequestResponse = require('./ItemRequestResponse'); const ItemRequestResponse = require('./ItemRequestResponse');
const UserAddress = require('./UserAddress');
User.hasMany(Item, { as: 'ownedItems', foreignKey: 'ownerId' }); User.hasMany(Item, { as: 'ownedItems', foreignKey: 'ownerId' });
Item.belongsTo(User, { as: 'owner', foreignKey: 'ownerId' }); Item.belongsTo(User, { as: 'owner', foreignKey: 'ownerId' });
@@ -33,6 +34,9 @@ ItemRequestResponse.belongsTo(User, { as: 'responder', foreignKey: 'responderId'
ItemRequestResponse.belongsTo(ItemRequest, { as: 'itemRequest', foreignKey: 'itemRequestId' }); ItemRequestResponse.belongsTo(ItemRequest, { as: 'itemRequest', foreignKey: 'itemRequestId' });
ItemRequestResponse.belongsTo(Item, { as: 'existingItem', foreignKey: 'existingItemId' }); ItemRequestResponse.belongsTo(Item, { as: 'existingItem', foreignKey: 'existingItemId' });
User.hasMany(UserAddress, { as: 'addresses', foreignKey: 'userId' });
UserAddress.belongsTo(User, { as: 'user', foreignKey: 'userId' });
module.exports = { module.exports = {
sequelize, sequelize,
User, User,
@@ -40,5 +44,6 @@ module.exports = {
Rental, Rental,
Message, Message,
ItemRequest, ItemRequest,
ItemRequestResponse ItemRequestResponse,
UserAddress
}; };

View File

@@ -1,5 +1,5 @@
const express = require('express'); const express = require('express');
const { User } = require('../models'); // Import from models/index.js to get models with associations const { User, UserAddress } = require('../models'); // Import from models/index.js to get models with associations
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { uploadProfileImage } = require('../middleware/upload'); const { uploadProfileImage } = require('../middleware/upload');
const fs = require('fs').promises; const fs = require('fs').promises;
@@ -17,6 +17,105 @@ router.get('/profile', authenticateToken, async (req, res) => {
} }
}); });
// Address routes (must come before /:id route)
router.get('/addresses', authenticateToken, async (req, res) => {
try {
const addresses = await UserAddress.findAll({
where: { userId: req.user.id },
order: [['isPrimary', 'DESC'], ['createdAt', 'ASC']]
});
res.json(addresses);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.post('/addresses', authenticateToken, async (req, res) => {
try {
const address = await UserAddress.create({
...req.body,
userId: req.user.id
});
res.status(201).json(address);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.put('/addresses/:id', authenticateToken, async (req, res) => {
try {
const address = await UserAddress.findByPk(req.params.id);
if (!address) {
return res.status(404).json({ error: 'Address not found' });
}
if (address.userId !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
await address.update(req.body);
res.json(address);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.delete('/addresses/:id', authenticateToken, async (req, res) => {
try {
const address = await UserAddress.findByPk(req.params.id);
if (!address) {
return res.status(404).json({ error: 'Address not found' });
}
if (address.userId !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
}
await address.destroy();
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// User availability routes (must come before /:id route)
router.get('/availability', authenticateToken, async (req, res) => {
try {
const user = await User.findByPk(req.user.id, {
attributes: ['defaultAvailableAfter', 'defaultAvailableBefore', 'defaultSpecifyTimesPerDay', 'defaultWeeklyTimes']
});
res.json({
generalAvailableAfter: user.defaultAvailableAfter,
generalAvailableBefore: user.defaultAvailableBefore,
specifyTimesPerDay: user.defaultSpecifyTimesPerDay,
weeklyTimes: user.defaultWeeklyTimes
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.put('/availability', authenticateToken, async (req, res) => {
try {
const { generalAvailableAfter, generalAvailableBefore, specifyTimesPerDay, weeklyTimes } = req.body;
await User.update({
defaultAvailableAfter: generalAvailableAfter,
defaultAvailableBefore: generalAvailableBefore,
defaultSpecifyTimesPerDay: specifyTimesPerDay,
defaultWeeklyTimes: weeklyTimes
}, {
where: { id: req.user.id }
});
res.json({ message: 'Availability updated successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
try { try {
const user = await User.findByPk(req.params.id, { const user = await User.findByPk(req.params.id, {

View File

@@ -1,4 +1,4 @@
import React from 'react'; import React from "react";
interface AvailabilityData { interface AvailabilityData {
generalAvailableAfter: string; generalAvailableAfter: string;
@@ -18,31 +18,35 @@ interface AvailabilityData {
interface AvailabilitySettingsProps { interface AvailabilitySettingsProps {
data: AvailabilityData; data: AvailabilityData;
onChange: (field: string, value: string | boolean) => void; onChange: (field: string, value: string | boolean) => void;
onWeeklyTimeChange: (day: string, field: 'availableAfter' | 'availableBefore', value: string) => void; onWeeklyTimeChange: (
showTitle?: boolean; day: string,
field: "availableAfter" | "availableBefore",
value: string
) => void;
} }
const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
data, data,
onChange, onChange,
onWeeklyTimeChange, onWeeklyTimeChange,
showTitle = true
}) => { }) => {
const generateTimeOptions = () => { const generateTimeOptions = () => {
const options = []; const options = [];
for (let hour = 0; hour < 24; hour++) { for (let hour = 0; hour < 24; hour++) {
const time24 = `${hour.toString().padStart(2, '0')}:00`; const time24 = `${hour.toString().padStart(2, "0")}:00`;
const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; const hour12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
const period = hour < 12 ? 'AM' : 'PM'; const period = hour < 12 ? "AM" : "PM";
const time12 = `${hour12}:00 ${period}`; const time12 = `${hour12}:00 ${period}`;
options.push({ value: time24, label: time12 }); options.push({ value: time24, label: time12 });
} }
return options; return options;
}; };
const handleGeneralChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => { const handleGeneralChange = (
e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>
) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
if (type === 'checkbox') { if (type === "checkbox") {
const checked = (e.target as HTMLInputElement).checked; const checked = (e.target as HTMLInputElement).checked;
onChange(name, checked); onChange(name, checked);
} else { } else {
@@ -52,8 +56,6 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
return ( return (
<div> <div>
{showTitle && <h5 className="card-title">Availability</h5>}
{/* General Times */} {/* General Times */}
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6">
@@ -129,7 +131,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
className="form-select form-select-sm" className="form-select form-select-sm"
value={times.availableAfter} value={times.availableAfter}
onChange={(e) => onChange={(e) =>
onWeeklyTimeChange(day, 'availableAfter', e.target.value) onWeeklyTimeChange(day, "availableAfter", e.target.value)
} }
> >
{generateTimeOptions().map((option) => ( {generateTimeOptions().map((option) => (
@@ -144,7 +146,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
className="form-select form-select-sm" className="form-select form-select-sm"
value={times.availableBefore} value={times.availableBefore}
onChange={(e) => onChange={(e) =>
onWeeklyTimeChange(day, 'availableBefore', e.target.value) onWeeklyTimeChange(day, "availableBefore", e.target.value)
} }
> >
{generateTimeOptions().map((option) => ( {generateTimeOptions().map((option) => (

View File

@@ -1,17 +1,18 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import api from "../services/api"; import api, { addressAPI, userAPI, itemAPI } from "../services/api";
import AvailabilitySettings from "../components/AvailabilitySettings"; import AvailabilitySettings from "../components/AvailabilitySettings";
import { Address } from "../types";
interface ItemFormData { interface ItemFormData {
name: string; name: string;
description: string; description: string;
pickUpAvailable: boolean; pickUpAvailable: boolean;
inPlaceUseAvailable: boolean; inPlaceUseAvailable: boolean;
pricePerHour?: number; pricePerHour?: number | string;
pricePerDay?: number; pricePerDay?: number | string;
replacementCost: number; replacementCost: number | string;
location: string; location: string;
address1: string; address1: string;
address2: string; address2: string;
@@ -48,8 +49,8 @@ const CreateItem: React.FC = () => {
description: "", description: "",
pickUpAvailable: false, pickUpAvailable: false,
inPlaceUseAvailable: false, inPlaceUseAvailable: false,
pricePerDay: undefined, pricePerDay: "",
replacementCost: 0, replacementCost: "",
location: "", location: "",
address1: "", address1: "",
address2: "", address2: "",
@@ -75,6 +76,65 @@ const CreateItem: React.FC = () => {
const [imageFiles, setImageFiles] = useState<File[]>([]); const [imageFiles, setImageFiles] = useState<File[]>([]);
const [imagePreviews, setImagePreviews] = useState<string[]>([]); const [imagePreviews, setImagePreviews] = useState<string[]>([]);
const [priceType, setPriceType] = useState<"hour" | "day">("day"); const [priceType, setPriceType] = useState<"hour" | "day">("day");
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
const [addressesLoading, setAddressesLoading] = useState(true);
useEffect(() => {
fetchUserAddresses();
fetchUserAvailability();
}, []);
const fetchUserAvailability = async () => {
try {
const response = await userAPI.getAvailability();
const userAvailability = response.data;
setFormData(prev => ({
...prev,
generalAvailableAfter: userAvailability.generalAvailableAfter,
generalAvailableBefore: userAvailability.generalAvailableBefore,
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
weeklyTimes: userAvailability.weeklyTimes
}));
} catch (error) {
console.error('Error fetching user availability:', error);
// Use default values if fetch fails
}
};
useEffect(() => {
// Auto-populate if user has exactly one address and addresses have finished loading
if (
!addressesLoading &&
userAddresses.length === 1 &&
selectedAddressId === ""
) {
const address = userAddresses[0];
setFormData((prev) => ({
...prev,
address1: address.address1,
address2: address.address2 || "",
city: address.city,
state: address.state,
zipCode: address.zipCode,
country: address.country,
latitude: address.latitude,
longitude: address.longitude,
}));
setSelectedAddressId(address.id);
}
}, [userAddresses, addressesLoading]);
const fetchUserAddresses = async () => {
try {
const response = await addressAPI.getAddresses();
setUserAddresses(response.data);
} catch (error) {
console.error("Error fetching addresses:", error);
} finally {
setAddressesLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -105,9 +165,52 @@ const CreateItem: React.FC = () => {
const response = await api.post("/items", { const response = await api.post("/items", {
...formData, ...formData,
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
location, location,
images: imageUrls, images: imageUrls,
}); });
// Auto-save address if user has no addresses and entered manual address
if (userAddresses.length === 0 && formData.address1) {
try {
await addressAPI.createAddress({
address1: formData.address1,
address2: formData.address2,
city: formData.city,
state: formData.state,
zipCode: formData.zipCode,
country: formData.country,
latitude: formData.latitude,
longitude: formData.longitude,
isPrimary: true,
});
} catch (addressError) {
console.error("Failed to save address:", addressError);
// Don't fail item creation if address save fails
}
}
// Check if this is user's first item and save availability settings
try {
const userItemsResponse = await itemAPI.getItems({ owner: user.id });
const userItems = userItemsResponse.data.items || [];
// If this is their first item (the one we just created), save availability to user
if (userItems.length <= 1) {
await userAPI.updateAvailability({
generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes
});
}
} catch (availabilityError) {
console.error("Failed to save availability:", availabilityError);
// Don't fail item creation if availability save fails
}
navigate(`/items/${response.data.id}`); navigate(`/items/${response.data.id}`);
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || "Failed to create listing"); setError(err.response?.data?.error || "Failed to create listing");
@@ -129,13 +232,104 @@ const CreateItem: React.FC = () => {
} else if (type === "number") { } else if (type === "number") {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value ? parseFloat(value) : undefined, [name]: value === "" ? "" : parseFloat(value) || 0,
})); }));
} else { } else {
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
}; };
const handleAddressSelect = (addressId: string) => {
if (addressId === "new") {
// Clear form for new address entry
setFormData((prev) => ({
...prev,
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
}));
setSelectedAddressId("");
} else {
const selectedAddress = userAddresses.find(
(addr) => addr.id === addressId
);
if (selectedAddress) {
setFormData((prev) => ({
...prev,
address1: selectedAddress.address1,
address2: selectedAddress.address2 || "",
city: selectedAddress.city,
state: selectedAddress.state,
zipCode: selectedAddress.zipCode,
country: selectedAddress.country,
latitude: selectedAddress.latitude,
longitude: selectedAddress.longitude,
}));
setSelectedAddressId(addressId);
}
}
};
const formatAddressDisplay = (address: Address) => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
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",
];
const handleWeeklyTimeChange = ( const handleWeeklyTimeChange = (
day: string, day: string,
field: "availableAfter" | "availableBefore", field: "availableAfter" | "availableBefore",
@@ -153,7 +347,6 @@ const CreateItem: React.FC = () => {
})); }));
}; };
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
@@ -285,10 +478,47 @@ const CreateItem: React.FC = () => {
<div className="mb-3"> <div className="mb-3">
<small className="text-muted"> <small className="text-muted">
<i className="bi bi-info-circle me-2"></i> <i className="bi bi-info-circle me-2"></i>
Your address is private. This will only be used to show This address is private. It will only be used to show
renters a general area. renters a general area.
</small> </small>
</div> </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) => handleAddressSelect(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 */}
{(userAddresses.length <= 1 ||
(userAddresses.length > 1 && !selectedAddressId)) && (
<>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6">
<label htmlFor="address1" className="form-label"> <label htmlFor="address1" className="form-label">
@@ -301,7 +531,7 @@ const CreateItem: React.FC = () => {
name="address1" name="address1"
value={formData.address1} value={formData.address1}
onChange={handleChange} onChange={handleChange}
placeholder="123 Main Street" placeholder=""
required required
/> />
</div> </div>
@@ -340,16 +570,21 @@ const CreateItem: React.FC = () => {
<label htmlFor="state" className="form-label"> <label htmlFor="state" className="form-label">
State * State *
</label> </label>
<input <select
type="text" className="form-select"
className="form-control"
id="state" id="state"
name="state" name="state"
value={formData.state} value={formData.state}
onChange={handleChange} onChange={handleChange}
placeholder="CA"
required required
/> >
<option value="">Select State</option>
{usStates.map((state) => (
<option key={state} value={state}>
{state}
</option>
))}
</select>
</div> </div>
<div className="col-md-3"> <div className="col-md-3">
<label htmlFor="zipCode" className="form-label"> <label htmlFor="zipCode" className="form-label">
@@ -362,27 +597,15 @@ const CreateItem: React.FC = () => {
name="zipCode" name="zipCode"
value={formData.zipCode} value={formData.zipCode}
onChange={handleChange} onChange={handleChange}
placeholder="12345" placeholder=""
required required
/> />
</div> </div>
</div> </div>
</>
<div className="mb-3"> )}
<label htmlFor="country" className="form-label"> </>
Country * )}
</label>
<input
type="text"
className="form-control"
id="country"
name="country"
value={formData.country}
onChange={handleChange}
placeholder="United States"
required
/>
</div>
</div> </div>
</div> </div>
@@ -438,10 +661,10 @@ const CreateItem: React.FC = () => {
generalAvailableAfter: formData.generalAvailableAfter, generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore, generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay, specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes weeklyTimes: formData.weeklyTimes,
}} }}
onChange={(field, value) => { onChange={(field, value) => {
setFormData(prev => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}} }}
onWeeklyTimeChange={handleWeeklyTimeChange} onWeeklyTimeChange={handleWeeklyTimeChange}
/> />
@@ -532,6 +755,7 @@ const CreateItem: React.FC = () => {
onChange={handleChange} onChange={handleChange}
step="0.01" step="0.01"
min="0" min="0"
placeholder="0"
required required
/> />
</div> </div>

View File

@@ -14,9 +14,9 @@ interface ItemFormData {
localDeliveryRadius?: number; localDeliveryRadius?: number;
shippingAvailable: boolean; shippingAvailable: boolean;
inPlaceUseAvailable: boolean; inPlaceUseAvailable: boolean;
pricePerHour?: number; pricePerHour?: number | string;
pricePerDay?: number; pricePerDay?: number | string;
replacementCost: number; replacementCost: number | string;
location: string; location: string;
latitude?: number; latitude?: number;
longitude?: number; longitude?: number;
@@ -146,7 +146,7 @@ const EditItem: React.FC = () => {
} else if (type === "number") { } else if (type === "number") {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value ? parseFloat(value) : undefined, [name]: value === "" ? "" : parseFloat(value) || 0,
})); }));
} else { } else {
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
@@ -163,6 +163,9 @@ const EditItem: React.FC = () => {
await itemAPI.updateItem(id!, { await itemAPI.updateItem(id!, {
...formData, ...formData,
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
images: imageUrls, images: imageUrls,
}); });
@@ -519,9 +522,12 @@ const EditItem: React.FC = () => {
</div> </div>
<div className="mb-3"> <div className="mb-3">
<label htmlFor="replacementCost" className="form-label"> <label htmlFor="replacementCost" className="form-label mb-0">
Replacement Cost * Replacement Cost *
</label> </label>
<div className="form-text mb-2">
The cost to replace the item if lost
</div>
<div className="input-group"> <div className="input-group">
<span className="input-group-text">$</span> <span className="input-group-text">$</span>
<input <input
@@ -533,12 +539,10 @@ const EditItem: React.FC = () => {
onChange={handleChange} onChange={handleChange}
step="0.01" step="0.01"
min="0" min="0"
placeholder="0"
required required
/> />
</div> </div>
<div className="form-text">
The cost to replace the item if lost
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { userAPI, itemAPI, rentalAPI } from "../services/api"; import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api";
import { User, Item, Rental } from "../types"; import { User, Item, Rental, Address } from "../types";
import { getImageUrl } from "../utils/imageUrl"; import { getImageUrl } from "../utils/imageUrl";
import AvailabilitySettings from "../components/AvailabilitySettings"; import AvailabilitySettings from "../components/AvailabilitySettings";
@@ -11,7 +11,7 @@ const Profile: React.FC = () => {
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const [activeSection, setActiveSection] = useState<string>('overview'); const [activeSection, setActiveSection] = useState<string>("overview");
const [profileData, setProfileData] = useState<User | null>(null); const [profileData, setProfileData] = useState<User | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: "", firstName: "",
@@ -47,12 +47,46 @@ const Profile: React.FC = () => {
saturday: { availableAfter: "09:00", availableBefore: "17:00" }, saturday: { availableAfter: "09:00", availableBefore: "17:00" },
}, },
}); });
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [addressesLoading, setAddressesLoading] = useState(true);
const [showAddressForm, setShowAddressForm] = useState(false);
const [editingAddressId, setEditingAddressId] = useState<string | null>(null);
const [addressFormData, setAddressFormData] = useState({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
useEffect(() => { useEffect(() => {
fetchProfile(); fetchProfile();
fetchStats(); fetchStats();
fetchUserAddresses();
fetchUserAvailability();
}, []); }, []);
const fetchUserAvailability = async () => {
try {
const response = await userAPI.getAvailability();
setAvailabilityData(response.data);
} catch (error) {
console.error("Error fetching user availability:", error);
}
};
const fetchUserAddresses = async () => {
try {
const response = await addressAPI.getAddresses();
setUserAddresses(response.data);
} catch (error) {
console.error("Error fetching addresses:", error);
} finally {
setAddressesLoading(false);
}
};
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
const response = await userAPI.getProfile(); const response = await userAPI.getProfile();
@@ -213,22 +247,180 @@ const Profile: React.FC = () => {
}; };
const handleAvailabilityChange = (field: string, value: string | boolean) => { const handleAvailabilityChange = (field: string, value: string | boolean) => {
setAvailabilityData(prev => ({ ...prev, [field]: value })); setAvailabilityData((prev) => ({ ...prev, [field]: value }));
}; };
const handleWeeklyTimeChange = (day: string, field: 'availableAfter' | 'availableBefore', value: string) => { const handleWeeklyTimeChange = (
setAvailabilityData(prev => ({ day: string,
field: "availableAfter" | "availableBefore",
value: string
) => {
setAvailabilityData((prev) => ({
...prev, ...prev,
weeklyTimes: { weeklyTimes: {
...prev.weeklyTimes, ...prev.weeklyTimes,
[day]: { [day]: {
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes], ...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
[field]: value [field]: value,
} },
} },
})); }));
}; };
const handleSaveAvailability = async () => {
try {
await userAPI.updateAvailability(availabilityData);
setSuccess("Availability settings saved successfully");
setTimeout(() => setSuccess(null), 3000);
} catch (error) {
console.error("Error saving availability:", error);
setError("Failed to save availability settings");
}
};
const formatAddressDisplay = (address: Address) => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
const handleDeleteAddress = async (addressId: string) => {
try {
await addressAPI.deleteAddress(addressId);
setUserAddresses((prev) => prev.filter((addr) => addr.id !== addressId));
} catch (error) {
console.error("Error deleting address:", error);
setError("Failed to delete address");
}
};
const handleAddAddress = () => {
setAddressFormData({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
setEditingAddressId(null);
setShowAddressForm(true);
};
const handleEditAddress = (address: Address) => {
setAddressFormData({
address1: address.address1,
address2: address.address2 || "",
city: address.city,
state: address.state,
zipCode: address.zipCode,
country: address.country,
});
setEditingAddressId(address.id);
setShowAddressForm(true);
};
const handleAddressFormChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setAddressFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSaveAddress = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingAddressId) {
// Update existing address
const response = await addressAPI.updateAddress(
editingAddressId,
addressFormData
);
setUserAddresses((prev) =>
prev.map((addr) =>
addr.id === editingAddressId ? response.data : addr
)
);
} else {
// Create new address
const response = await addressAPI.createAddress({
...addressFormData,
isPrimary: userAddresses.length === 0,
});
setUserAddresses((prev) => [...prev, response.data]);
}
setShowAddressForm(false);
setEditingAddressId(null);
} catch (error) {
console.error("Error saving address:", error);
setError("Failed to save address");
}
};
const handleCancelAddressForm = () => {
setShowAddressForm(false);
setEditingAddressId(null);
setAddressFormData({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
};
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",
];
if (loading) { if (loading) {
return ( return (
<div className="container mt-5"> <div className="container mt-5">
@@ -263,43 +455,55 @@ const Profile: React.FC = () => {
<div className="card"> <div className="card">
<div className="list-group list-group-flush"> <div className="list-group list-group-flush">
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'overview' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('overview')} activeSection === "overview" ? "active" : ""
}`}
onClick={() => setActiveSection("overview")}
> >
<i className="bi bi-person-circle me-2"></i> <i className="bi bi-person-circle me-2"></i>
Overview Overview
</button> </button>
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'owner-settings' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('owner-settings')} activeSection === "owner-settings" ? "active" : ""
}`}
onClick={() => setActiveSection("owner-settings")}
> >
<i className="bi bi-gear me-2"></i> <i className="bi bi-gear me-2"></i>
Owner Settings Owner Settings
</button> </button>
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'personal-info' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('personal-info')} activeSection === "personal-info" ? "active" : ""
}`}
onClick={() => setActiveSection("personal-info")}
> >
<i className="bi bi-person me-2"></i> <i className="bi bi-person me-2"></i>
Personal Information Personal Information
</button> </button>
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'notifications' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('notifications')} activeSection === "notifications" ? "active" : ""
}`}
onClick={() => setActiveSection("notifications")}
> >
<i className="bi bi-bell me-2"></i> <i className="bi bi-bell me-2"></i>
Notification Settings Notification Settings
</button> </button>
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'privacy' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('privacy')} activeSection === "privacy" ? "active" : ""
}`}
onClick={() => setActiveSection("privacy")}
> >
<i className="bi bi-shield-lock me-2"></i> <i className="bi bi-shield-lock me-2"></i>
Privacy & Security Privacy & Security
</button> </button>
<button <button
className={`list-group-item list-group-item-action ${activeSection === 'payment' ? 'active' : ''}`} className={`list-group-item list-group-item-action ${
onClick={() => setActiveSection('payment')} activeSection === "payment" ? "active" : ""
}`}
onClick={() => setActiveSection("payment")}
> >
<i className="bi bi-credit-card me-2"></i> <i className="bi bi-credit-card me-2"></i>
Payment Methods Payment Methods
@@ -318,7 +522,7 @@ const Profile: React.FC = () => {
{/* Right Content Area */} {/* Right Content Area */}
<div className="col-md-9"> <div className="col-md-9">
{/* Overview Section */} {/* Overview Section */}
{activeSection === 'overview' && ( {activeSection === "overview" && (
<div> <div>
<h4 className="mb-4">Overview</h4> <h4 className="mb-4">Overview</h4>
@@ -354,7 +558,11 @@ const Profile: React.FC = () => {
<label <label
htmlFor="profileImageOverview" htmlFor="profileImageOverview"
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle" className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
style={{ width: "35px", height: "35px", padding: "0" }} style={{
width: "35px",
height: "35px",
padding: "0",
}}
> >
<i className="bi bi-camera-fill"></i> <i className="bi bi-camera-fill"></i>
<input <input
@@ -415,7 +623,8 @@ const Profile: React.FC = () => {
<p className="text-muted">@{profileData?.username}</p> <p className="text-muted">@{profileData?.username}</p>
{profileData?.isVerified && ( {profileData?.isVerified && (
<span className="badge bg-success mb-3"> <span className="badge bg-success mb-3">
<i className="bi bi-check-circle-fill"></i> Verified <i className="bi bi-check-circle-fill"></i>{" "}
Verified
</span> </span>
)} )}
<div> <div>
@@ -442,13 +651,17 @@ const Profile: React.FC = () => {
<div className="row text-center"> <div className="row text-center">
<div className="col-md-4"> <div className="col-md-4">
<div className="p-3"> <div className="p-3">
<h4 className="text-primary mb-1">{stats.itemsListed}</h4> <h4 className="text-primary mb-1">
{stats.itemsListed}
</h4>
<h6 className="text-muted">Items Listed</h6> <h6 className="text-muted">Items Listed</h6>
</div> </div>
</div> </div>
<div className="col-md-4"> <div className="col-md-4">
<div className="p-3"> <div className="p-3">
<h4 className="text-success mb-1">{stats.acceptedRentals}</h4> <h4 className="text-success mb-1">
{stats.acceptedRentals}
</h4>
<h6 className="text-muted">Active Rentals</h6> <h6 className="text-muted">Active Rentals</h6>
</div> </div>
</div> </div>
@@ -465,7 +678,7 @@ const Profile: React.FC = () => {
)} )}
{/* Personal Information Section */} {/* Personal Information Section */}
{activeSection === 'personal-info' && ( {activeSection === "personal-info" && (
<div> <div>
<h4 className="mb-4">Personal Information</h4> <h4 className="mb-4">Personal Information</h4>
<div className="card"> <div className="card">
@@ -531,70 +744,278 @@ const Profile: React.FC = () => {
)} )}
{/* Owner Settings Section */} {/* Owner Settings Section */}
{activeSection === 'owner-settings' && ( {activeSection === "owner-settings" && (
<div> <div>
<h4 className="mb-4">Owner Settings</h4> <h4 className="mb-4">Owner Settings</h4>
<div className="card">
{/* Addresses Card */}
<div className="card mb-4">
<div className="card-body"> <div className="card-body">
{/* Addresses Section */} <h5 className="card-title">Saved Addresses</h5>
<div className="mb-5">
<h5 className="mb-3">Saved Addresses</h5> {addressesLoading ? (
<p className="text-muted small mb-3">Manage addresses for your rental locations</p> <div className="text-center py-3">
<button className="btn btn-outline-primary"> <div
<i className="bi bi-plus-circle me-2"></i> className="spinner-border spinner-border-sm"
role="status"
>
<span className="visually-hidden">
Loading addresses...
</span>
</div>
</div>
) : (
<>
{userAddresses.length === 0 && !showAddressForm ? (
<div className="text-center py-3">
<p className="text-muted">No saved addresses yet</p>
<small className="text-muted">
Add an address or create your first listing to save
one automatically
</small>
</div>
) : (
<>
{userAddresses.length > 0 && !showAddressForm && (
<>
<div className="list-group list-group-flush mb-3">
{userAddresses.map((address) => (
<div
key={address.id}
className="list-group-item d-flex justify-content-between align-items-start"
>
<div className="flex-grow-1">
<div className="fw-medium">
{formatAddressDisplay(address)}
</div>
{address.address2 && (
<small className="text-muted">
{address.address2}
</small>
)}
</div>
<div className="btn-group">
<button
className="btn btn-outline-secondary btn-sm"
onClick={() =>
handleEditAddress(address)
}
>
<i className="bi bi-pencil"></i>
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={() =>
handleDeleteAddress(address.id)
}
>
<i className="bi bi-trash"></i>
</button>
</div>
</div>
))}
</div>
<button
className="btn btn-outline-primary"
onClick={handleAddAddress}
>
Add New Address
</button>
</>
)}
</>
)}
{/* Show Add New Address button even when no addresses exist */}
{userAddresses.length === 0 && !showAddressForm && (
<div className="text-center">
<button
className="btn btn-outline-primary"
onClick={handleAddAddress}
>
Add New Address Add New Address
</button> </button>
</div> </div>
)}
{/* Availability Section */} {/* Address Form */}
<div> {showAddressForm && (
<h5 className="mb-3">Default Availability</h5> <form onSubmit={handleSaveAddress}>
<p className="text-muted small mb-3">Set your general availability for all items</p> <div className="row mb-3">
<div className="col-md-6">
<label
htmlFor="addressFormAddress1"
className="form-label"
>
Address Line 1 *
</label>
<input
type="text"
className="form-control"
id="addressFormAddress1"
name="address1"
value={addressFormData.address1}
onChange={handleAddressFormChange}
placeholder=""
required
/>
</div>
<div className="col-md-6">
<label
htmlFor="addressFormAddress2"
className="form-label"
>
Address Line 2
</label>
<input
type="text"
className="form-control"
id="addressFormAddress2"
name="address2"
value={addressFormData.address2}
onChange={handleAddressFormChange}
placeholder="Apt, Suite, Unit, etc."
/>
</div>
</div>
<div className="row mb-3">
<div className="col-md-6">
<label
htmlFor="addressFormCity"
className="form-label"
>
City *
</label>
<input
type="text"
className="form-control"
id="addressFormCity"
name="city"
value={addressFormData.city}
onChange={handleAddressFormChange}
required
/>
</div>
<div className="col-md-3">
<label
htmlFor="addressFormState"
className="form-label"
>
State *
</label>
<select
className="form-select"
id="addressFormState"
name="state"
value={addressFormData.state}
onChange={handleAddressFormChange}
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="addressFormZipCode"
className="form-label"
>
ZIP Code *
</label>
<input
type="text"
className="form-control"
id="addressFormZipCode"
name="zipCode"
value={addressFormData.zipCode}
onChange={handleAddressFormChange}
placeholder="12345"
required
/>
</div>
</div>
<div className="d-flex gap-2">
<button type="submit" className="btn btn-primary">
{editingAddressId
? "Update Address"
: "Save Address"}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancelAddressForm}
>
Cancel
</button>
</div>
</form>
)}
</>
)}
</div>
</div>
{/* Availability Card */}
<div className="card">
<div className="card-body">
<h5 className="card-title">Availability</h5>
<AvailabilitySettings <AvailabilitySettings
data={availabilityData} data={availabilityData}
onChange={handleAvailabilityChange} onChange={handleAvailabilityChange}
onWeeklyTimeChange={handleWeeklyTimeChange} onWeeklyTimeChange={handleWeeklyTimeChange}
showTitle={false}
/> />
<button className="btn btn-outline-success mt-3"> <button
<i className="bi bi-check2 me-2"></i> className="btn btn-outline-success mt-3"
onClick={handleSaveAvailability}
>
Save Availability Save Availability
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div>
)} )}
{/* Placeholder sections for other menu items */} {/* Placeholder sections for other menu items */}
{activeSection === 'notifications' && ( {activeSection === "notifications" && (
<div> <div>
<h4 className="mb-4">Notification Settings</h4> <h4 className="mb-4">Notification Settings</h4>
<div className="card"> <div className="card">
<div className="card-body"> <div className="card-body">
<p className="text-muted">Notification preferences coming soon...</p> <p className="text-muted">
Notification preferences coming soon...
</p>
</div> </div>
</div> </div>
</div> </div>
)} )}
{activeSection === 'privacy' && ( {activeSection === "privacy" && (
<div> <div>
<h4 className="mb-4">Privacy & Security</h4> <h4 className="mb-4">Privacy & Security</h4>
<div className="card"> <div className="card">
<div className="card-body"> <div className="card-body">
<p className="text-muted">Privacy and security settings coming soon...</p> <p className="text-muted">
Privacy and security settings coming soon...
</p>
</div> </div>
</div> </div>
</div> </div>
)} )}
{activeSection === 'payment' && ( {activeSection === "payment" && (
<div> <div>
<h4 className="mb-4">Payment Methods</h4> <h4 className="mb-4">Payment Methods</h4>
<div className="card"> <div className="card">
<div className="card-body"> <div className="card-body">
<p className="text-muted">Payment method management coming soon...</p> <p className="text-muted">
Payment method management coming soon...
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -51,6 +51,15 @@ export const userAPI = {
}, },
}), }),
getPublicProfile: (id: string) => api.get(`/users/${id}`), getPublicProfile: (id: string) => api.get(`/users/${id}`),
getAvailability: () => api.get("/users/availability"),
updateAvailability: (data: any) => api.put("/users/availability", data),
};
export const addressAPI = {
getAddresses: () => api.get("/users/addresses"),
createAddress: (data: any) => api.post("/users/addresses", data),
updateAddress: (id: string, data: any) => api.put(`/users/addresses/${id}`, data),
deleteAddress: (id: string) => api.delete(`/users/addresses/${id}`),
}; };
export const itemAPI = { export const itemAPI = {

View File

@@ -1,3 +1,19 @@
export interface Address {
id: string;
userId: string;
address1: string;
address2?: string;
city: string;
state: string;
zipCode: string;
country: string;
latitude?: number;
longitude?: number;
isPrimary: boolean;
createdAt: string;
updatedAt: string;
}
export interface User { export interface User {
id: string; id: string;
username: string; username: string;
@@ -13,6 +29,7 @@ export interface User {
country?: string; country?: string;
profileImage?: string; profileImage?: string;
isVerified: boolean; isVerified: boolean;
addresses?: Address[];
} }
export interface Message { export interface Message {