Item request notifications
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface User {
|
||||
role?: "user" | "admin";
|
||||
stripeConnectedAccountId?: string;
|
||||
addresses?: Address[];
|
||||
itemRequestNotificationRadius?: number;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
|
||||
Reference in New Issue
Block a user