streamlined address and availability

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,18 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import api from "../services/api"; import api, { addressAPI, userAPI, itemAPI } from "../services/api";
import AvailabilitySettings from "../components/AvailabilitySettings"; import AvailabilitySettings from "../components/AvailabilitySettings";
import { Address } from "../types";
interface ItemFormData { interface ItemFormData {
name: string; name: string;
description: string; description: string;
pickUpAvailable: boolean; pickUpAvailable: boolean;
inPlaceUseAvailable: boolean; inPlaceUseAvailable: boolean;
pricePerHour?: number; pricePerHour?: number | string;
pricePerDay?: number; pricePerDay?: number | string;
replacementCost: number; replacementCost: number | string;
location: string; location: string;
address1: string; address1: string;
address2: string; address2: string;
@@ -48,8 +49,8 @@ const CreateItem: React.FC = () => {
description: "", description: "",
pickUpAvailable: false, pickUpAvailable: false,
inPlaceUseAvailable: false, inPlaceUseAvailable: false,
pricePerDay: undefined, pricePerDay: "",
replacementCost: 0, replacementCost: "",
location: "", location: "",
address1: "", address1: "",
address2: "", address2: "",
@@ -75,6 +76,65 @@ const CreateItem: React.FC = () => {
const [imageFiles, setImageFiles] = useState<File[]>([]); const [imageFiles, setImageFiles] = useState<File[]>([]);
const [imagePreviews, setImagePreviews] = useState<string[]>([]); const [imagePreviews, setImagePreviews] = useState<string[]>([]);
const [priceType, setPriceType] = useState<"hour" | "day">("day"); const [priceType, setPriceType] = useState<"hour" | "day">("day");
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
const [addressesLoading, setAddressesLoading] = useState(true);
useEffect(() => {
fetchUserAddresses();
fetchUserAvailability();
}, []);
const fetchUserAvailability = async () => {
try {
const response = await userAPI.getAvailability();
const userAvailability = response.data;
setFormData(prev => ({
...prev,
generalAvailableAfter: userAvailability.generalAvailableAfter,
generalAvailableBefore: userAvailability.generalAvailableBefore,
specifyTimesPerDay: userAvailability.specifyTimesPerDay,
weeklyTimes: userAvailability.weeklyTimes
}));
} catch (error) {
console.error('Error fetching user availability:', error);
// Use default values if fetch fails
}
};
useEffect(() => {
// Auto-populate if user has exactly one address and addresses have finished loading
if (
!addressesLoading &&
userAddresses.length === 1 &&
selectedAddressId === ""
) {
const address = userAddresses[0];
setFormData((prev) => ({
...prev,
address1: address.address1,
address2: address.address2 || "",
city: address.city,
state: address.state,
zipCode: address.zipCode,
country: address.country,
latitude: address.latitude,
longitude: address.longitude,
}));
setSelectedAddressId(address.id);
}
}, [userAddresses, addressesLoading]);
const fetchUserAddresses = async () => {
try {
const response = await addressAPI.getAddresses();
setUserAddresses(response.data);
} catch (error) {
console.error("Error fetching addresses:", error);
} finally {
setAddressesLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -105,9 +165,52 @@ const CreateItem: React.FC = () => {
const response = await api.post("/items", { const response = await api.post("/items", {
...formData, ...formData,
pricePerDay: formData.pricePerDay ? parseFloat(formData.pricePerDay.toString()) : undefined,
pricePerHour: formData.pricePerHour ? parseFloat(formData.pricePerHour.toString()) : undefined,
replacementCost: formData.replacementCost ? parseFloat(formData.replacementCost.toString()) : 0,
location, location,
images: imageUrls, images: imageUrls,
}); });
// Auto-save address if user has no addresses and entered manual address
if (userAddresses.length === 0 && formData.address1) {
try {
await addressAPI.createAddress({
address1: formData.address1,
address2: formData.address2,
city: formData.city,
state: formData.state,
zipCode: formData.zipCode,
country: formData.country,
latitude: formData.latitude,
longitude: formData.longitude,
isPrimary: true,
});
} catch (addressError) {
console.error("Failed to save address:", addressError);
// Don't fail item creation if address save fails
}
}
// Check if this is user's first item and save availability settings
try {
const userItemsResponse = await itemAPI.getItems({ owner: user.id });
const userItems = userItemsResponse.data.items || [];
// If this is their first item (the one we just created), save availability to user
if (userItems.length <= 1) {
await userAPI.updateAvailability({
generalAvailableAfter: formData.generalAvailableAfter,
generalAvailableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes
});
}
} catch (availabilityError) {
console.error("Failed to save availability:", availabilityError);
// Don't fail item creation if availability save fails
}
navigate(`/items/${response.data.id}`); navigate(`/items/${response.data.id}`);
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || "Failed to create listing"); setError(err.response?.data?.error || "Failed to create listing");
@@ -129,13 +232,104 @@ const CreateItem: React.FC = () => {
} else if (type === "number") { } else if (type === "number") {
setFormData((prev) => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value ? parseFloat(value) : undefined, [name]: value === "" ? "" : parseFloat(value) || 0,
})); }));
} else { } else {
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
} }
}; };
const handleAddressSelect = (addressId: string) => {
if (addressId === "new") {
// Clear form for new address entry
setFormData((prev) => ({
...prev,
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
}));
setSelectedAddressId("");
} else {
const selectedAddress = userAddresses.find(
(addr) => addr.id === addressId
);
if (selectedAddress) {
setFormData((prev) => ({
...prev,
address1: selectedAddress.address1,
address2: selectedAddress.address2 || "",
city: selectedAddress.city,
state: selectedAddress.state,
zipCode: selectedAddress.zipCode,
country: selectedAddress.country,
latitude: selectedAddress.latitude,
longitude: selectedAddress.longitude,
}));
setSelectedAddressId(addressId);
}
}
};
const formatAddressDisplay = (address: Address) => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
const usStates = [
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming",
];
const handleWeeklyTimeChange = ( const handleWeeklyTimeChange = (
day: string, day: string,
field: "availableAfter" | "availableBefore", field: "availableAfter" | "availableBefore",
@@ -153,7 +347,6 @@ const CreateItem: React.FC = () => {
})); }));
}; };
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []); const files = Array.from(e.target.files || []);
@@ -285,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>

View File

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

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { userAPI, itemAPI, rentalAPI } from "../services/api"; import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api";
import { User, Item, Rental } from "../types"; import { User, Item, Rental, Address } from "../types";
import { getImageUrl } from "../utils/imageUrl"; import { getImageUrl } from "../utils/imageUrl";
import AvailabilitySettings from "../components/AvailabilitySettings"; import AvailabilitySettings from "../components/AvailabilitySettings";
@@ -11,7 +11,7 @@ const Profile: React.FC = () => {
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
const [activeSection, setActiveSection] = useState<string>('overview'); const [activeSection, setActiveSection] = useState<string>("overview");
const [profileData, setProfileData] = useState<User | null>(null); const [profileData, setProfileData] = useState<User | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
firstName: "", firstName: "",
@@ -47,12 +47,46 @@ const Profile: React.FC = () => {
saturday: { availableAfter: "09:00", availableBefore: "17:00" }, saturday: { availableAfter: "09:00", availableBefore: "17:00" },
}, },
}); });
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [addressesLoading, setAddressesLoading] = useState(true);
const [showAddressForm, setShowAddressForm] = useState(false);
const [editingAddressId, setEditingAddressId] = useState<string | null>(null);
const [addressFormData, setAddressFormData] = useState({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
useEffect(() => { useEffect(() => {
fetchProfile(); fetchProfile();
fetchStats(); fetchStats();
fetchUserAddresses();
fetchUserAvailability();
}, []); }, []);
const fetchUserAvailability = async () => {
try {
const response = await userAPI.getAvailability();
setAvailabilityData(response.data);
} catch (error) {
console.error("Error fetching user availability:", error);
}
};
const fetchUserAddresses = async () => {
try {
const response = await addressAPI.getAddresses();
setUserAddresses(response.data);
} catch (error) {
console.error("Error fetching addresses:", error);
} finally {
setAddressesLoading(false);
}
};
const fetchProfile = async () => { const fetchProfile = async () => {
try { try {
const response = await userAPI.getProfile(); const response = await userAPI.getProfile();
@@ -213,22 +247,180 @@ const Profile: React.FC = () => {
}; };
const handleAvailabilityChange = (field: string, value: string | boolean) => { const handleAvailabilityChange = (field: string, value: string | boolean) => {
setAvailabilityData(prev => ({ ...prev, [field]: value })); setAvailabilityData((prev) => ({ ...prev, [field]: value }));
}; };
const handleWeeklyTimeChange = (day: string, field: 'availableAfter' | 'availableBefore', value: string) => { const handleWeeklyTimeChange = (
setAvailabilityData(prev => ({ day: string,
field: "availableAfter" | "availableBefore",
value: string
) => {
setAvailabilityData((prev) => ({
...prev, ...prev,
weeklyTimes: { weeklyTimes: {
...prev.weeklyTimes, ...prev.weeklyTimes,
[day]: { [day]: {
...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes], ...prev.weeklyTimes[day as keyof typeof prev.weeklyTimes],
[field]: value [field]: value,
} },
} },
})); }));
}; };
const handleSaveAvailability = async () => {
try {
await userAPI.updateAvailability(availabilityData);
setSuccess("Availability settings saved successfully");
setTimeout(() => setSuccess(null), 3000);
} catch (error) {
console.error("Error saving availability:", error);
setError("Failed to save availability settings");
}
};
const formatAddressDisplay = (address: Address) => {
return `${address.address1}, ${address.city}, ${address.state} ${address.zipCode}`;
};
const handleDeleteAddress = async (addressId: string) => {
try {
await addressAPI.deleteAddress(addressId);
setUserAddresses((prev) => prev.filter((addr) => addr.id !== addressId));
} catch (error) {
console.error("Error deleting address:", error);
setError("Failed to delete address");
}
};
const handleAddAddress = () => {
setAddressFormData({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
setEditingAddressId(null);
setShowAddressForm(true);
};
const handleEditAddress = (address: Address) => {
setAddressFormData({
address1: address.address1,
address2: address.address2 || "",
city: address.city,
state: address.state,
zipCode: address.zipCode,
country: address.country,
});
setEditingAddressId(address.id);
setShowAddressForm(true);
};
const handleAddressFormChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setAddressFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSaveAddress = async (e: React.FormEvent) => {
e.preventDefault();
try {
if (editingAddressId) {
// Update existing address
const response = await addressAPI.updateAddress(
editingAddressId,
addressFormData
);
setUserAddresses((prev) =>
prev.map((addr) =>
addr.id === editingAddressId ? response.data : addr
)
);
} else {
// Create new address
const response = await addressAPI.createAddress({
...addressFormData,
isPrimary: userAddresses.length === 0,
});
setUserAddresses((prev) => [...prev, response.data]);
}
setShowAddressForm(false);
setEditingAddressId(null);
} catch (error) {
console.error("Error saving address:", error);
setError("Failed to save address");
}
};
const handleCancelAddressForm = () => {
setShowAddressForm(false);
setEditingAddressId(null);
setAddressFormData({
address1: "",
address2: "",
city: "",
state: "",
zipCode: "",
country: "US",
});
};
const usStates = [
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachusetts",
"Michigan",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Tennessee",
"Texas",
"Utah",
"Vermont",
"Virginia",
"Washington",
"West Virginia",
"Wisconsin",
"Wyoming",
];
if (loading) { if (loading) {
return ( return (
<div className="container mt-5"> <div className="container mt-5">
@@ -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>

View File

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

View File

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