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: {
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: {

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

View File

@@ -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, {

View File

@@ -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<AvailabilitySettingsProps> = ({
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<HTMLSelectElement | HTMLInputElement>) => {
const handleGeneralChange = (
e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>
) => {
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<AvailabilitySettingsProps> = ({
return (
<div>
{showTitle && <h5 className="card-title">Availability</h5>}
{/* General Times */}
<div className="row mb-3">
<div className="col-md-6">
@@ -129,7 +131,7 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
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<AvailabilitySettingsProps> = ({
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) => (

View File

@@ -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<File[]>([]);
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
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) => {
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<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
@@ -285,10 +478,47 @@ const CreateItem: React.FC = () => {
<div className="mb-3">
<small className="text-muted">
<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.
</small>
</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="col-md-6">
<label htmlFor="address1" className="form-label">
@@ -301,7 +531,7 @@ const CreateItem: React.FC = () => {
name="address1"
value={formData.address1}
onChange={handleChange}
placeholder="123 Main Street"
placeholder=""
required
/>
</div>
@@ -340,16 +570,21 @@ const CreateItem: React.FC = () => {
<label htmlFor="state" className="form-label">
State *
</label>
<input
type="text"
className="form-control"
<select
className="form-select"
id="state"
name="state"
value={formData.state}
onChange={handleChange}
placeholder="CA"
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="zipCode" className="form-label">
@@ -362,27 +597,15 @@ const CreateItem: React.FC = () => {
name="zipCode"
value={formData.zipCode}
onChange={handleChange}
placeholder="12345"
placeholder=""
required
/>
</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>
@@ -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
/>
</div>

View File

@@ -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 = () => {
</div>
<div className="mb-3">
<label htmlFor="replacementCost" className="form-label">
<label htmlFor="replacementCost" className="form-label mb-0">
Replacement Cost *
</label>
<div className="form-text mb-2">
The cost to replace the item if lost
</div>
<div className="input-group">
<span className="input-group-text">$</span>
<input
@@ -533,12 +539,10 @@ const EditItem: React.FC = () => {
onChange={handleChange}
step="0.01"
min="0"
placeholder="0"
required
/>
</div>
<div className="form-text">
The cost to replace the item if lost
</div>
</div>
</div>
</div>

View File

@@ -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<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 [formData, setFormData] = useState({
firstName: "",
@@ -47,12 +47,46 @@ const Profile: React.FC = () => {
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(() => {
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<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) {
return (
<div className="container mt-5">
@@ -263,43 +455,55 @@ const Profile: React.FC = () => {
<div className="card">
<div className="list-group list-group-flush">
<button
className={`list-group-item list-group-item-action ${activeSection === 'overview' ? 'active' : ''}`}
onClick={() => setActiveSection('overview')}
className={`list-group-item list-group-item-action ${
activeSection === "overview" ? "active" : ""
}`}
onClick={() => setActiveSection("overview")}
>
<i className="bi bi-person-circle me-2"></i>
Overview
</button>
<button
className={`list-group-item list-group-item-action ${activeSection === 'owner-settings' ? 'active' : ''}`}
onClick={() => setActiveSection('owner-settings')}
className={`list-group-item list-group-item-action ${
activeSection === "owner-settings" ? "active" : ""
}`}
onClick={() => setActiveSection("owner-settings")}
>
<i className="bi bi-gear me-2"></i>
Owner Settings
</button>
<button
className={`list-group-item list-group-item-action ${activeSection === 'personal-info' ? 'active' : ''}`}
onClick={() => setActiveSection('personal-info')}
className={`list-group-item list-group-item-action ${
activeSection === "personal-info" ? "active" : ""
}`}
onClick={() => setActiveSection("personal-info")}
>
<i className="bi bi-person me-2"></i>
Personal Information
</button>
<button
className={`list-group-item list-group-item-action ${activeSection === 'notifications' ? 'active' : ''}`}
onClick={() => setActiveSection('notifications')}
className={`list-group-item list-group-item-action ${
activeSection === "notifications" ? "active" : ""
}`}
onClick={() => setActiveSection("notifications")}
>
<i className="bi bi-bell me-2"></i>
Notification Settings
</button>
<button
className={`list-group-item list-group-item-action ${activeSection === 'privacy' ? 'active' : ''}`}
onClick={() => setActiveSection('privacy')}
className={`list-group-item list-group-item-action ${
activeSection === "privacy" ? "active" : ""
}`}
onClick={() => setActiveSection("privacy")}
>
<i className="bi bi-shield-lock me-2"></i>
Privacy & Security
</button>
<button
className={`list-group-item list-group-item-action ${activeSection === 'payment' ? 'active' : ''}`}
onClick={() => setActiveSection('payment')}
className={`list-group-item list-group-item-action ${
activeSection === "payment" ? "active" : ""
}`}
onClick={() => setActiveSection("payment")}
>
<i className="bi bi-credit-card me-2"></i>
Payment Methods
@@ -318,7 +522,7 @@ const Profile: React.FC = () => {
{/* Right Content Area */}
<div className="col-md-9">
{/* Overview Section */}
{activeSection === 'overview' && (
{activeSection === "overview" && (
<div>
<h4 className="mb-4">Overview</h4>
@@ -354,7 +558,11 @@ const Profile: React.FC = () => {
<label
htmlFor="profileImageOverview"
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>
<input
@@ -415,7 +623,8 @@ const Profile: React.FC = () => {
<p className="text-muted">@{profileData?.username}</p>
{profileData?.isVerified && (
<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>
)}
<div>
@@ -442,13 +651,17 @@ const Profile: React.FC = () => {
<div className="row text-center">
<div className="col-md-4">
<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>
</div>
</div>
<div className="col-md-4">
<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>
</div>
</div>
@@ -465,7 +678,7 @@ const Profile: React.FC = () => {
)}
{/* Personal Information Section */}
{activeSection === 'personal-info' && (
{activeSection === "personal-info" && (
<div>
<h4 className="mb-4">Personal Information</h4>
<div className="card">
@@ -531,70 +744,278 @@ const Profile: React.FC = () => {
)}
{/* Owner Settings Section */}
{activeSection === 'owner-settings' && (
{activeSection === "owner-settings" && (
<div>
<h4 className="mb-4">Owner Settings</h4>
<div className="card">
{/* Addresses Card */}
<div className="card mb-4">
<div className="card-body">
{/* Addresses Section */}
<div className="mb-5">
<h5 className="mb-3">Saved Addresses</h5>
<p className="text-muted small mb-3">Manage addresses for your rental locations</p>
<button className="btn btn-outline-primary">
<i className="bi bi-plus-circle me-2"></i>
<h5 className="card-title">Saved Addresses</h5>
{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>
) : (
<>
{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
</button>
</div>
)}
{/* Availability Section */}
<div>
<h5 className="mb-3">Default Availability</h5>
<p className="text-muted small mb-3">Set your general availability for all items</p>
{/* Address Form */}
{showAddressForm && (
<form onSubmit={handleSaveAddress}>
<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
data={availabilityData}
onChange={handleAvailabilityChange}
onWeeklyTimeChange={handleWeeklyTimeChange}
showTitle={false}
/>
<button className="btn btn-outline-success mt-3">
<i className="bi bi-check2 me-2"></i>
<button
className="btn btn-outline-success mt-3"
onClick={handleSaveAvailability}
>
Save Availability
</button>
</div>
</div>
</div>
</div>
)}
{/* Placeholder sections for other menu items */}
{activeSection === 'notifications' && (
{activeSection === "notifications" && (
<div>
<h4 className="mb-4">Notification Settings</h4>
<div className="card">
<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>
)}
{activeSection === 'privacy' && (
{activeSection === "privacy" && (
<div>
<h4 className="mb-4">Privacy & Security</h4>
<div className="card">
<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>
)}
{activeSection === 'payment' && (
{activeSection === "payment" && (
<div>
<h4 className="mb-4">Payment Methods</h4>
<div className="card">
<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>

View File

@@ -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 = {

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 {
id: string;
username: string;
@@ -13,6 +29,7 @@ export interface User {
country?: string;
profileImage?: string;
isVerified: boolean;
addresses?: Address[];
}
export interface Message {