phone auth, image uploading, address broken up
This commit is contained in:
@@ -134,16 +134,16 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
console.log("Current mode:", mode);
|
||||
console.log("First Name:", firstName);
|
||||
console.log("Last Name:", lastName);
|
||||
|
||||
|
||||
const requestBody = {
|
||||
phoneNumber: cleanPhone,
|
||||
code: verificationCode,
|
||||
firstName: mode === "signup" ? firstName : undefined,
|
||||
lastName: mode === "signup" ? lastName : undefined,
|
||||
};
|
||||
|
||||
|
||||
console.log("Request body:", requestBody);
|
||||
|
||||
|
||||
const response = await fetch(
|
||||
"http://localhost:5001/api/auth/phone/verify-code",
|
||||
{
|
||||
@@ -171,23 +171,23 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
||||
// Store token and user data
|
||||
console.log("Storing token:", data.token);
|
||||
localStorage.setItem("token", data.token);
|
||||
|
||||
|
||||
// Verify token was stored
|
||||
const storedToken = localStorage.getItem("token");
|
||||
console.log("Token stored successfully:", !!storedToken);
|
||||
console.log("User data:", data.user);
|
||||
|
||||
|
||||
// Update auth context with the user data
|
||||
updateUser(data.user);
|
||||
|
||||
|
||||
// Close modal and reset state
|
||||
onHide();
|
||||
resetModal();
|
||||
|
||||
|
||||
// Force a page reload to ensure auth state is properly initialized
|
||||
// This is needed because AuthContext's useEffect only runs once on mount
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
window.location.href = "/";
|
||||
}, 100);
|
||||
} catch (err: any) {
|
||||
console.error("Verification error:", err);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
|
||||
import { User } from '../types';
|
||||
import { authAPI, userAPI } from '../services/api';
|
||||
import React, {
|
||||
createContext,
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { User } from "../types";
|
||||
import { authAPI, userAPI } from "../services/api";
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
@@ -16,7 +22,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -30,41 +36,38 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
console.log('AuthContext: Found token, fetching profile...');
|
||||
userAPI.getProfile()
|
||||
.then(response => {
|
||||
console.log('AuthContext: Profile loaded', response.data);
|
||||
userAPI
|
||||
.getProfile()
|
||||
.then((response) => {
|
||||
setUser(response.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('AuthContext: Failed to load profile', error);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem("token");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
console.log('AuthContext: No token found');
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const response = await authAPI.login({ email, password });
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem("token", response.data.token);
|
||||
setUser(response.data.user);
|
||||
};
|
||||
|
||||
const register = async (data: any) => {
|
||||
const response = await authAPI.register(data);
|
||||
localStorage.setItem('token', response.data.token);
|
||||
localStorage.setItem("token", response.data.token);
|
||||
setUser(response.data.user);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem("token");
|
||||
setUser(null);
|
||||
};
|
||||
|
||||
@@ -73,8 +76,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, register, logout, updateUser }}>
|
||||
<AuthContext.Provider
|
||||
value={{ user, loading, login, register, logout, updateUser }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import api from "../services/api";
|
||||
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
||||
import AddressAutocomplete from "../components/AddressAutocomplete";
|
||||
|
||||
interface ItemFormData {
|
||||
name: string;
|
||||
@@ -18,6 +17,12 @@ interface ItemFormData {
|
||||
pricePerDay?: number;
|
||||
replacementCost: number;
|
||||
location: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zipCode: string;
|
||||
country: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
rules?: string;
|
||||
@@ -49,6 +54,12 @@ const CreateItem: React.FC = () => {
|
||||
pricePerDay: undefined,
|
||||
replacementCost: 0,
|
||||
location: "",
|
||||
address1: "",
|
||||
address2: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "",
|
||||
minimumRentalDays: 1,
|
||||
needsTraining: false,
|
||||
unavailablePeriods: [],
|
||||
@@ -73,8 +84,21 @@ const CreateItem: React.FC = () => {
|
||||
// In production, you'd upload to a service like S3
|
||||
const imageUrls = imagePreviews;
|
||||
|
||||
// Construct location from address components
|
||||
const locationParts = [
|
||||
formData.address1,
|
||||
formData.address2,
|
||||
formData.city,
|
||||
formData.state,
|
||||
formData.zipCode,
|
||||
formData.country
|
||||
].filter(part => part && part.trim());
|
||||
|
||||
const location = locationParts.join(', ');
|
||||
|
||||
const response = await api.post("/items", {
|
||||
...formData,
|
||||
location,
|
||||
images: imageUrls,
|
||||
});
|
||||
navigate(`/items/${response.data.id}`);
|
||||
@@ -271,23 +295,99 @@ const CreateItem: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 className="mb-3">Location *</h6>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="state"
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
placeholder="CA"
|
||||
required
|
||||
/>
|
||||
</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="12345"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor="location" className="form-label">
|
||||
Location *
|
||||
<label htmlFor="country" className="form-label">
|
||||
Country
|
||||
</label>
|
||||
<AddressAutocomplete
|
||||
id="location"
|
||||
name="location"
|
||||
value={formData.location}
|
||||
onChange={(value, lat, lon) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
location: value,
|
||||
latitude: lat,
|
||||
longitude: lon
|
||||
}));
|
||||
}}
|
||||
placeholder="Address"
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="country"
|
||||
name="country"
|
||||
value={formData.country}
|
||||
onChange={handleChange}
|
||||
placeholder="United States"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -305,7 +405,10 @@ const CreateItem: React.FC = () => {
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||
Pick-Up
|
||||
<div className="small text-muted">They pick-up the item from your location and they return the item to your location</div>
|
||||
<div className="small text-muted">
|
||||
They pick-up the item from your location and they return the
|
||||
item to your location
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
@@ -325,24 +428,27 @@ const CreateItem: React.FC = () => {
|
||||
Local Delivery
|
||||
{formData.localDeliveryAvailable && (
|
||||
<span className="ms-2">
|
||||
(Delivery Radius:
|
||||
(Delivery Radius:
|
||||
<input
|
||||
type="number"
|
||||
className="form-control form-control-sm d-inline-block mx-1"
|
||||
id="localDeliveryRadius"
|
||||
name="localDeliveryRadius"
|
||||
value={formData.localDeliveryRadius || ''}
|
||||
value={formData.localDeliveryRadius || ""}
|
||||
onChange={handleChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
placeholder="25"
|
||||
min="1"
|
||||
max="100"
|
||||
style={{ width: '60px' }}
|
||||
style={{ width: "60px" }}
|
||||
/>
|
||||
miles)
|
||||
</span>
|
||||
)}
|
||||
<div className="small text-muted">You deliver and then pick-up the item when the rental period ends</div>
|
||||
<div className="small text-muted">
|
||||
You deliver and then pick-up the item when the rental
|
||||
period ends
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@@ -373,7 +479,9 @@ const CreateItem: React.FC = () => {
|
||||
htmlFor="inPlaceUseAvailable"
|
||||
>
|
||||
In-Place Use
|
||||
<div className="small text-muted">They use at your location</div>
|
||||
<div className="small text-muted">
|
||||
They use at your location
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,7 +495,9 @@ const CreateItem: React.FC = () => {
|
||||
<select
|
||||
className="form-select"
|
||||
value={priceType}
|
||||
onChange={(e) => setPriceType(e.target.value as "hour" | "day")}
|
||||
onChange={(e) =>
|
||||
setPriceType(e.target.value as "hour" | "day")
|
||||
}
|
||||
>
|
||||
<option value="hour">Hour</option>
|
||||
<option value="day">Day</option>
|
||||
@@ -400,8 +510,14 @@ const CreateItem: React.FC = () => {
|
||||
type="number"
|
||||
className="form-control"
|
||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
||||
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
||||
value={priceType === "hour" ? (formData.pricePerHour || "") : (formData.pricePerDay || "")}
|
||||
name={
|
||||
priceType === "hour" ? "pricePerHour" : "pricePerDay"
|
||||
}
|
||||
value={
|
||||
priceType === "hour"
|
||||
? formData.pricePerHour || ""
|
||||
: formData.pricePerDay || ""
|
||||
}
|
||||
onChange={handleChange}
|
||||
step="0.01"
|
||||
min="0"
|
||||
@@ -429,11 +545,16 @@ const CreateItem: React.FC = () => {
|
||||
|
||||
<div className="mb-4">
|
||||
<h5>Availability Schedule</h5>
|
||||
<p className="text-muted">Select dates when the item is NOT available for rent</p>
|
||||
<p className="text-muted">
|
||||
Select dates when the item is NOT available for rent
|
||||
</p>
|
||||
<AvailabilityCalendar
|
||||
unavailablePeriods={formData.unavailablePeriods || []}
|
||||
onPeriodsChange={(periods) =>
|
||||
setFormData(prev => ({ ...prev, unavailablePeriods: periods }))
|
||||
onPeriodsChange={(periods) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
unavailablePeriods: periods,
|
||||
}))
|
||||
}
|
||||
mode="owner"
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { userAPI, itemAPI, rentalAPI } from '../services/api';
|
||||
import { User, Item, Rental } from '../types';
|
||||
import AddressAutocomplete from '../components/AddressAutocomplete';
|
||||
import { getImageUrl } from '../utils/imageUrl';
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const { user, updateUser, logout } = useAuth();
|
||||
@@ -16,7 +16,12 @@ const Profile: React.FC = () => {
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
address1: '',
|
||||
address2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zipCode: '',
|
||||
country: '',
|
||||
profileImage: ''
|
||||
});
|
||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||
@@ -41,11 +46,16 @@ const Profile: React.FC = () => {
|
||||
lastName: response.data.lastName || '',
|
||||
email: response.data.email || '',
|
||||
phone: response.data.phone || '',
|
||||
address: response.data.address || '',
|
||||
address1: response.data.address1 || '',
|
||||
address2: response.data.address2 || '',
|
||||
city: response.data.city || '',
|
||||
state: response.data.state || '',
|
||||
zipCode: response.data.zipCode || '',
|
||||
country: response.data.country || '',
|
||||
profileImage: response.data.profileImage || ''
|
||||
});
|
||||
if (response.data.profileImage) {
|
||||
setImagePreview(response.data.profileImage);
|
||||
setImagePreview(getImageUrl(response.data.profileImage));
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to fetch profile');
|
||||
@@ -82,15 +92,43 @@ const Profile: React.FC = () => {
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
setImageFile(file);
|
||||
|
||||
// Show preview
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Upload image immediately
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('profileImage', file);
|
||||
|
||||
const response = await userAPI.uploadProfileImage(formData);
|
||||
|
||||
// Update the profileImage in formData with the new filename
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
profileImage: response.data.filename
|
||||
}));
|
||||
|
||||
// Update preview to use the uploaded image URL
|
||||
setImagePreview(getImageUrl(response.data.imageUrl));
|
||||
} catch (err: any) {
|
||||
console.error('Image upload error:', err);
|
||||
setError(err.response?.data?.error || 'Failed to upload image');
|
||||
// Reset on error
|
||||
setImageFile(null);
|
||||
setImagePreview(profileData?.profileImage ?
|
||||
getImageUrl(profileData.profileImage) :
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,18 +138,24 @@ const Profile: React.FC = () => {
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
...formData,
|
||||
profileImage: imagePreview || formData.profileImage
|
||||
};
|
||||
// Don't send profileImage in the update data as it's handled separately
|
||||
const { profileImage, ...updateData } = formData;
|
||||
|
||||
const response = await userAPI.updateProfile(updateData);
|
||||
setProfileData(response.data);
|
||||
updateUser(response.data); // Update the auth context
|
||||
setSuccess('Profile updated successfully!');
|
||||
setEditing(false);
|
||||
} catch (err: any) {
|
||||
setError(err.response?.data?.message || 'Failed to update profile');
|
||||
console.error('Profile update error:', err.response?.data);
|
||||
const errorMessage = err.response?.data?.error || err.response?.data?.message || 'Failed to update profile';
|
||||
const errorDetails = err.response?.data?.details;
|
||||
|
||||
if (errorDetails && Array.isArray(errorDetails)) {
|
||||
const detailMessages = errorDetails.map((d: any) => `${d.field}: ${d.message}`).join(', ');
|
||||
setError(`${errorMessage} - ${detailMessages}`);
|
||||
} else {
|
||||
setError(errorMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -126,10 +170,18 @@ const Profile: React.FC = () => {
|
||||
lastName: profileData.lastName || '',
|
||||
email: profileData.email || '',
|
||||
phone: profileData.phone || '',
|
||||
address: profileData.address || '',
|
||||
address1: profileData.address1 || '',
|
||||
address2: profileData.address2 || '',
|
||||
city: profileData.city || '',
|
||||
state: profileData.state || '',
|
||||
zipCode: profileData.zipCode || '',
|
||||
country: profileData.country || '',
|
||||
profileImage: profileData.profileImage || ''
|
||||
});
|
||||
setImagePreview(profileData.profileImage || null);
|
||||
setImagePreview(profileData.profileImage ?
|
||||
getImageUrl(profileData.profileImage) :
|
||||
null
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,7 +300,6 @@ const Profile: React.FC = () => {
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -266,30 +317,88 @@ const Profile: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="address" className="form-label">Address</label>
|
||||
{editing ? (
|
||||
<AddressAutocomplete
|
||||
id="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
onChange={(value) => {
|
||||
setFormData(prev => ({ ...prev, address: value }));
|
||||
}}
|
||||
placeholder="Enter your address"
|
||||
required={false}
|
||||
/>
|
||||
) : (
|
||||
<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="address"
|
||||
name="address"
|
||||
value={formData.address}
|
||||
disabled
|
||||
readOnly
|
||||
id="address1"
|
||||
name="address1"
|
||||
value={formData.address1}
|
||||
onChange={handleChange}
|
||||
placeholder="123 Main Street"
|
||||
disabled={!editing}
|
||||
/>
|
||||
)}
|
||||
</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."
|
||||
disabled={!editing}
|
||||
/>
|
||||
</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}
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label htmlFor="state" className="form-label">State</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="state"
|
||||
name="state"
|
||||
value={formData.state}
|
||||
onChange={handleChange}
|
||||
placeholder="CA"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</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="12345"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="country" className="form-label">Country</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="country"
|
||||
name="country"
|
||||
value={formData.country}
|
||||
onChange={handleChange}
|
||||
placeholder="United States"
|
||||
disabled={!editing}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import axios from 'axios';
|
||||
import axios from "axios";
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL;
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
@@ -22,12 +22,12 @@ api.interceptors.response.use(
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Only redirect to login if we have a token (user was logged in)
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
if (token) {
|
||||
// User was logged in but token expired/invalid
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
// For non-authenticated users, just reject the error without redirecting
|
||||
// Let individual components handle 401 errors as needed
|
||||
@@ -37,42 +37,47 @@ api.interceptors.response.use(
|
||||
);
|
||||
|
||||
export const authAPI = {
|
||||
register: (data: any) => api.post('/auth/register', data),
|
||||
login: (data: any) => api.post('/auth/login', data),
|
||||
register: (data: any) => api.post("/auth/register", data),
|
||||
login: (data: any) => api.post("/auth/login", data),
|
||||
};
|
||||
|
||||
export const userAPI = {
|
||||
getProfile: () => api.get('/users/profile'),
|
||||
updateProfile: (data: any) => api.put('/users/profile', data),
|
||||
getPublicProfile: (userId: string) => api.get(`/users/${userId}`),
|
||||
getProfile: () => api.get("/users/profile"),
|
||||
updateProfile: (data: any) => api.put("/users/profile", data),
|
||||
uploadProfileImage: (formData: FormData) =>
|
||||
api.post("/users/profile/image", formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}),
|
||||
getPublicProfile: (id: string) => api.get(`/users/${id}`),
|
||||
};
|
||||
|
||||
export const itemAPI = {
|
||||
getItems: (params?: any) => api.get('/items', { params }),
|
||||
getItems: (params?: any) => api.get("/items", { params }),
|
||||
getItem: (id: string) => api.get(`/items/${id}`),
|
||||
createItem: (data: any) => api.post('/items', data),
|
||||
createItem: (data: any) => api.post("/items", data),
|
||||
updateItem: (id: string, data: any) => api.put(`/items/${id}`, data),
|
||||
deleteItem: (id: string) => api.delete(`/items/${id}`),
|
||||
getRecommendations: () => api.get('/items/recommendations'),
|
||||
getRecommendations: () => api.get("/items/recommendations"),
|
||||
};
|
||||
|
||||
export const rentalAPI = {
|
||||
createRental: (data: any) => api.post('/rentals', data),
|
||||
getMyRentals: () => api.get('/rentals/my-rentals'),
|
||||
getMyListings: () => api.get('/rentals/my-listings'),
|
||||
updateRentalStatus: (id: string, status: string) =>
|
||||
createRental: (data: any) => api.post("/rentals", data),
|
||||
getMyRentals: () => api.get("/rentals/my-rentals"),
|
||||
getMyListings: () => api.get("/rentals/my-listings"),
|
||||
updateRentalStatus: (id: string, status: string) =>
|
||||
api.put(`/rentals/${id}/status`, { status }),
|
||||
addReview: (id: string, data: any) =>
|
||||
api.post(`/rentals/${id}/review`, data),
|
||||
addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data),
|
||||
};
|
||||
|
||||
export const messageAPI = {
|
||||
getMessages: () => api.get('/messages'),
|
||||
getSentMessages: () => api.get('/messages/sent'),
|
||||
getMessages: () => api.get("/messages"),
|
||||
getSentMessages: () => api.get("/messages/sent"),
|
||||
getMessage: (id: string) => api.get(`/messages/${id}`),
|
||||
sendMessage: (data: any) => api.post('/messages', data),
|
||||
sendMessage: (data: any) => api.post("/messages", data),
|
||||
markAsRead: (id: string) => api.put(`/messages/${id}/read`),
|
||||
getUnreadCount: () => api.get('/messages/unread/count'),
|
||||
getUnreadCount: () => api.get("/messages/unread/count"),
|
||||
};
|
||||
|
||||
export default api;
|
||||
export default api;
|
||||
|
||||
@@ -5,7 +5,12 @@ export interface User {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zipCode?: string;
|
||||
country?: string;
|
||||
profileImage?: string;
|
||||
isVerified: boolean;
|
||||
}
|
||||
@@ -42,10 +47,16 @@ export interface Item {
|
||||
pricePerMonth?: number;
|
||||
replacementCost: number;
|
||||
location: string;
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
city?: string;
|
||||
state?: string;
|
||||
zipCode?: string;
|
||||
country?: string;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
images: string[];
|
||||
condition: 'excellent' | 'good' | 'fair' | 'poor';
|
||||
condition: "excellent" | "good" | "fair" | "poor";
|
||||
availability: boolean;
|
||||
specifications: Record<string, any>;
|
||||
rules?: string;
|
||||
@@ -73,9 +84,9 @@ export interface Rental {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
totalAmount: number;
|
||||
status: 'pending' | 'confirmed' | 'active' | 'completed' | 'cancelled';
|
||||
paymentStatus: 'pending' | 'paid' | 'refunded';
|
||||
deliveryMethod: 'pickup' | 'delivery';
|
||||
status: "pending" | "confirmed" | "active" | "completed" | "cancelled";
|
||||
paymentStatus: "pending" | "paid" | "refunded";
|
||||
deliveryMethod: "pickup" | "delivery";
|
||||
deliveryAddress?: string;
|
||||
notes?: string;
|
||||
rating?: number;
|
||||
@@ -86,4 +97,4 @@ export interface Rental {
|
||||
owner?: User;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
}
|
||||
|
||||
13
frontend/src/utils/imageUrl.ts
Normal file
13
frontend/src/utils/imageUrl.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const getImageUrl = (imagePath: string): string => {
|
||||
// Get the base URL without /api
|
||||
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||
const baseUrl = apiUrl.replace('/api', '');
|
||||
|
||||
// If imagePath already includes the full path, use it
|
||||
if (imagePath.startsWith('/uploads/')) {
|
||||
return `${baseUrl}${imagePath}`;
|
||||
}
|
||||
|
||||
// Otherwise, construct the full path
|
||||
return `${baseUrl}/uploads/profiles/${imagePath}`;
|
||||
};
|
||||
Reference in New Issue
Block a user