This commit is contained in:
jackiettran
2025-12-11 20:05:18 -05:00
parent 11593606aa
commit b0268a2fb7
28 changed files with 2578 additions and 432 deletions

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { forumAPI, addressAPI } from "../services/api";
import { uploadFiles } from "../services/uploadService";
import TagInput from "../components/TagInput";
import ForumImageUpload from "../components/ForumImageUpload";
import { Address } from "../types";
@@ -151,36 +152,53 @@ const CreateForumPost: React.FC = () => {
try {
setIsSubmitting(true);
// Create FormData
const submitData = new FormData();
submitData.append('title', formData.title);
submitData.append('content', formData.content);
submitData.append('category', formData.category);
// Upload images to S3 first (if any)
let imageFilenames: string[] = [];
if (imageFiles.length > 0) {
const uploadResults = await uploadFiles("forum", imageFiles);
imageFilenames = uploadResults.map((result) => result.key);
}
// Add tags as JSON string
// Build the post data
const postData: {
title: string;
content: string;
category: string;
tags?: string[];
zipCode?: string;
latitude?: number;
longitude?: number;
imageFilenames?: string[];
} = {
title: formData.title,
content: formData.content,
category: formData.category,
};
// Add tags if present
if (formData.tags.length > 0) {
submitData.append('tags', JSON.stringify(formData.tags));
postData.tags = formData.tags;
}
// Add location data for item requests
if (formData.category === 'item_request' && formData.zipCode) {
submitData.append('zipCode', formData.zipCode);
postData.zipCode = formData.zipCode;
// If we have coordinates from a saved address, send them to avoid re-geocoding
if (formData.latitude !== undefined && formData.longitude !== undefined) {
submitData.append('latitude', formData.latitude.toString());
submitData.append('longitude', formData.longitude.toString());
postData.latitude = formData.latitude;
postData.longitude = formData.longitude;
}
}
// Add images
imageFiles.forEach((file) => {
submitData.append('images', file);
});
// Add S3 image keys
if (imageFilenames.length > 0) {
postData.imageFilenames = imageFilenames;
}
const response = await forumAPI.createPost(submitData);
const response = await forumAPI.createPost(postData);
navigate(`/forum/${response.data.id}`);
} catch (err: any) {
setError(err.response?.data?.error || "Failed to create post");
setError(err.response?.data?.error || err.message || "Failed to create post");
setIsSubmitting(false);
}
};

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import api, { addressAPI, userAPI, itemAPI } from "../services/api";
import { uploadFiles } from "../services/uploadService";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ImageUpload from "../components/ImageUpload";
import ItemInformation from "../components/ItemInformation";
@@ -175,9 +176,12 @@ const CreateItem: React.FC = () => {
}
try {
// For now, we'll store image URLs as base64 strings
// In production, you'd upload to a service like S3
const imageUrls = imagePreviews;
// Upload images to S3 first
let imageFilenames: string[] = [];
if (imageFiles.length > 0) {
const uploadResults = await uploadFiles("item", imageFiles);
imageFilenames = uploadResults.map((result) => result.key);
}
// Construct location from address components
const locationParts = [
@@ -216,7 +220,7 @@ const CreateItem: React.FC = () => {
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
location,
images: imageUrls,
imageFilenames,
});
// Auto-save address if user has no addresses and entered manual address
@@ -260,7 +264,7 @@ const CreateItem: React.FC = () => {
navigate(`/items/${response.data.id}`);
} catch (err: any) {
setError(err.response?.data?.error || "Failed to create listing");
setError(err.response?.data?.error || err.message || "Failed to create listing");
} finally {
setLoading(false);
}

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { Item, Rental, Address } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI, addressAPI, userAPI } from "../services/api";
import { uploadFiles, getPublicImageUrl } from "../services/uploadService";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ImageUpload from "../components/ImageUpload";
import ItemInformation from "../components/ItemInformation";
@@ -53,6 +54,7 @@ const EditItem: React.FC = () => {
const [success, setSuccess] = useState(false);
const [imageFiles, setImageFiles] = useState<File[]>([]);
const [imagePreviews, setImagePreviews] = useState<string[]>([]);
const [existingImageKeys, setExistingImageKeys] = useState<string[]>([]); // S3 keys for existing images
const [acceptedRentals, setAcceptedRentals] = useState<Rental[]>([]);
const [userAddresses, setUserAddresses] = useState<Address[]>([]);
const [selectedAddressId, setSelectedAddressId] = useState<string>("");
@@ -161,9 +163,11 @@ const EditItem: React.FC = () => {
},
});
// Set existing images as previews
// Set existing images - store S3 keys and generate preview URLs
if (item.imageFilenames && item.imageFilenames.length > 0) {
setImagePreviews(item.imageFilenames);
setExistingImageKeys(item.imageFilenames);
// Generate preview URLs from S3 keys
setImagePreviews(item.imageFilenames.map((key: string) => getPublicImageUrl(key)));
}
// Determine which pricing unit to select based on existing data
@@ -270,8 +274,15 @@ const EditItem: React.FC = () => {
}
try {
// Use existing image previews (which includes both old and new images)
const imageUrls = imagePreviews;
// Upload new images to S3 and get their keys
let newImageKeys: string[] = [];
if (imageFiles.length > 0) {
const uploadResults = await uploadFiles("item", imageFiles);
newImageKeys = uploadResults.map((result) => result.key);
}
// Combine existing S3 keys with newly uploaded keys
const allImageKeys = [...existingImageKeys, ...newImageKeys];
const updatePayload = {
...formData,
@@ -297,7 +308,7 @@ const EditItem: React.FC = () => {
availableBefore: formData.generalAvailableBefore,
specifyTimesPerDay: formData.specifyTimesPerDay,
weeklyTimes: formData.weeklyTimes,
images: imageUrls,
imageFilenames: allImageKeys,
};
await itemAPI.updateItem(id!, updatePayload);
@@ -328,7 +339,7 @@ const EditItem: React.FC = () => {
navigate(`/items/${id}`);
}, 1500);
} catch (err: any) {
setError(err.response?.data?.message || "Failed to update item");
setError(err.response?.data?.message || err.message || "Failed to update item");
}
};
@@ -355,6 +366,16 @@ const EditItem: React.FC = () => {
};
const removeImage = (index: number) => {
// Check if removing an existing image or a new upload
if (index < existingImageKeys.length) {
// Removing an existing S3 image
setExistingImageKeys((prev) => prev.filter((_, i) => i !== index));
} else {
// Removing a new upload - adjust index for the imageFiles array
const newFileIndex = index - existingImageKeys.length;
setImageFiles((prev) => prev.filter((_, i) => i !== newFileIndex));
}
// Always update previews
setImagePreviews((prev) => prev.filter((_, i) => i !== index));
};

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, Link, useSearchParams } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { forumAPI, getForumImageUrl } from '../services/api';
import { forumAPI } from '../services/api';
import { uploadFiles, getPublicImageUrl } from '../services/uploadService';
import { ForumPost, ForumComment } from '../types';
import CategoryBadge from '../components/CategoryBadge';
import PostStatusBadge from '../components/PostStatusBadge';
@@ -54,17 +55,20 @@ const ForumPostDetail: React.FC = () => {
}
try {
const formData = new FormData();
formData.append('content', content);
// Upload images to S3 first (if any)
let imageFilenames: string[] = [];
if (images.length > 0) {
const uploadResults = await uploadFiles("forum", images);
imageFilenames = uploadResults.map((result) => result.key);
}
images.forEach((file) => {
formData.append('images', file);
await forumAPI.createComment(id!, {
content,
imageFilenames: imageFilenames.length > 0 ? imageFilenames : undefined,
});
await forumAPI.createComment(id!, formData);
await fetchPost(); // Refresh to get new comment
} catch (err: any) {
throw new Error(err.response?.data?.error || 'Failed to post comment');
throw new Error(err.response?.data?.error || err.message || 'Failed to post comment');
}
};
@@ -75,14 +79,13 @@ const ForumPostDetail: React.FC = () => {
}
try {
const formData = new FormData();
formData.append('content', content);
formData.append('parentCommentId', parentCommentId);
await forumAPI.createComment(id!, formData);
await forumAPI.createComment(id!, {
content,
parentId: parentCommentId,
});
await fetchPost(); // Refresh to get new reply
} catch (err: any) {
throw new Error(err.response?.data?.error || 'Failed to post reply');
throw new Error(err.response?.data?.error || err.message || 'Failed to post reply');
}
};
@@ -348,11 +351,11 @@ const ForumPostDetail: React.FC = () => {
{post.imageFilenames.map((image, index) => (
<div key={index} className="col-6 col-md-4">
<img
src={getForumImageUrl(image)}
src={getPublicImageUrl(image)}
alt={`Post image`}
className="img-fluid rounded"
style={{ width: '100%', height: '200px', objectFit: 'cover', cursor: 'pointer' }}
onClick={() => window.open(getForumImageUrl(image), '_blank')}
onClick={() => window.open(getPublicImageUrl(image), '_blank')}
/>
</div>
))}

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { Item, Rental } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI } from "../services/api";
import { getPublicImageUrl } from "../services/uploadService";
import GoogleMapWithRadius from "../components/GoogleMapWithRadius";
import ItemReviews from "../components/ItemReviews";
import ConfirmationModal from "../components/ConfirmationModal";
@@ -417,13 +418,14 @@ const ItemDetail: React.FC = () => {
{item.imageFilenames.length > 0 ? (
<div className="mb-4">
<img
src={item.imageFilenames[selectedImage]}
src={getPublicImageUrl(item.imageFilenames[selectedImage])}
alt={item.name}
className="img-fluid rounded mb-3"
style={{
width: "100%",
maxHeight: "500px",
objectFit: "cover",
objectFit: "contain",
backgroundColor: "#f8f9fa",
}}
/>
{item.imageFilenames.length > 1 && (
@@ -431,7 +433,7 @@ const ItemDetail: React.FC = () => {
{item.imageFilenames.map((image, index) => (
<img
key={index}
src={image}
src={getPublicImageUrl(image)}
alt={`${item.name} ${index + 1}`}
className={`rounded cursor-pointer ${
selectedImage === index

View File

@@ -4,6 +4,7 @@ import { useAuth } from "../contexts/AuthContext";
import api from "../services/api";
import { Item, Rental } from "../types";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import { getPublicImageUrl } from "../services/uploadService";
import ReviewRenterModal from "../components/ReviewRenterModal";
import RentalCancellationModal from "../components/RentalCancellationModal";
import DeclineRentalModal from "../components/DeclineRentalModal";
@@ -308,10 +309,10 @@ const Owning: React.FC = () => {
<div className="card h-100">
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={rental.item.imageFilenames[0]}
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{ height: "200px", objectFit: "cover" }}
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
/>
)}
<div className="card-body">
@@ -529,10 +530,10 @@ const Owning: React.FC = () => {
>
{item.imageFilenames && item.imageFilenames[0] && (
<img
src={item.imageFilenames[0]}
src={getPublicImageUrl(item.imageFilenames[0])}
className="card-img-top"
alt={item.name}
style={{ height: "200px", objectFit: "cover" }}
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
/>
)}
<div className="card-body">

View File

@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { userAPI, itemAPI, rentalAPI, addressAPI } from "../services/api";
import { User, Item, Rental, Address } from "../types";
import { getImageUrl } from "../utils/imageUrl";
import { uploadFile, getPublicImageUrl } from "../services/uploadService";
import AvailabilitySettings from "../components/AvailabilitySettings";
import ReviewItemModal from "../components/ReviewModal";
import ReviewRenterModal from "../components/ReviewRenterModal";
@@ -161,7 +161,7 @@ const Profile: React.FC = () => {
response.data.itemRequestNotificationRadius || 10,
});
if (response.data.imageFilename) {
setImagePreview(getImageUrl(response.data.imageFilename));
setImagePreview(getPublicImageUrl(response.data.imageFilename));
}
} catch (err: any) {
setError(err.response?.data?.message || "Failed to fetch profile");
@@ -301,29 +301,26 @@ const Profile: React.FC = () => {
};
reader.readAsDataURL(file);
// Upload image immediately
// Upload image to S3
try {
const formData = new FormData();
formData.append("imageFilename", file);
const { key, publicUrl } = await uploadFile("profile", file);
const response = await userAPI.uploadProfileImage(formData);
// Update the imageFilename in formData with the new filename
// Update the imageFilename in formData with the S3 key
setFormData((prev) => ({
...prev,
imageFilename: response.data.filename,
imageFilename: key,
}));
// Update preview to use the uploaded image URL
setImagePreview(getImageUrl(response.data.imageUrl));
// Update preview to use the S3 URL
setImagePreview(publicUrl);
} catch (err: any) {
console.error("Image upload error:", err);
setError(err.response?.data?.error || "Failed to upload image");
setError(err.message || "Failed to upload image");
// Reset on error
setImageFile(null);
setImagePreview(
profileData?.imageFilename
? getImageUrl(profileData.imageFilename)
? getPublicImageUrl(profileData.imageFilename)
: null
);
}
@@ -384,7 +381,7 @@ const Profile: React.FC = () => {
profileData.itemRequestNotificationRadius || 10,
});
setImagePreview(
profileData.imageFilename ? getImageUrl(profileData.imageFilename) : null
profileData.imageFilename ? getPublicImageUrl(profileData.imageFilename) : null
);
}
};
@@ -1224,7 +1221,7 @@ const Profile: React.FC = () => {
<div className="card h-100">
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={rental.item.imageFilenames[0]}
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{
@@ -1361,7 +1358,7 @@ const Profile: React.FC = () => {
<div className="card h-100">
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={rental.item.imageFilenames[0]}
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { User, Item } from '../types';
import { userAPI, itemAPI } from '../services/api';
import { getPublicImageUrl } from '../services/uploadService';
import { useAuth } from '../contexts/AuthContext';
import ChatWindow from '../components/ChatWindow';
@@ -113,10 +114,10 @@ const PublicProfile: React.FC = () => {
>
{item.imageFilenames.length > 0 ? (
<img
src={item.imageFilenames[0]}
src={getPublicImageUrl(item.imageFilenames[0])}
className="card-img-top"
alt={item.name}
style={{ height: '200px', objectFit: 'cover' }}
style={{ height: '200px', objectFit: 'contain', backgroundColor: '#f8f9fa' }}
/>
) : (
<div className="bg-light d-flex align-items-center justify-content-center" style={{ height: '200px' }}>

View File

@@ -3,6 +3,7 @@ import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import { Item } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { itemAPI, rentalAPI } from "../services/api";
import { getPublicImageUrl } from "../services/uploadService";
import EmbeddedStripeCheckout from "../components/EmbeddedStripeCheckout";
const RentItem: React.FC = () => {
@@ -343,13 +344,14 @@ const RentItem: React.FC = () => {
<div className="card-body">
{item.imageFilenames && item.imageFilenames[0] && (
<img
src={item.imageFilenames[0]}
src={getPublicImageUrl(item.imageFilenames[0])}
alt={item.name}
className="img-fluid rounded mb-3"
style={{
width: "100%",
height: "150px",
objectFit: "cover",
objectFit: "contain",
backgroundColor: "#f8f9fa",
}}
/>
)}

View File

@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import { getPublicImageUrl } from "../services/uploadService";
import { Rental } from "../types";
import ReviewItemModal from "../components/ReviewModal";
import RentalCancellationModal from "../components/RentalCancellationModal";
@@ -232,10 +233,10 @@ const Renting: React.FC = () => {
>
{rental.item?.imageFilenames && rental.item.imageFilenames[0] && (
<img
src={rental.item.imageFilenames[0]}
src={getPublicImageUrl(rental.item.imageFilenames[0])}
className="card-img-top"
alt={rental.item.name}
style={{ height: "200px", objectFit: "cover" }}
style={{ height: "200px", objectFit: "contain", backgroundColor: "#f8f9fa" }}
/>
)}
<div className="card-body">