feat: Return S3 keys from image uploads and resolve them for display previews in the CMS.
This commit is contained in:
@@ -289,15 +289,14 @@ export default function CmsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic image upload — returns the presigned download URL
|
// Generic image upload — returns the S3 key for permanent storage
|
||||||
async function handleImageUpload(file: File): Promise<string> {
|
async function handleImageUpload(file: File): Promise<string> {
|
||||||
setIsUploadingImage(true);
|
setIsUploadingImage(true);
|
||||||
setModalError('');
|
setModalError('');
|
||||||
try {
|
try {
|
||||||
const { uploadUrl, key } = await uploadService.getPresignedUploadUrl(file.name, file.type, 'properties');
|
const { uploadUrl, key } = await uploadService.getPresignedUploadUrl(file.name, file.type, 'properties');
|
||||||
await uploadService.uploadFileToS3(uploadUrl, file);
|
await uploadService.uploadFileToS3(uploadUrl, file);
|
||||||
const downloadUrl = await uploadService.getPresignedDownloadUrl(key);
|
return key;
|
||||||
return downloadUrl;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setModalError(getErrorMessage(err));
|
setModalError(getErrorMessage(err));
|
||||||
return '';
|
return '';
|
||||||
@@ -306,6 +305,19 @@ export default function CmsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve an S3 key or URL for display — returns a viewable URL
|
||||||
|
async function resolveImageUrl(value: string): Promise<string> {
|
||||||
|
if (!value) return '';
|
||||||
|
// Already a full URL or local asset path — use directly
|
||||||
|
if (value.startsWith('http') || value.startsWith('/')) return value;
|
||||||
|
// Treat as S3 key — get presigned download URL
|
||||||
|
try {
|
||||||
|
return await uploadService.getPresignedDownloadUrl(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Section-specific form renderers
|
// Section-specific form renderers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -649,14 +661,27 @@ export default function CmsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reusable image upload field
|
// Reusable image upload field with S3 key resolution for preview
|
||||||
function renderImageUploadField(label: string, currentUrl: string, onUrlChange: (url: string) => void) {
|
function ImageUploadField({ label, currentUrl, onUrlChange }: { label: string; currentUrl: string; onUrlChange: (url: string) => void }) {
|
||||||
|
const [previewUrl, setPreviewUrl] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUrl) { setPreviewUrl(''); return; }
|
||||||
|
// If it's already a full URL or local path, use directly
|
||||||
|
if (currentUrl.startsWith('http') || currentUrl.startsWith('/')) {
|
||||||
|
setPreviewUrl(currentUrl);
|
||||||
|
} else {
|
||||||
|
// S3 key — resolve to presigned URL for preview
|
||||||
|
resolveImageUrl(currentUrl).then(setPreviewUrl);
|
||||||
|
}
|
||||||
|
}, [currentUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[#00293d] mb-1">{label}</label>
|
<label className="block text-sm font-medium text-[#00293d] mb-1">{label}</label>
|
||||||
{currentUrl && (
|
{previewUrl && (
|
||||||
<div className="mb-3 relative rounded-lg overflow-hidden border border-[#e5e7eb]">
|
<div className="mb-3 relative rounded-lg overflow-hidden border border-[#e5e7eb]">
|
||||||
<img src={currentUrl} alt="Preview" className="w-full h-48 object-cover" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
<img src={previewUrl} alt="Preview" className="w-full h-48 object-cover" onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }} />
|
||||||
<button type="button" onClick={() => onUrlChange('')} className="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors">Remove</button>
|
<button type="button" onClick={() => onUrlChange('')} className="absolute top-2 right-2 px-2 py-1 bg-red-600 text-white text-xs rounded-lg hover:bg-red-700 transition-colors">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -671,11 +696,15 @@ export default function CmsPage() {
|
|||||||
</label>
|
</label>
|
||||||
<span className="text-xs text-[#666666] self-center">or enter URL below</span>
|
<span className="text-xs text-[#666666] self-center">or enter URL below</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" className={inputCls} value={currentUrl} onChange={(e) => onUrlChange(e.target.value)} placeholder="https://..." />
|
<input type="text" className={inputCls} value={currentUrl} onChange={(e) => onUrlChange(e.target.value)} placeholder="S3 key or https://..." />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderImageUploadField(label: string, currentUrl: string, onUrlChange: (url: string) => void) {
|
||||||
|
return <ImageUploadField label={label} currentUrl={currentUrl} onUrlChange={onUrlChange} />;
|
||||||
|
}
|
||||||
|
|
||||||
// --- About Us form renderers ---
|
// --- About Us form renderers ---
|
||||||
|
|
||||||
function renderAboutHeroForm() {
|
function renderAboutHeroForm() {
|
||||||
|
|||||||
Reference in New Issue
Block a user