image optimization. Image resizing client side, index added to db, pagination

This commit is contained in:
jackiettran
2025-12-30 20:23:32 -05:00
parent 3e31b9d08b
commit 807082eebf
25 changed files with 587 additions and 123 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { getPublicImageUrl } from "../services/uploadService";
import { getImageUrl } from "../services/uploadService";
interface AvatarUser {
id?: string;
@@ -98,8 +98,8 @@ const Avatar: React.FC<AvatarProps> = ({
}
const { firstName, lastName, imageFilename, id } = user;
// Use direct imageUrl if provided, otherwise construct from imageFilename
const imageUrl = directImageUrl || (imageFilename ? getPublicImageUrl(imageFilename) : null);
// Use direct imageUrl if provided, otherwise construct from imageFilename (use thumbnail for avatars)
const imageUrl = directImageUrl || (imageFilename ? getImageUrl(imageFilename, 'thumbnail') : null);
const hasValidImage = imageUrl && !imageError;
if (hasValidImage) {

View File

@@ -6,7 +6,7 @@ import React, {
useCallback,
} from "react";
import { messageAPI } from "../services/api";
import { getSignedUrl, uploadFile } from "../services/uploadService";
import { getSignedImageUrl, uploadImageWithVariants } from "../services/uploadService";
import { User, Message } from "../types";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
@@ -204,7 +204,8 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
const newUrls = new Map(imageUrls);
await Promise.all(
messagesWithImages.map(async (m) => {
const url = await getSignedUrl(m.imageFilename!);
// Use thumbnail size for chat previews
const url = await getSignedImageUrl(m.imageFilename!, 'thumbnail');
newUrls.set(m.imageFilename!, url);
})
);
@@ -374,11 +375,11 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
}
try {
// Upload image to S3 first if present
// Upload image to S3 first if present (with resizing)
let imageFilename: string | undefined;
if (imageToSend) {
const { key } = await uploadFile("message", imageToSend);
imageFilename = key;
const { baseKey } = await uploadImageWithVariants("message", imageToSend);
imageFilename = baseKey;
}
const response = await messageAPI.sendMessage({

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { ForumComment } from "../types";
import CommentForm from "./CommentForm";
import ForumImageUpload from "./ForumImageUpload";
import { getPublicImageUrl } from "../services/uploadService";
import { getImageUrl } from "../services/uploadService";
import { IMAGE_LIMITS } from "../config/imageLimits";
interface CommentThreadProps {
@@ -77,7 +77,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
setEditContent(comment.content);
const existingKeys = comment.imageFilenames || [];
setExistingImageKeys(existingKeys);
setEditImagePreviews(existingKeys.map((key) => getPublicImageUrl(key)));
setEditImagePreviews(existingKeys.map((key) => getImageUrl(key, 'thumbnail')));
setEditImageFiles([]);
};
@@ -280,7 +280,7 @@ const CommentThread: React.FC<CommentThreadProps> = ({
{comment.imageFilenames.map((image, index) => (
<div key={index} className="col-4 col-md-3">
<img
src={getPublicImageUrl(image)}
src={getImageUrl(image, 'thumbnail')}
alt={`Comment image`}
className="img-fluid rounded"
style={{
@@ -289,8 +289,15 @@ const CommentThread: React.FC<CommentThreadProps> = ({
objectFit: "contain",
cursor: "pointer",
}}
onError={(e) => {
const target = e.currentTarget;
if (!target.dataset.fallback) {
target.dataset.fallback = 'true';
target.src = getImageUrl(image, 'original');
}
}}
onClick={() =>
window.open(getPublicImageUrl(image), "_blank")
window.open(getImageUrl(image, 'original'), "_blank")
}
/>
</div>

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { conditionCheckAPI } from "../services/api";
import { uploadFiles } from "../services/uploadService";
import { uploadImagesWithVariants } from "../services/uploadService";
import { IMAGE_LIMITS } from "../config/imageLimits";
interface ConditionCheckModalProps {
@@ -84,9 +84,9 @@ const ConditionCheckModal: React.FC<ConditionCheckModalProps> = ({
setSubmitting(true);
setError(null);
// Upload photos to S3 first
const uploadResults = await uploadFiles("condition-check", photos);
const imageFilenames = uploadResults.map((result) => result.key);
// Upload photos to S3 first (with resizing)
const uploadResults = await uploadImagesWithVariants("condition-check", photos);
const imageFilenames = uploadResults.map((result) => result.baseKey);
// Submit condition check with S3 keys
await conditionCheckAPI.submitConditionCheck(rentalId, {

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { ConditionCheck } from "../types";
import { getSignedUrl } from "../services/uploadService";
import { getSignedImageUrl } from "../services/uploadService";
interface ConditionCheckViewerModalProps {
show: boolean;
@@ -51,7 +51,7 @@ const ConditionCheckViewerModal: React.FC<ConditionCheckViewerModalProps> = ({
try {
await Promise.all(
validKeys.map(async (key) => {
const url = await getSignedUrl(key);
const url = await getSignedImageUrl(key, 'medium');
newUrls.set(key, url);
})
);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Item } from '../types';
import { getPublicImageUrl } from '../services/uploadService';
import { getImageUrl } from '../services/uploadService';
interface ItemCardProps {
item: Item;
@@ -50,9 +50,18 @@ const ItemCard: React.FC<ItemCardProps> = ({
<div className="card h-100" style={{ cursor: 'pointer' }}>
{item.imageFilenames && item.imageFilenames[0] ? (
<img
src={getPublicImageUrl(item.imageFilenames[0])}
src={getImageUrl(item.imageFilenames[0], 'thumbnail')}
className="card-img-top"
alt={item.name}
loading="lazy"
onError={(e) => {
// Fallback to original for images uploaded before resizing was added
const target = e.currentTarget;
if (!target.dataset.fallback) {
target.dataset.fallback = 'true';
target.src = getImageUrl(item.imageFilenames[0], 'original');
}
}}
style={{
height: isCompact ? '150px' : '200px',
objectFit: 'contain',

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Item } from '../types';
import { getPublicImageUrl } from '../services/uploadService';
import { getImageUrl } from '../services/uploadService';
interface ItemMarkerInfoProps {
item: Item;
@@ -26,9 +26,16 @@ const ItemMarkerInfo: React.FC<ItemMarkerInfoProps> = ({ item, onViewDetails })
<div className="card border-0">
{item.imageFilenames && item.imageFilenames[0] ? (
<img
src={getPublicImageUrl(item.imageFilenames[0])}
src={getImageUrl(item.imageFilenames[0], 'thumbnail')}
className="card-img-top"
alt={item.name}
onError={(e) => {
const target = e.currentTarget;
if (!target.dataset.fallback) {
target.dataset.fallback = 'true';
target.src = getImageUrl(item.imageFilenames[0], 'original');
}
}}
style={{
height: '120px',
objectFit: 'contain',

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { rentalAPI, conditionCheckAPI } from "../services/api";
import { uploadFiles } from "../services/uploadService";
import { uploadImagesWithVariants } from "../services/uploadService";
import { Rental } from "../types";
interface ReturnStatusModalProps {
@@ -290,9 +290,9 @@ const ReturnStatusModal: React.FC<ReturnStatusModalProps> = ({
// Submit post-rental condition check if photos are provided
if (photos.length > 0) {
// Upload photos to S3 first
const uploadResults = await uploadFiles("condition-check", photos);
const imageFilenames = uploadResults.map((result) => result.key);
// Upload photos to S3 first (with resizing)
const uploadResults = await uploadImagesWithVariants("condition-check", photos);
const imageFilenames = uploadResults.map((result) => result.baseKey);
await conditionCheckAPI.submitConditionCheck(rental.id, {
checkType: "post_rental_owner",