diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx index 707a76e..207e370 100644 --- a/apps/admin/src/components/EditorShell.tsx +++ b/apps/admin/src/components/EditorShell.tsx @@ -208,7 +208,6 @@ export default function EditorShell({ onLogout: _onLogout, initialPostId, onBack draftHtml={draft} onChangeDraft={setDraft} generatedDraft={generatedDraft} - selectedImageKeys={genImageKeys} onAutoSave={triggerImmediateAutoSave} /> diff --git a/apps/admin/src/components/steps/StepEdit.tsx b/apps/admin/src/components/steps/StepEdit.tsx index e321d64..facaddb 100644 --- a/apps/admin/src/components/steps/StepEdit.tsx +++ b/apps/admin/src/components/steps/StepEdit.tsx @@ -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 | 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 = `${fallbackAlt}`; @@ -160,7 +157,15 @@ export default function StepEdit({ {/* Image picker dialog */} - !generatingAltText && setShowImagePicker(false)} maxWidth="sm" fullWidth> + !generatingAltText && setShowImagePicker(false)} + maxWidth="lg" + fullWidth + PaperProps={{ + sx: { height: '90vh' } + }} + > Replace: {`{{IMAGE:${currentPlaceholder}}}`} @@ -183,33 +188,15 @@ export default function StepEdit({ sx={{ mt: 1 }} /> - - {selectedImageKeys && selectedImageKeys.length > 0 ? ( - - {selectedImageKeys.map((key) => ( - - replacePlaceholder(currentPlaceholder, key)}> - - - - - - - ))} - - ) : ( - No images selected. Go to Assets step to select images. - )} + + + replacePlaceholder(currentPlaceholder, url)} + /> + - + diff --git a/apps/api/src/media.ts b/apps/api/src/media.ts index 9af6327..a7eecfa 100644 --- a/apps/api/src/media.ts +++ b/apps/api/src/media.ts @@ -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' }); diff --git a/apps/api/src/routes/stock-photos.routes.ts b/apps/api/src/routes/stock-photos.routes.ts index 4855482..fb80eed 100644 --- a/apps/api/src/routes/stock-photos.routes.ts +++ b/apps/api/src/routes/stock-photos.routes.ts @@ -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);