feat: enhance media handling with public bucket support
All checks were successful
Deploy to Production / deploy (push) Successful in 2m2s
All checks were successful
Deploy to Production / deploy (push) Successful in 2m2s
- Added public bucket integration for serving media files directly instead of through API proxy - Updated image picker to use new MediaLibrary component with improved UI/UX - Removed selectedImageKeys prop dependency from EditorShell and StepEdit components - Modified image upload endpoints to automatically copy files to public bucket when configured - Added fallback to API proxy URLs when public bucket copy fails - Improved image picker dialog
This commit is contained in:
parent
21e02bb34f
commit
a2a011dc8c
@ -208,7 +208,6 @@ export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack
|
||||
draftHtml={draft}
|
||||
onChangeDraft={setDraft}
|
||||
generatedDraft={generatedDraft}
|
||||
selectedImageKeys={genImageKeys}
|
||||
onAutoSave={triggerImmediateAutoSave}
|
||||
/>
|
||||
</StepContainer>
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, Button, Alert, Stack, Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, ListItemButton, ListItemText, CircularProgress, Typography, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import { Box, Button, Alert, Stack, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Typography, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import RichEditor, { type RichEditorHandle } from '../RichEditor';
|
||||
import StepHeader from './StepHeader';
|
||||
import MediaLibrary from '../MediaLibrary';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { generateAltText } from '../../services/ai';
|
||||
|
||||
@ -10,14 +11,12 @@ export default function StepEdit({
|
||||
draftHtml,
|
||||
onChangeDraft,
|
||||
generatedDraft,
|
||||
selectedImageKeys,
|
||||
onAutoSave,
|
||||
}: {
|
||||
editorRef: ForwardedRef<RichEditorHandle> | any;
|
||||
draftHtml: string;
|
||||
onChangeDraft: (html: string) => void;
|
||||
generatedDraft?: string;
|
||||
selectedImageKeys?: string[];
|
||||
onAutoSave?: () => void;
|
||||
}) {
|
||||
const [showImagePicker, setShowImagePicker] = useState(false);
|
||||
@ -34,7 +33,7 @@ export default function StepEdit({
|
||||
onChangeDraft(generatedDraft);
|
||||
};
|
||||
|
||||
const replacePlaceholder = async (placeholder: string, imageKey: string) => {
|
||||
const replacePlaceholder = async (placeholder: string, imageUrl: string) => {
|
||||
setGeneratingAltText(true);
|
||||
|
||||
try {
|
||||
@ -54,7 +53,6 @@ export default function StepEdit({
|
||||
setAltTextCache(prev => ({ ...prev, [placeholder]: cached }));
|
||||
}
|
||||
|
||||
const imageUrl = `/api/media/obj?key=${encodeURIComponent(imageKey)}`;
|
||||
const placeholderPattern = `{{IMAGE:${placeholder}}}`;
|
||||
|
||||
// Use figure/figcaption if caption exists, otherwise plain img
|
||||
@ -76,7 +74,6 @@ export default function StepEdit({
|
||||
} catch (err) {
|
||||
console.error('Failed to generate image text:', err);
|
||||
// Fallback to simple alt text
|
||||
const imageUrl = `/api/media/obj?key=${encodeURIComponent(imageKey)}`;
|
||||
const placeholderPattern = `{{IMAGE:${placeholder}}}`;
|
||||
const fallbackAlt = placeholder.replace(/_/g, ' ');
|
||||
const replacement = `<img src="${imageUrl}" alt="${fallbackAlt}" />`;
|
||||
@ -160,7 +157,15 @@ export default function StepEdit({
|
||||
</Box>
|
||||
|
||||
{/* Image picker dialog */}
|
||||
<Dialog open={showImagePicker} onClose={() => !generatingAltText && setShowImagePicker(false)} maxWidth="sm" fullWidth>
|
||||
<Dialog
|
||||
open={showImagePicker}
|
||||
onClose={() => !generatingAltText && setShowImagePicker(false)}
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: { height: '90vh' }
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Box>
|
||||
Replace: {`{{IMAGE:${currentPlaceholder}}}`}
|
||||
@ -183,33 +188,15 @@ export default function StepEdit({
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{selectedImageKeys && selectedImageKeys.length > 0 ? (
|
||||
<List>
|
||||
{selectedImageKeys.map((key) => (
|
||||
<ListItem key={key} disablePadding>
|
||||
<ListItemButton onClick={() => replacePlaceholder(currentPlaceholder, key)}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
|
||||
<img
|
||||
src={`/api/media/obj?key=${encodeURIComponent(key)}`}
|
||||
alt=""
|
||||
style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={key.split('/').pop()}
|
||||
secondary={key}
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ p: 2, height: '100%', overflow: 'auto' }}>
|
||||
<MediaLibrary
|
||||
onInsert={(url) => replacePlaceholder(currentPlaceholder, url)}
|
||||
/>
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Alert severity="warning">No images selected. Go to Assets step to select images.</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setShowImagePicker(false)}>Cancel</Button>
|
||||
<Button onClick={() => setShowImagePicker(false)} disabled={generatingAltText}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import crypto from 'crypto';
|
||||
import { uploadBuffer, downloadObject, listObjects, deleteObject as s3DeleteObject } from './storage/s3';
|
||||
import { uploadBuffer, downloadObject, listObjects, deleteObject as s3DeleteObject, copyObject, getPublicUrlForKey } from './storage/s3';
|
||||
import { db } from './db';
|
||||
import { audioClips, posts } from './db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
@ -86,10 +86,31 @@ router.post('/image', upload.single('image'), async (
|
||||
contentType: mime,
|
||||
});
|
||||
|
||||
// Provide a proxied URL for immediate use in editor
|
||||
const url = `/api/media/obj?bucket=${encodeURIComponent(out.bucket)}&key=${encodeURIComponent(out.key)}`;
|
||||
// Copy to public bucket if configured
|
||||
const publicBucket = process.env.PUBLIC_MEDIA_BUCKET;
|
||||
const publicBaseUrl = process.env.PUBLIC_MEDIA_BASE_URL;
|
||||
let finalUrl = `/api/media/obj?bucket=${encodeURIComponent(out.bucket)}&key=${encodeURIComponent(out.key)}`;
|
||||
|
||||
if (publicBucket && publicBaseUrl) {
|
||||
try {
|
||||
await copyObject({
|
||||
srcBucket: bucket,
|
||||
srcKey: key,
|
||||
destBucket: publicBucket,
|
||||
destKey: key,
|
||||
});
|
||||
const publicUrl = getPublicUrlForKey(key);
|
||||
if (publicUrl) {
|
||||
finalUrl = publicUrl;
|
||||
console.log('[API] Image copied to public bucket, using public URL:', publicUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[API] Failed to copy image to public bucket, using API proxy URL:', err);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[API] Image upload success', out);
|
||||
return res.status(200).json({ success: true, ...out, url });
|
||||
return res.status(200).json({ success: true, ...out, url: finalUrl });
|
||||
} catch (err) {
|
||||
console.error('Image upload failed:', err);
|
||||
return res.status(500).json({ error: 'Image upload failed' });
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import express from 'express';
|
||||
import { uploadBuffer } from '../storage/s3';
|
||||
import { uploadBuffer, copyObject, getPublicUrlForKey } from '../storage/s3';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const router = express.Router();
|
||||
@ -219,7 +219,7 @@ router.post('/import', async (req, res) => {
|
||||
const filename = `pexels-${photoId}-${randomId}.${ext}`;
|
||||
const key = `images/${timestamp}/${filename}`;
|
||||
|
||||
// Upload to S3
|
||||
// Upload to S3 (private bucket)
|
||||
const bucket = process.env.S3_BUCKET || '';
|
||||
await uploadBuffer({
|
||||
bucket,
|
||||
@ -230,11 +230,34 @@ router.post('/import', async (req, res) => {
|
||||
|
||||
console.log('[Stock Photos] Imported successfully:', key);
|
||||
|
||||
// Copy to public bucket if configured
|
||||
const publicBucket = process.env.PUBLIC_MEDIA_BUCKET;
|
||||
const publicBaseUrl = process.env.PUBLIC_MEDIA_BASE_URL;
|
||||
let finalUrl = `/api/media/obj?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}`;
|
||||
|
||||
if (publicBucket && publicBaseUrl) {
|
||||
try {
|
||||
await copyObject({
|
||||
srcBucket: bucket,
|
||||
srcKey: key,
|
||||
destBucket: publicBucket,
|
||||
destKey: key,
|
||||
});
|
||||
const publicUrl = getPublicUrlForKey(key);
|
||||
if (publicUrl) {
|
||||
finalUrl = publicUrl;
|
||||
console.log('[Stock Photos] Copied to public bucket, using public URL:', publicUrl);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Stock Photos] Failed to copy to public bucket, using API proxy URL:', err);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
key,
|
||||
bucket,
|
||||
url: `/api/media/obj?bucket=${encodeURIComponent(bucket)}&key=${encodeURIComponent(key)}`,
|
||||
url: finalUrl,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('[Stock Photos] Import error:', err);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user