disable item request notifications

This commit is contained in:
jackiettran
2025-11-20 15:28:16 -05:00
parent 83872fe039
commit 88c831419c
2 changed files with 466 additions and 347 deletions

View File

@@ -395,7 +395,21 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res)
attributes: ['itemRequestNotificationRadius'] attributes: ['itemRequestNotificationRadius']
}); });
const userPreferredRadius = userProfile?.itemRequestNotificationRadius || 10; const userPreferredRadius = userProfile?.itemRequestNotificationRadius;
// Skip if user has disabled notifications (null)
if (userPreferredRadius === null || userPreferredRadius === undefined) {
logger.info("User has disabled item request notifications", {
postId: post.id,
userId: user.id,
userDistance: user.distance
});
usersSkipped++;
continue;
}
// Default to 10 miles if somehow not set
const effectiveRadius = userPreferredRadius || 10;
logger.info("Checking user notification eligibility", { logger.info("Checking user notification eligibility", {
postId: post.id, postId: post.id,
@@ -404,12 +418,12 @@ router.post('/posts', authenticateToken, uploadForumPostImages, async (req, res)
userCoordinates: { lat: user.latitude, lng: user.longitude }, userCoordinates: { lat: user.latitude, lng: user.longitude },
postCoordinates: { lat: latitude, lng: longitude }, postCoordinates: { lat: latitude, lng: longitude },
userDistance: user.distance, userDistance: user.distance,
userPreferredRadius, userPreferredRadius: effectiveRadius,
willNotify: parseFloat(user.distance) <= userPreferredRadius willNotify: parseFloat(user.distance) <= effectiveRadius
}); });
// Only notify if within user's preferred radius // Only notify if within user's preferred radius
if (parseFloat(user.distance) <= userPreferredRadius) { if (parseFloat(user.distance) <= effectiveRadius) {
try { try {
await emailServices.forum.sendItemRequestNotification( await emailServices.forum.sendItemRequestNotification(
user, user,

View File

@@ -14,7 +14,10 @@ import {
} from "../services/geocodingService"; } from "../services/geocodingService";
import AddressAutocomplete from "../components/AddressAutocomplete"; import AddressAutocomplete from "../components/AddressAutocomplete";
import { PlaceDetails } from "../services/placesService"; import { PlaceDetails } from "../services/placesService";
import { useAddressAutocomplete, usStates } from "../hooks/useAddressAutocomplete"; import {
useAddressAutocomplete,
usStates,
} from "../hooks/useAddressAutocomplete";
const Profile: React.FC = () => { const Profile: React.FC = () => {
const { user, updateUser, logout } = useAuth(); const { user, updateUser, logout } = useAuth();
@@ -25,7 +28,20 @@ const Profile: React.FC = () => {
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: string;
lastName: string;
email: string;
phone: string;
address1: string;
address2: string;
city: string;
state: string;
zipCode: string;
country: string;
profileImage: string;
itemRequestNotificationRadius: number | null;
}>({
firstName: "", firstName: "",
lastName: "", lastName: "",
email: "", email: "",
@@ -141,7 +157,8 @@ const Profile: React.FC = () => {
zipCode: response.data.zipCode || "", zipCode: response.data.zipCode || "",
country: response.data.country || "", country: response.data.country || "",
profileImage: response.data.profileImage || "", profileImage: response.data.profileImage || "",
itemRequestNotificationRadius: response.data.itemRequestNotificationRadius || 10, itemRequestNotificationRadius:
response.data.itemRequestNotificationRadius || 10,
}); });
if (response.data.profileImage) { if (response.data.profileImage) {
setImagePreview(getImageUrl(response.data.profileImage)); setImagePreview(getImageUrl(response.data.profileImage));
@@ -264,7 +281,9 @@ const Profile: React.FC = () => {
}; };
const handleChange = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>
) => { ) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value })); setFormData((prev) => ({ ...prev, [name]: value }));
@@ -361,7 +380,8 @@ const Profile: React.FC = () => {
zipCode: profileData.zipCode || "", zipCode: profileData.zipCode || "",
country: profileData.country || "", country: profileData.country || "",
profileImage: profileData.profileImage || "", profileImage: profileData.profileImage || "",
itemRequestNotificationRadius: profileData.itemRequestNotificationRadius || 10, itemRequestNotificationRadius:
profileData.itemRequestNotificationRadius || 10,
}); });
setImagePreview( setImagePreview(
profileData.profileImage ? getImageUrl(profileData.profileImage) : null profileData.profileImage ? getImageUrl(profileData.profileImage) : null
@@ -415,7 +435,10 @@ const Profile: React.FC = () => {
setSuccess("Notification preferences saved successfully"); setSuccess("Notification preferences saved successfully");
setTimeout(() => setSuccess(null), 3000); setTimeout(() => setSuccess(null), 3000);
} catch (err: any) { } catch (err: any) {
console.error("Notification preferences update error:", err.response?.data); console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage = const errorMessage =
err.response?.data?.error || err.response?.data?.error ||
err.response?.data?.message || err.response?.data?.message ||
@@ -428,7 +451,10 @@ const Profile: React.FC = () => {
e: React.ChangeEvent<HTMLSelectElement> e: React.ChangeEvent<HTMLSelectElement>
) => { ) => {
const { value } = e.target; const { value } = e.target;
setFormData((prev) => ({ ...prev, itemRequestNotificationRadius: parseInt(value) })); setFormData((prev) => ({
...prev,
itemRequestNotificationRadius: parseInt(value),
}));
setError(null); setError(null);
try { try {
@@ -438,7 +464,10 @@ const Profile: React.FC = () => {
setProfileData(response.data); setProfileData(response.data);
updateUser(response.data); updateUser(response.data);
} catch (err: any) { } catch (err: any) {
console.error("Notification preferences update error:", err.response?.data); console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage = const errorMessage =
err.response?.data?.error || err.response?.data?.error ||
err.response?.data?.message || err.response?.data?.message ||
@@ -560,8 +589,8 @@ const Profile: React.FC = () => {
...addressFormData, ...addressFormData,
...(coordinates && { ...(coordinates && {
latitude: coordinates.latitude, latitude: coordinates.latitude,
longitude: coordinates.longitude longitude: coordinates.longitude,
}) }),
}; };
try { try {
@@ -614,17 +643,20 @@ const Profile: React.FC = () => {
const { parsePlace } = useAddressAutocomplete(); const { parsePlace } = useAddressAutocomplete();
// Handle place selection from autocomplete // Handle place selection from autocomplete
const handlePlaceSelect = useCallback((place: PlaceDetails) => { const handlePlaceSelect = useCallback(
const parsedAddress = parsePlace(place); (place: PlaceDetails) => {
if (parsedAddress) { const parsedAddress = parsePlace(place);
setAddressFormData((prev) => ({ if (parsedAddress) {
...prev, setAddressFormData((prev) => ({
...parsedAddress, ...prev,
})); ...parsedAddress,
setAddressGeocodeSuccess(true); }));
setTimeout(() => setAddressGeocodeSuccess(false), 3000); setAddressGeocodeSuccess(true);
} setTimeout(() => setAddressGeocodeSuccess(false), 3000);
}, [parsePlace]); }
},
[parsePlace]
);
if (loading) { if (loading) {
return ( return (
@@ -781,335 +813,344 @@ const Profile: React.FC = () => {
type="button" type="button"
className="btn btn-link text-primary p-0 ms-2" className="btn btn-link text-primary p-0 ms-2"
onClick={() => setShowPersonalInfo(!showPersonalInfo)} onClick={() => setShowPersonalInfo(!showPersonalInfo)}
style={{ textDecoration: 'none' }} style={{ textDecoration: "none" }}
> >
<i className={`bi ${showPersonalInfo ? 'bi-eye' : 'bi-eye-slash'} fs-5`}></i> <i
className={`bi ${
showPersonalInfo ? "bi-eye" : "bi-eye-slash"
} fs-5`}
></i>
</button> </button>
</div> </div>
{showPersonalInfo && ( {showPersonalInfo && (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="row mb-3"> <div className="row mb-3">
<div className="col-md-6"> <div className="col-md-6">
<label htmlFor="firstName" className="form-label"> <label htmlFor="firstName" className="form-label">
First Name First Name
</label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleChange}
disabled={!editing}
required
/>
</div>
<div className="col-md-6">
<label htmlFor="lastName" className="form-label">
Last Name
</label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleChange}
disabled={!editing}
required
/>
</div>
</div>
<div className="mb-3">
<label htmlFor="email" className="form-label">
Email
</label> </label>
<input <input
type="text" type="email"
className="form-control" className="form-control"
id="firstName" id="email"
name="firstName" name="email"
value={formData.firstName} value={formData.email}
onChange={handleChange} onChange={handleChange}
disabled={!editing} disabled={!editing}
required
/> />
</div> </div>
<div className="col-md-6">
<label htmlFor="lastName" className="form-label"> <div className="mb-3">
Last Name <label htmlFor="phone" className="form-label">
Phone Number
</label> </label>
<input <input
type="text" type="tel"
className="form-control" className="form-control"
id="lastName" id="phone"
name="lastName" name="phone"
value={formData.lastName} value={formData.phone}
onChange={handleChange} onChange={handleChange}
placeholder="(123) 456-7890"
disabled={!editing} disabled={!editing}
required
/> />
</div> </div>
</div>
<div className="mb-3"> <hr className="my-4" />
<label htmlFor="email" className="form-label">
Email
</label>
<input
type="email"
className="form-control"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
disabled={!editing}
/>
</div>
<div className="mb-3"> {/* Saved Addresses Section */}
<label htmlFor="phone" className="form-label"> <div className="mb-3">
Phone Number <label className="form-label">Saved Addresses</label>
</label>
<input
type="tel"
className="form-control"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="(123) 456-7890"
disabled={!editing}
/>
</div>
<hr className="my-4" /> {addressesLoading ? (
<div className="text-center py-3">
{/* Saved Addresses Section */} <div
<div className="mb-3"> className="spinner-border spinner-border-sm"
<label className="form-label">Saved Addresses</label> role="status"
>
{addressesLoading ? ( <span className="visually-hidden">
<div className="text-center py-3"> Loading addresses...
<div </span>
className="spinner-border spinner-border-sm" </div>
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 mb-2">
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 && (
<div>
<div className="row mb-3">
<div className="col-md-6">
<label
htmlFor="addressFormAddress1"
className="form-label"
>
Address Line 1 *
</label>
<AddressAutocomplete
id="addressFormAddress1"
name="address1"
value={addressFormData.address1}
onChange={(value) => {
const syntheticEvent = {
target: {
name: "address1",
value,
type: "text",
},
} as React.ChangeEvent<HTMLInputElement>;
handleAddressFormChange(syntheticEvent);
}}
onPlaceSelect={handlePlaceSelect}
placeholder="Start typing an address..."
className="form-control"
required
countryRestriction="us"
types={["address"]}
/>
</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}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSaveAddress(e);
}
}}
placeholder="12345"
required
/>
</div>
</div>
<div className="d-flex gap-2">
<button
type="button"
className="btn btn-primary"
onClick={handleSaveAddress}
>
{editingAddressId
? "Update Address"
: "Save Address"}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancelAddressForm}
>
Cancel
</button>
</div>
</div>
)}
</>
)}
</div>
<hr className="my-4" />
{editing ? (
<div className="d-flex gap-2">
<button type="submit" className="btn btn-primary">
Save Changes
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancel}
>
Cancel
</button>
</div> </div>
) : ( ) : (
<>
{userAddresses.length === 0 && !showAddressForm ? (
<div className="text-center py-3">
<p className="text-muted mb-2">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 && (
<div>
<div className="row mb-3">
<div className="col-md-6">
<label
htmlFor="addressFormAddress1"
className="form-label"
>
Address Line 1 *
</label>
<AddressAutocomplete
id="addressFormAddress1"
name="address1"
value={addressFormData.address1}
onChange={(value) => {
const syntheticEvent = {
target: {
name: "address1",
value,
type: "text",
},
} as React.ChangeEvent<HTMLInputElement>;
handleAddressFormChange(syntheticEvent);
}}
onPlaceSelect={handlePlaceSelect}
placeholder="Start typing an address..."
className="form-control"
required
countryRestriction="us"
types={["address"]}
/>
</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}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSaveAddress(e);
}
}}
placeholder="12345"
required
/>
</div>
</div>
<div className="d-flex gap-2">
<button
type="button"
className="btn btn-primary"
onClick={handleSaveAddress}
>
{editingAddressId
? "Update Address"
: "Save Address"}
</button>
<button
type="button"
className="btn btn-secondary"
onClick={handleCancelAddressForm}
>
Cancel
</button>
</div>
</div>
)}
</>
)}
</div>
<hr className="my-4" />
{editing ? (
<div className="d-flex gap-2">
<button type="submit" className="btn btn-primary">
Save Changes
</button>
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-primary"
onClick={handleCancel} onClick={() => setEditing(true)}
> >
Cancel Edit Information
</button> </button>
</div> )}
) : ( </form>
<button
type="button"
className="btn btn-primary"
onClick={() => setEditing(true)}
>
Edit Information
</button>
)}
</form>
)} )}
</div> </div>
</div> </div>
@@ -1225,10 +1266,13 @@ const Profile: React.FC = () => {
<p className="mb-1 small"> <p className="mb-1 small">
<strong>Owner:</strong>{" "} <strong>Owner:</strong>{" "}
<span <span
onClick={() => navigate(`/users/${rental.ownerId}`)} onClick={() =>
navigate(`/users/${rental.ownerId}`)
}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
{rental.owner.firstName} {rental.owner.lastName} {rental.owner.firstName}{" "}
{rental.owner.lastName}
</span> </span>
</p> </p>
)} )}
@@ -1330,10 +1374,13 @@ const Profile: React.FC = () => {
<p className="mb-1 small"> <p className="mb-1 small">
<strong>Renter:</strong>{" "} <strong>Renter:</strong>{" "}
<span <span
onClick={() => navigate(`/users/${rental.renterId}`)} onClick={() =>
navigate(`/users/${rental.renterId}`)
}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
{rental.renter.firstName} {rental.renter.lastName} {rental.renter.firstName}{" "}
{rental.renter.lastName}
</span> </span>
</p> </p>
)} )}
@@ -1460,26 +1507,84 @@ const Profile: React.FC = () => {
<div className="card"> <div className="card">
<div className="card-body"> <div className="card-body">
<div className="mb-3"> <div className="mb-3">
<label htmlFor="itemRequestNotificationRadius" className="form-label"> <div className="form-check">
Item Requests Notification Radius <input
</label> className="form-check-input"
<select type="checkbox"
className="form-select" id="enableItemRequestNotifications"
id="itemRequestNotificationRadius" checked={
name="itemRequestNotificationRadius" formData.itemRequestNotificationRadius !== null &&
value={formData.itemRequestNotificationRadius} formData.itemRequestNotificationRadius !== undefined
onChange={handleNotificationRadiusChange} }
> onChange={async (e) => {
<option value="5">5 miles</option> const isEnabled = e.target.checked;
<option value="10">10 miles</option> const newRadius = isEnabled ? 10 : null; // Default to 10 miles when enabled
<option value="25">25 miles</option> setFormData((prev) => ({
<option value="50">50 miles</option> ...prev,
<option value="100">100 miles</option> itemRequestNotificationRadius: newRadius,
</select> }));
<div className="form-text"> setError(null);
You'll receive notifications when someone posts an item request within this distance from your primary address
try {
const response = await userAPI.updateProfile({
itemRequestNotificationRadius: newRadius,
});
setProfileData(response.data);
updateUser(response.data);
} catch (err: any) {
console.error(
"Notification preferences update error:",
err.response?.data
);
const errorMessage =
err.response?.data?.error ||
err.response?.data?.message ||
"Failed to update notification preferences";
setError(errorMessage);
}
}}
/>
<label
className="form-check-label"
htmlFor="enableItemRequestNotifications"
>
Enable Item Request Notifications
</label>
</div>
<div className="form-text mb-3">
Receive notifications when someone nearby posts an item
request
</div> </div>
</div> </div>
{formData.itemRequestNotificationRadius !== null &&
formData.itemRequestNotificationRadius !== undefined && (
<div className="mb-3">
<label
htmlFor="itemRequestNotificationRadius"
className="form-label"
>
Notification Radius
</label>
<select
className="form-select"
id="itemRequestNotificationRadius"
name="itemRequestNotificationRadius"
value={formData.itemRequestNotificationRadius}
onChange={handleNotificationRadiusChange}
>
<option value="5">5 miles</option>
<option value="10">10 miles</option>
<option value="25">25 miles</option>
<option value="50">50 miles</option>
<option value="100">100 miles</option>
</select>
<div className="form-text">
You'll receive notifications for item requests within
this distance from your primary address
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>