diff --git a/backend/models/User.js b/backend/models/User.js index e47938c..248ac72 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -74,6 +74,30 @@ const User = sequelize.define('User', { isVerified: { type: DataTypes.BOOLEAN, 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: { diff --git a/backend/models/UserAddress.js b/backend/models/UserAddress.js new file mode 100644 index 0000000..ee7a39d --- /dev/null +++ b/backend/models/UserAddress.js @@ -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; \ No newline at end of file diff --git a/backend/models/index.js b/backend/models/index.js index 46af393..1cb26d0 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -5,6 +5,7 @@ const Rental = require('./Rental'); const Message = require('./Message'); const ItemRequest = require('./ItemRequest'); const ItemRequestResponse = require('./ItemRequestResponse'); +const UserAddress = require('./UserAddress'); User.hasMany(Item, { as: 'ownedItems', 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(Item, { as: 'existingItem', foreignKey: 'existingItemId' }); +User.hasMany(UserAddress, { as: 'addresses', foreignKey: 'userId' }); +UserAddress.belongsTo(User, { as: 'user', foreignKey: 'userId' }); + module.exports = { sequelize, User, @@ -40,5 +44,6 @@ module.exports = { Rental, Message, ItemRequest, - ItemRequestResponse + ItemRequestResponse, + UserAddress }; \ No newline at end of file diff --git a/backend/routes/users.js b/backend/routes/users.js index c2295ab..d26083e 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -1,5 +1,5 @@ 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 { uploadProfileImage } = require('../middleware/upload'); 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) => { try { const user = await User.findByPk(req.params.id, { diff --git a/frontend/src/components/AvailabilitySettings.tsx b/frontend/src/components/AvailabilitySettings.tsx index e5f1332..4c2b222 100644 --- a/frontend/src/components/AvailabilitySettings.tsx +++ b/frontend/src/components/AvailabilitySettings.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface AvailabilityData { generalAvailableAfter: string; @@ -18,31 +18,35 @@ interface AvailabilityData { interface AvailabilitySettingsProps { data: AvailabilityData; onChange: (field: string, value: string | boolean) => void; - onWeeklyTimeChange: (day: string, field: 'availableAfter' | 'availableBefore', value: string) => void; - showTitle?: boolean; + onWeeklyTimeChange: ( + day: string, + field: "availableAfter" | "availableBefore", + value: string + ) => void; } const AvailabilitySettings: React.FC = ({ data, onChange, onWeeklyTimeChange, - showTitle = true }) => { const generateTimeOptions = () => { const options = []; 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 period = hour < 12 ? 'AM' : 'PM'; + const period = hour < 12 ? "AM" : "PM"; const time12 = `${hour12}:00 ${period}`; options.push({ value: time24, label: time12 }); } return options; }; - const handleGeneralChange = (e: React.ChangeEvent) => { + const handleGeneralChange = ( + e: React.ChangeEvent + ) => { const { name, value, type } = e.target; - if (type === 'checkbox') { + if (type === "checkbox") { const checked = (e.target as HTMLInputElement).checked; onChange(name, checked); } else { @@ -52,8 +56,6 @@ const AvailabilitySettings: React.FC = ({ return (
- {showTitle &&
Availability
} - {/* General Times */}
@@ -129,7 +131,7 @@ const AvailabilitySettings: React.FC = ({ className="form-select form-select-sm" value={times.availableAfter} onChange={(e) => - onWeeklyTimeChange(day, 'availableAfter', e.target.value) + onWeeklyTimeChange(day, "availableAfter", e.target.value) } > {generateTimeOptions().map((option) => ( @@ -144,7 +146,7 @@ const AvailabilitySettings: React.FC = ({ className="form-select form-select-sm" value={times.availableBefore} onChange={(e) => - onWeeklyTimeChange(day, 'availableBefore', e.target.value) + onWeeklyTimeChange(day, "availableBefore", e.target.value) } > {generateTimeOptions().map((option) => ( @@ -162,4 +164,4 @@ const AvailabilitySettings: React.FC = ({ ); }; -export default AvailabilitySettings; \ No newline at end of file +export default AvailabilitySettings; diff --git a/frontend/src/pages/CreateItem.tsx b/frontend/src/pages/CreateItem.tsx index 4fe6258..ecfd788 100644 --- a/frontend/src/pages/CreateItem.tsx +++ b/frontend/src/pages/CreateItem.tsx @@ -1,17 +1,18 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useAuth } from "../contexts/AuthContext"; -import api from "../services/api"; +import api, { addressAPI, userAPI, itemAPI } from "../services/api"; import AvailabilitySettings from "../components/AvailabilitySettings"; +import { Address } from "../types"; interface ItemFormData { name: string; description: string; pickUpAvailable: boolean; inPlaceUseAvailable: boolean; - pricePerHour?: number; - pricePerDay?: number; - replacementCost: number; + pricePerHour?: number | string; + pricePerDay?: number | string; + replacementCost: number | string; location: string; address1: string; address2: string; @@ -48,8 +49,8 @@ const CreateItem: React.FC = () => { description: "", pickUpAvailable: false, inPlaceUseAvailable: false, - pricePerDay: undefined, - replacementCost: 0, + pricePerDay: "", + replacementCost: "", location: "", address1: "", address2: "", @@ -75,6 +76,65 @@ const CreateItem: React.FC = () => { const [imageFiles, setImageFiles] = useState([]); const [imagePreviews, setImagePreviews] = useState([]); const [priceType, setPriceType] = useState<"hour" | "day">("day"); + const [userAddresses, setUserAddresses] = useState([]); + const [selectedAddressId, setSelectedAddressId] = useState(""); + 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) => { e.preventDefault(); @@ -105,9 +165,52 @@ const CreateItem: React.FC = () => { const response = await api.post("/items", { ...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, 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}`); } catch (err: any) { setError(err.response?.data?.error || "Failed to create listing"); @@ -129,13 +232,104 @@ const CreateItem: React.FC = () => { } else if (type === "number") { setFormData((prev) => ({ ...prev, - [name]: value ? parseFloat(value) : undefined, + [name]: value === "" ? "" : parseFloat(value) || 0, })); } else { 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 = ( day: string, field: "availableAfter" | "availableBefore", @@ -153,7 +347,6 @@ const CreateItem: React.FC = () => { })); }; - const handleImageChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); @@ -285,104 +478,134 @@ const CreateItem: React.FC = () => {
- 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.
-
-
- - -
-
- - -
-
-
-
- - + {addressesLoading ? ( +
+
+ + Loading addresses... + +
-
- - -
-
- - -
-
+ ) : ( + <> + {/* Multiple addresses - show dropdown */} + {userAddresses.length > 1 && ( +
+ + +
+ )} -
- - -
+ {/* Show form fields for all scenarios */} + {(userAddresses.length <= 1 || + (userAddresses.length > 1 && !selectedAddressId)) && ( + <> +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + )} + + )}
@@ -438,10 +661,10 @@ const CreateItem: React.FC = () => { generalAvailableAfter: formData.generalAvailableAfter, generalAvailableBefore: formData.generalAvailableBefore, specifyTimesPerDay: formData.specifyTimesPerDay, - weeklyTimes: formData.weeklyTimes + weeklyTimes: formData.weeklyTimes, }} onChange={(field, value) => { - setFormData(prev => ({ ...prev, [field]: value })); + setFormData((prev) => ({ ...prev, [field]: value })); }} onWeeklyTimeChange={handleWeeklyTimeChange} /> @@ -532,6 +755,7 @@ const CreateItem: React.FC = () => { onChange={handleChange} step="0.01" min="0" + placeholder="0" required />
diff --git a/frontend/src/pages/EditItem.tsx b/frontend/src/pages/EditItem.tsx index 64cc387..5f7d4e3 100644 --- a/frontend/src/pages/EditItem.tsx +++ b/frontend/src/pages/EditItem.tsx @@ -14,9 +14,9 @@ interface ItemFormData { localDeliveryRadius?: number; shippingAvailable: boolean; inPlaceUseAvailable: boolean; - pricePerHour?: number; - pricePerDay?: number; - replacementCost: number; + pricePerHour?: number | string; + pricePerDay?: number | string; + replacementCost: number | string; location: string; latitude?: number; longitude?: number; @@ -146,7 +146,7 @@ const EditItem: React.FC = () => { } else if (type === "number") { setFormData((prev) => ({ ...prev, - [name]: value ? parseFloat(value) : undefined, + [name]: value === "" ? "" : parseFloat(value) || 0, })); } else { setFormData((prev) => ({ ...prev, [name]: value })); @@ -163,6 +163,9 @@ const EditItem: React.FC = () => { await itemAPI.updateItem(id!, { ...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, }); @@ -519,9 +522,12 @@ const EditItem: React.FC = () => {
-
diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index c7681d7..673ba65 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { useAuth } from "../contexts/AuthContext"; -import { userAPI, itemAPI, rentalAPI } from "../services/api"; -import { User, Item, Rental } from "../types"; +import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api"; +import { User, Item, Rental, Address } from "../types"; import { getImageUrl } from "../utils/imageUrl"; import AvailabilitySettings from "../components/AvailabilitySettings"; @@ -11,7 +11,7 @@ const Profile: React.FC = () => { const [editing, setEditing] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); - const [activeSection, setActiveSection] = useState('overview'); + const [activeSection, setActiveSection] = useState("overview"); const [profileData, setProfileData] = useState(null); const [formData, setFormData] = useState({ firstName: "", @@ -47,12 +47,46 @@ const Profile: React.FC = () => { saturday: { availableAfter: "09:00", availableBefore: "17:00" }, }, }); + const [userAddresses, setUserAddresses] = useState([]); + const [addressesLoading, setAddressesLoading] = useState(true); + const [showAddressForm, setShowAddressForm] = useState(false); + const [editingAddressId, setEditingAddressId] = useState(null); + const [addressFormData, setAddressFormData] = useState({ + address1: "", + address2: "", + city: "", + state: "", + zipCode: "", + country: "US", + }); useEffect(() => { fetchProfile(); 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 () => { try { const response = await userAPI.getProfile(); @@ -213,22 +247,180 @@ const Profile: React.FC = () => { }; 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) => { - setAvailabilityData(prev => ({ + const handleWeeklyTimeChange = ( + day: string, + field: "availableAfter" | "availableBefore", + value: string + ) => { + setAvailabilityData((prev) => ({ ...prev, weeklyTimes: { ...prev.weeklyTimes, [day]: { ...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 + ) => { + 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) { return (
@@ -244,7 +436,7 @@ const Profile: React.FC = () => { return (

Profile

- + {error && (
{error} @@ -263,43 +455,55 @@ const Profile: React.FC = () => {
- -
-
- ) : ( -
-
- {profileData?.firstName} {profileData?.lastName} -
-

@{profileData?.username}

- {profileData?.isVerified && ( - - Verified - - )} -
- -
-
+
+ + +
+
+ ) : ( +
+
+ {profileData?.firstName} {profileData?.lastName} +
+

@{profileData?.username}

+ {profileData?.isVerified && ( + + {" "} + Verified + )} +
+ +
- + )}
-
- - {/* Stats Card */} -
-
-
Account Statistics
-
-
-
-

{stats.itemsListed}

-
Items Listed
-
-
-
-
-

{stats.acceptedRentals}

-
Active Rentals
-
-
-
-
-

{stats.totalRentals}

-
Total Rentals
-
-
+ +
+
+ + {/* Stats Card */} +
+
+
Account Statistics
+
+
+
+

+ {stats.itemsListed} +

+
Items Listed
+
+
+
+
+

+ {stats.acceptedRentals} +

+
Active Rentals
+
+
+
+
+

{stats.totalRentals}

+
Total Rentals
+
+
)} {/* Personal Information Section */} - {activeSection === 'personal-info' && ( + {activeSection === "personal-info" && (

Personal Information

@@ -531,70 +744,278 @@ const Profile: React.FC = () => { )} {/* Owner Settings Section */} - {activeSection === 'owner-settings' && ( + {activeSection === "owner-settings" && (

Owner Settings

+ + {/* Addresses Card */} +
+
+
Saved Addresses
+ + {addressesLoading ? ( +
+
+ + Loading addresses... + +
+
+ ) : ( + <> + {userAddresses.length === 0 && !showAddressForm ? ( +
+

No saved addresses yet

+ + Add an address or create your first listing to save + one automatically + +
+ ) : ( + <> + {userAddresses.length > 0 && !showAddressForm && ( + <> +
+ {userAddresses.map((address) => ( +
+
+
+ {formatAddressDisplay(address)} +
+ {address.address2 && ( + + {address.address2} + + )} +
+
+ + +
+
+ ))} +
+ + + )} + + )} + + {/* Show Add New Address button even when no addresses exist */} + {userAddresses.length === 0 && !showAddressForm && ( +
+ +
+ )} + + {/* Address Form */} + {showAddressForm && ( +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ )} + + )} +
+
+ + {/* Availability Card */}
- {/* Addresses Section */} -
-
Saved Addresses
-

Manage addresses for your rental locations

- -
- - {/* Availability Section */} -
-
Default Availability
-

Set your general availability for all items

- - -
+
Availability
+ +
)} {/* Placeholder sections for other menu items */} - {activeSection === 'notifications' && ( + {activeSection === "notifications" && (

Notification Settings

-

Notification preferences coming soon...

+

+ Notification preferences coming soon... +

)} - {activeSection === 'privacy' && ( + {activeSection === "privacy" && (

Privacy & Security

-

Privacy and security settings coming soon...

+

+ Privacy and security settings coming soon... +

)} - {activeSection === 'payment' && ( + {activeSection === "payment" && (

Payment Methods

-

Payment method management coming soon...

+

+ Payment method management coming soon... +

diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 19c2de7..9cc4870 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -51,6 +51,15 @@ export const userAPI = { }, }), 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 = { diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 43e4bce..fdcdfe3 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -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 { id: string; username: string; @@ -13,6 +29,7 @@ export interface User { country?: string; profileImage?: string; isVerified: boolean; + addresses?: Address[]; } export interface Message {