mono/packages/ui/src/hooks/useResponsiveImage.ts
babayaga 8ec419b87e ui
2026-01-29 17:57:27 +01:00

146 lines
5.7 KiB
TypeScript

import { useState, useEffect } from 'react';
import { ResponsiveData } from '@/components/ResponsiveImage';
interface UseResponsiveImageProps {
src: string | File | null;
responsiveSizes?: number[];
formats?: string[];
enabled?: boolean;
apiUrl?: string;
}
// Module-level cache to deduplicate requests
// Key: stringified request params, Value: Promise<ResponsiveData>
const requestCache = new Map<string, Promise<ResponsiveData>>();
export const useResponsiveImage = ({
src,
responsiveSizes = [180, 640, 1024],
formats = ['avif', 'webp', 'jpeg'],
enabled = true,
apiUrl,
}: UseResponsiveImageProps) => {
const [data, setData] = useState<ResponsiveData | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const generateResponsiveImages = async () => {
if (!src || !enabled) {
if (isMounted) {
// Only reset if disabled or no src, but if we already have data we might want to keep it?
// actually if distinct src change, reset. if just disabled, maybe do nothing?
// For now, if disabled, we just don't start.
if (!src) {
setData(null);
setLoading(false);
}
}
return;
}
if (isMounted) {
setLoading(true);
setError(null);
}
try {
// Only cache string URLs (remote images)
// File objects are harder to cache reliably and usually come from user input anyway
if (typeof src === 'string') {
// Check for Data URI
if (src.startsWith('data:')) {
if (isMounted) {
setData({
img: {
src: src,
width: 0, // Unknown dimensions without parsing
height: 0,
format: 'unknown'
},
sources: [] // No alternative sources for raw data URI
});
setLoading(false);
}
return;
}
const cacheKey = JSON.stringify({ src, sizes: responsiveSizes, formats, apiUrl });
if (!requestCache.has(cacheKey)) {
const requestPromise = (async () => {
const formData = new FormData();
formData.append('url', src);
formData.append('sizes', JSON.stringify(responsiveSizes));
formData.append('formats', JSON.stringify(formats));
const serverUrl = apiUrl || import.meta.env.VITE_SERVER_IMAGE_API_URL || 'http://192.168.1.11:3333';
const response = await fetch(`${serverUrl}/api/images/responsive`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const txt = await response.text();
// Remove from cache on error so it can be retried
requestCache.delete(cacheKey);
throw new Error(txt || 'Failed to generate responsive images');
}
return response.json();
})();
requestCache.set(cacheKey, requestPromise);
}
const result = await requestCache.get(cacheKey)!;
if (isMounted) {
setData(result);
}
} else {
// Handle File objects (no caching)
const formData = new FormData();
formData.append('file', src);
formData.append('sizes', JSON.stringify(responsiveSizes));
formData.append('formats', JSON.stringify(formats));
const serverUrl = apiUrl || import.meta.env.VITE_SERVER_IMAGE_API_URL || '';
const response = await fetch(`${serverUrl}/api/images/responsive`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const txt = await response.text();
throw new Error(txt || 'Failed to generate responsive images');
}
const result = await response.json();
if (isMounted) {
setData(result);
}
}
} catch (err: any) {
console.error('Error generating responsive images:', err);
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
generateResponsiveImages();
return () => {
isMounted = false;
};
}, [src, JSON.stringify(responsiveSizes), JSON.stringify(formats), enabled]);
return { data, loading, error };
};