Item request notifications

This commit is contained in:
jackiettran
2025-11-18 22:28:47 -05:00
parent 026e748bf8
commit 413ac6b6e2
11 changed files with 875 additions and 224 deletions

View File

@@ -20,6 +20,7 @@ const CreateForumPost: React.FC = () => {
| "community_resources"
| "general_discussion",
tags: [] as string[],
zipCode: user?.zipCode || "",
});
const [imageFiles, setImageFiles] = useState<File[]>([]);
@@ -111,6 +112,11 @@ const CreateForumPost: React.FC = () => {
return;
}
if (formData.category === "item_request" && !formData.zipCode.trim()) {
setError("Zip code is required for item requests");
return;
}
try {
setIsSubmitting(true);
@@ -125,6 +131,11 @@ const CreateForumPost: React.FC = () => {
submitData.append('tags', JSON.stringify(formData.tags));
}
// Add location data for item requests
if (formData.category === 'item_request' && formData.zipCode) {
submitData.append('zipCode', formData.zipCode);
}
// Add images
imageFiles.forEach((file) => {
submitData.append('images', file);
@@ -247,6 +258,31 @@ const CreateForumPost: React.FC = () => {
</div>
</div>
{/* Location fields for item requests */}
{formData.category === "item_request" && (
<div className="mb-3">
<label htmlFor="zipCode" className="form-label">
Zip Code <span className="text-danger">*</span>
</label>
<input
type="text"
className="form-control"
id="zipCode"
name="zipCode"
value={formData.zipCode}
onChange={handleInputChange}
placeholder="Enter your zip code..."
maxLength={10}
disabled={isSubmitting}
required
/>
<div className="form-text">
Your zip code helps notify nearby users who might have
the item you're looking for
</div>
</div>
)}
{/* Content */}
<div className="mb-3">
<label htmlFor="content" className="form-label">

View File

@@ -34,6 +34,7 @@ const Profile: React.FC = () => {
zipCode: "",
country: "",
profileImage: "",
itemRequestNotificationRadius: 10,
});
const [imageFile, setImageFile] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
@@ -137,6 +138,7 @@ const Profile: React.FC = () => {
zipCode: response.data.zipCode || "",
country: response.data.country || "",
profileImage: response.data.profileImage || "",
itemRequestNotificationRadius: response.data.itemRequestNotificationRadius || 10,
});
if (response.data.profileImage) {
setImagePreview(getImageUrl(response.data.profileImage));
@@ -259,7 +261,7 @@ const Profile: React.FC = () => {
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
@@ -356,6 +358,7 @@ const Profile: React.FC = () => {
zipCode: profileData.zipCode || "",
country: profileData.country || "",
profileImage: profileData.profileImage || "",
itemRequestNotificationRadius: profileData.itemRequestNotificationRadius || 10,
});
setImagePreview(
profileData.profileImage ? getImageUrl(profileData.profileImage) : null
@@ -835,6 +838,251 @@ const Profile: React.FC = () => {
/>
</div>
<hr className="my-4" />
{/* Saved Addresses Section */}
<div className="mb-3">
<label className="form-label">Saved Addresses</label>
{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 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 && (
<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>
<hr className="my-4" />
{/* Notification Preferences Section */}
<div className="mb-3">
<label className="form-label">Notification Preferences</label>
<div className="mb-3">
<label htmlFor="itemRequestNotificationRadius" className="form-label">
Item Requests Notification Radius
</label>
<select
className="form-select"
id="itemRequestNotificationRadius"
name="itemRequestNotificationRadius"
value={formData.itemRequestNotificationRadius}
onChange={handleChange}
disabled={!editing}
>
<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 when someone posts an item request within this distance from your primary address
</div>
</div>
</div>
<hr className="my-4" />
{editing ? (
<div className="d-flex gap-2">
<button type="submit" className="btn btn-primary">
@@ -1180,219 +1428,6 @@ const Profile: React.FC = () => {
<div>
<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-body">

View File

@@ -32,6 +32,7 @@ export interface User {
role?: "user" | "admin";
stripeConnectedAccountId?: string;
addresses?: Address[];
itemRequestNotificationRadius?: number;
}
export interface Message {