streamlined address and availability
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
54
backend/models/UserAddress.js
Normal file
54
backend/models/UserAddress.js
Normal 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;
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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) => (
|
||||||
@@ -162,4 +164,4 @@ const AvailabilitySettings: React.FC<AvailabilitySettingsProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AvailabilitySettings;
|
export default AvailabilitySettings;
|
||||||
|
|||||||
@@ -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,104 +478,134 @@ 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>
|
||||||
<div className="row mb-3">
|
|
||||||
<div className="col-md-6">
|
|
||||||
<label htmlFor="address1" className="form-label">
|
|
||||||
Address Line 1 *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="address1"
|
|
||||||
name="address1"
|
|
||||||
value={formData.address1}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="123 Main Street"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6">
|
|
||||||
<label htmlFor="address2" className="form-label">
|
|
||||||
Address Line 2
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="address2"
|
|
||||||
name="address2"
|
|
||||||
value={formData.address2}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Apt, Suite, Unit, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="row mb-3">
|
{addressesLoading ? (
|
||||||
<div className="col-md-6">
|
<div className="text-center py-3">
|
||||||
<label htmlFor="city" className="form-label">
|
<div
|
||||||
City *
|
className="spinner-border spinner-border-sm"
|
||||||
</label>
|
role="status"
|
||||||
<input
|
>
|
||||||
type="text"
|
<span className="visually-hidden">
|
||||||
className="form-control"
|
Loading addresses...
|
||||||
id="city"
|
</span>
|
||||||
name="city"
|
</div>
|
||||||
value={formData.city}
|
|
||||||
onChange={handleChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-3">
|
) : (
|
||||||
<label htmlFor="state" className="form-label">
|
<>
|
||||||
State *
|
{/* Multiple addresses - show dropdown */}
|
||||||
</label>
|
{userAddresses.length > 1 && (
|
||||||
<input
|
<div className="mb-3">
|
||||||
type="text"
|
<label className="form-label">Select Address</label>
|
||||||
className="form-control"
|
<select
|
||||||
id="state"
|
className="form-select"
|
||||||
name="state"
|
value={selectedAddressId || "new"}
|
||||||
value={formData.state}
|
onChange={(e) => handleAddressSelect(e.target.value)}
|
||||||
onChange={handleChange}
|
>
|
||||||
placeholder="CA"
|
<option value="new">Enter new address</option>
|
||||||
required
|
{userAddresses.map((address) => (
|
||||||
/>
|
<option key={address.id} value={address.id}>
|
||||||
</div>
|
{formatAddressDisplay(address)}
|
||||||
<div className="col-md-3">
|
</option>
|
||||||
<label htmlFor="zipCode" className="form-label">
|
))}
|
||||||
ZIP Code *
|
</select>
|
||||||
</label>
|
</div>
|
||||||
<input
|
)}
|
||||||
type="text"
|
|
||||||
className="form-control"
|
|
||||||
id="zipCode"
|
|
||||||
name="zipCode"
|
|
||||||
value={formData.zipCode}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="12345"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
{/* Show form fields for all scenarios */}
|
||||||
<label htmlFor="country" className="form-label">
|
{(userAddresses.length <= 1 ||
|
||||||
Country *
|
(userAddresses.length > 1 && !selectedAddressId)) && (
|
||||||
</label>
|
<>
|
||||||
<input
|
<div className="row mb-3">
|
||||||
type="text"
|
<div className="col-md-6">
|
||||||
className="form-control"
|
<label htmlFor="address1" className="form-label">
|
||||||
id="country"
|
Address Line 1 *
|
||||||
name="country"
|
</label>
|
||||||
value={formData.country}
|
<input
|
||||||
onChange={handleChange}
|
type="text"
|
||||||
placeholder="United States"
|
className="form-control"
|
||||||
required
|
id="address1"
|
||||||
/>
|
name="address1"
|
||||||
</div>
|
value={formData.address1}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder=""
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address2" className="form-label">
|
||||||
|
Address Line 2
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address2"
|
||||||
|
name="address2"
|
||||||
|
value={formData.address2}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Apt, Suite, Unit, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="city" className="form-label">
|
||||||
|
City *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="city"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="state" className="form-label">
|
||||||
|
State *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
id="state"
|
||||||
|
name="state"
|
||||||
|
value={formData.state}
|
||||||
|
onChange={handleChange}
|
||||||
|
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">
|
||||||
|
ZIP Code *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="zipCode"
|
||||||
|
name="zipCode"
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder=""
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -244,7 +436,7 @@ const Profile: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="container mt-4">
|
<div className="container mt-4">
|
||||||
<h1 className="mb-4">Profile</h1>
|
<h1 className="mb-4">Profile</h1>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
{error}
|
{error}
|
||||||
@@ -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,154 +522,163 @@ 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>
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="position-relative d-inline-block mb-3">
|
<div className="position-relative d-inline-block mb-3">
|
||||||
{imagePreview ? (
|
{imagePreview ? (
|
||||||
<img
|
<img
|
||||||
src={imagePreview}
|
src={imagePreview}
|
||||||
alt="Profile"
|
alt="Profile"
|
||||||
className="rounded-circle"
|
className="rounded-circle"
|
||||||
style={{
|
style={{
|
||||||
width: "120px",
|
width: "120px",
|
||||||
height: "120px",
|
height: "120px",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
||||||
|
style={{ width: "120px", height: "120px" }}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="bi bi-person-fill text-white"
|
||||||
|
style={{ fontSize: "2.5rem" }}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{editing && (
|
||||||
|
<label
|
||||||
|
htmlFor="profileImageOverview"
|
||||||
|
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
||||||
|
style={{
|
||||||
|
width: "35px",
|
||||||
|
height: "35px",
|
||||||
|
padding: "0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="bi bi-camera-fill"></i>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="profileImageOverview"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="d-none"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{editing ? (
|
||||||
|
<div>
|
||||||
|
<div className="row justify-content-center mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control mb-2"
|
||||||
|
name="firstName"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="First Name"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div
|
<div className="col-md-6">
|
||||||
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center"
|
<input
|
||||||
style={{ width: "120px", height: "120px" }}
|
type="text"
|
||||||
>
|
className="form-control mb-2"
|
||||||
<i
|
name="lastName"
|
||||||
className="bi bi-person-fill text-white"
|
value={formData.lastName}
|
||||||
style={{ fontSize: "2.5rem" }}
|
onChange={handleChange}
|
||||||
></i>
|
placeholder="Last Name"
|
||||||
</div>
|
required
|
||||||
)}
|
/>
|
||||||
{editing && (
|
</div>
|
||||||
<label
|
|
||||||
htmlFor="profileImageOverview"
|
|
||||||
className="position-absolute bottom-0 end-0 btn btn-sm btn-primary rounded-circle"
|
|
||||||
style={{ width: "35px", height: "35px", padding: "0" }}
|
|
||||||
>
|
|
||||||
<i className="bi bi-camera-fill"></i>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="profileImageOverview"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleImageChange}
|
|
||||||
className="d-none"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="d-flex gap-2 justify-content-center">
|
||||||
{editing ? (
|
<button type="submit" className="btn btn-primary">
|
||||||
<div>
|
Save Changes
|
||||||
<div className="row justify-content-center mb-3">
|
</button>
|
||||||
<div className="col-md-6">
|
<button
|
||||||
<input
|
type="button"
|
||||||
type="text"
|
className="btn btn-secondary"
|
||||||
className="form-control mb-2"
|
onClick={handleCancel}
|
||||||
name="firstName"
|
>
|
||||||
value={formData.firstName}
|
Cancel
|
||||||
onChange={handleChange}
|
</button>
|
||||||
placeholder="First Name"
|
</div>
|
||||||
required
|
</div>
|
||||||
/>
|
) : (
|
||||||
</div>
|
<div>
|
||||||
<div className="col-md-6">
|
<h5>
|
||||||
<input
|
{profileData?.firstName} {profileData?.lastName}
|
||||||
type="text"
|
</h5>
|
||||||
className="form-control mb-2"
|
<p className="text-muted">@{profileData?.username}</p>
|
||||||
name="lastName"
|
{profileData?.isVerified && (
|
||||||
value={formData.lastName}
|
<span className="badge bg-success mb-3">
|
||||||
onChange={handleChange}
|
<i className="bi bi-check-circle-fill"></i>{" "}
|
||||||
placeholder="Last Name"
|
Verified
|
||||||
required
|
</span>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="d-flex gap-2 justify-content-center">
|
|
||||||
<button type="submit" className="btn btn-primary">
|
|
||||||
Save Changes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<h5>
|
|
||||||
{profileData?.firstName} {profileData?.lastName}
|
|
||||||
</h5>
|
|
||||||
<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
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={() => setEditing(true)}
|
|
||||||
>
|
|
||||||
<i className="bi bi-pencil me-2"></i>
|
|
||||||
Edit Profile
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
>
|
||||||
|
<i className="bi bi-pencil me-2"></i>
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
|
</div>
|
||||||
{/* Stats Card */}
|
</div>
|
||||||
<div className="card">
|
|
||||||
<div className="card-body">
|
{/* Stats Card */}
|
||||||
<h5 className="card-title">Account Statistics</h5>
|
<div className="card">
|
||||||
<div className="row text-center">
|
<div className="card-body">
|
||||||
<div className="col-md-4">
|
<h5 className="card-title">Account Statistics</h5>
|
||||||
<div className="p-3">
|
<div className="row text-center">
|
||||||
<h4 className="text-primary mb-1">{stats.itemsListed}</h4>
|
<div className="col-md-4">
|
||||||
<h6 className="text-muted">Items Listed</h6>
|
<div className="p-3">
|
||||||
</div>
|
<h4 className="text-primary mb-1">
|
||||||
</div>
|
{stats.itemsListed}
|
||||||
<div className="col-md-4">
|
</h4>
|
||||||
<div className="p-3">
|
<h6 className="text-muted">Items Listed</h6>
|
||||||
<h4 className="text-success mb-1">{stats.acceptedRentals}</h4>
|
</div>
|
||||||
<h6 className="text-muted">Active Rentals</h6>
|
</div>
|
||||||
</div>
|
<div className="col-md-4">
|
||||||
</div>
|
<div className="p-3">
|
||||||
<div className="col-md-4">
|
<h4 className="text-success mb-1">
|
||||||
<div className="p-3">
|
{stats.acceptedRentals}
|
||||||
<h4 className="text-info mb-1">{stats.totalRentals}</h4>
|
</h4>
|
||||||
<h6 className="text-muted">Total Rentals</h6>
|
<h6 className="text-muted">Active Rentals</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<div className="p-3">
|
||||||
|
<h4 className="text-info mb-1">{stats.totalRentals}</h4>
|
||||||
|
<h6 className="text-muted">Total Rentals</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 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>
|
||||||
|
|
||||||
|
{/* Addresses Card */}
|
||||||
|
<div className="card mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{/* Addresses Section */}
|
<h5 className="card-title">Availability</h5>
|
||||||
<div className="mb-5">
|
<AvailabilitySettings
|
||||||
<h5 className="mb-3">Saved Addresses</h5>
|
data={availabilityData}
|
||||||
<p className="text-muted small mb-3">Manage addresses for your rental locations</p>
|
onChange={handleAvailabilityChange}
|
||||||
<button className="btn btn-outline-primary">
|
onWeeklyTimeChange={handleWeeklyTimeChange}
|
||||||
<i className="bi bi-plus-circle me-2"></i>
|
/>
|
||||||
Add New Address
|
<button
|
||||||
</button>
|
className="btn btn-outline-success mt-3"
|
||||||
</div>
|
onClick={handleSaveAvailability}
|
||||||
|
>
|
||||||
{/* Availability Section */}
|
Save Availability
|
||||||
<div>
|
</button>
|
||||||
<h5 className="mb-3">Default Availability</h5>
|
|
||||||
<p className="text-muted small mb-3">Set your general availability for all items</p>
|
|
||||||
<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>
|
|
||||||
Save Availability
|
|
||||||
</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>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user