diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index d6e014f..f130494 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -32,6 +32,7 @@ import LazadaOrderPage from './pages/LazadaOrderPage' import LazadaAnalyticsPage from './pages/LazadaAnalyticsPage' import LazadaOverviewPage from './pages/LazadaOverviewPage' import GlobalAnalyticsPage from './pages/GlobalAnalyticsPage' +import MediaLibraryPage from './pages/MediaLibraryPage' function App() { return ( @@ -52,6 +53,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/admin/src/components/MediaLibraryModal.tsx b/apps/admin/src/components/MediaLibraryModal.tsx new file mode 100644 index 0000000..c21feab --- /dev/null +++ b/apps/admin/src/components/MediaLibraryModal.tsx @@ -0,0 +1,152 @@ +import React, { useState, useEffect } from 'react'; +import { API_BASE } from '@homaroma/types'; +import { X, Upload, Loader2, Image as ImageIcon } from 'lucide-react'; + +interface MediaLibraryModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (url: string) => void; +} + +interface MediaItem { + id?: string; + url: string; + filename?: string; +} + +export default function MediaLibraryModal({ isOpen, onClose, onSelect }: MediaLibraryModalProps) { + const [media, setMedia] = useState([]); + const [loading, setLoading] = useState(false); + const [uploading, setUploading] = useState(false); + + const fetchMedia = async () => { + try { + setLoading(true); + const res = await fetch(`${API_BASE}/media`); + if (res.ok) { + const data = await res.json(); + setMedia(data); + } + } catch (error) { + console.error('Failed to fetch media', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (isOpen) { + fetchMedia(); + } + }, [isOpen]); + + const handleUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); + + try { + setUploading(true); + const res = await fetch(`${API_BASE}/upload`, { + method: 'POST', + body: formData, + }); + if (res.ok) { + const data = await res.json(); + await fetchMedia(); // Refresh list + if (data.url) { + // Auto-select uploaded image (optional, or let user pick from list) + onSelect(data.url); + onClose(); + } + } + } catch (error) { + console.error('Failed to upload', error); + } finally { + setUploading(false); + e.target.value = ''; // Reset input + } + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+

+ + Media Library +

+

Select or upload images for your content.

+
+ +
+ + {/* Actions */} +
+ +
+ {media.length} items found +
+
+ + {/* Grid */} +
+ {loading ? ( +
+ +

Loading Media...

+
+ ) : ( +
+ {media.map((item, idx) => ( +
{ + onSelect(item.url); + onClose(); + }} + > +
+ {item.filename +
+ {item.filename && ( +
+ {item.filename} +
+ )} +
+ ))} + {media.length === 0 && !loading && ( +
+ +

No images found

+

Upload your first image to get started.

+
+ )} +
+ )} +
+
+
+ ); +} diff --git a/apps/admin/src/components/Sidebar.tsx b/apps/admin/src/components/Sidebar.tsx index 2dde2b6..084861b 100644 --- a/apps/admin/src/components/Sidebar.tsx +++ b/apps/admin/src/components/Sidebar.tsx @@ -15,7 +15,8 @@ import { BarChart3, Layers, Globe2, - Users + Users, + Image as ImageIcon } from 'lucide-react' const MENU_ITEMS = [ @@ -28,6 +29,7 @@ const MENU_ITEMS = [ { icon: Ticket, label: 'Promotions', to: '/promotions' }, { icon: Ticket, label: 'Coupons', to: '/coupons' }, { icon: Layers, label: 'Page Builder (CMS)', to: '/page-builder' }, + { icon: ImageIcon, label: 'Media Library', to: '/media' }, { icon: BookOpen, label: 'Scent Guide', to: '/articles' }, { icon: TrendingUp, label: 'TikTok Data', to: '/tiktok' }, { icon: TrendingUp, label: 'TikTok Analysis', to: '/tiktok-analysis' }, diff --git a/apps/admin/src/pages/ArticleEditPage.tsx b/apps/admin/src/pages/ArticleEditPage.tsx index 2343db9..d812b6c 100644 --- a/apps/admin/src/pages/ArticleEditPage.tsx +++ b/apps/admin/src/pages/ArticleEditPage.tsx @@ -4,6 +4,7 @@ import { API_BASE } from '@homaroma/types' import { ArrowLeft, Save, Loader2, Image as ImageIcon } from 'lucide-react' import ReactQuill from 'react-quill-new' import 'react-quill-new/dist/quill.snow.css' +import MediaLibraryModal from '../components/MediaLibraryModal' export default function ArticleEditPage() { const { id } = useParams<{ id: string }>() @@ -21,6 +22,9 @@ export default function ArticleEditPage() { const [content, setContent] = useState('') const [isPublished, setIsPublished] = useState(false) + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) + const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null) + useEffect(() => { if (isNew) return fetch(`${API_BASE}/articles/id/${id}`) @@ -43,24 +47,10 @@ export default function ArticleEditPage() { } }, [title, isNew]) - const handleImageUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0] - if (!file) return - setUploading(true) - const formData = new FormData() - formData.append('file', file) - try { - const res = await fetch(`${API_BASE}/upload`, { method: 'POST', body: formData }) - const data = await res.json() - if (data.url) setCoverImage(data.url) - } catch (err) { - console.error(err) - alert('Upload failed') - } finally { - setUploading(false) - } + const openMediaModal = (onSelect: (url: string) => void) => { + setOnSelectMedia(() => onSelect) + setIsMediaModalOpen(true) } - const handleSave = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) @@ -82,35 +72,13 @@ export default function ArticleEditPage() { } const imageHandler = () => { - const url = prompt('Please enter the Image URL: \n(Or leave blank and click OK to upload from your computer)') - - if (url) { + openMediaModal((url) => { const quill = quillRef.current?.getEditor() if (quill) { const range = quill.getSelection(true) quill.insertEmbed(range?.index || 0, 'image', url) } - } else if (url === "") { - const input = document.createElement('input') - input.setAttribute('type', 'file') - input.setAttribute('accept', 'image/*') - input.click() - - input.onchange = () => { - const file = input.files?.[0] - if (file) { - const reader = new FileReader() - reader.onload = (e) => { - const quill = quillRef.current?.getEditor() - if (quill) { - const range = quill.getSelection(true) - quill.insertEmbed(range?.index || 0, 'image', e.target?.result) - } - } - reader.readAsDataURL(file) - } - } - } + }) } const modules = useMemo(() => ({ @@ -171,16 +139,18 @@ export default function ArticleEditPage() {
-
+
openMediaModal((url) => setCoverImage(url))} + > {coverImage ? ( Cover ) : (
- {uploading ? : } -

Upload Cover

+ +

Select Cover Image

)} -
@@ -236,6 +206,14 @@ export default function ArticleEditPage() { + + setIsMediaModalOpen(false)} + onSelect={(url) => { + if (onSelectMedia) onSelectMedia(url) + }} + /> ) } diff --git a/apps/admin/src/pages/CategoriesPage.tsx b/apps/admin/src/pages/CategoriesPage.tsx index cca8c35..66b2946 100644 --- a/apps/admin/src/pages/CategoriesPage.tsx +++ b/apps/admin/src/pages/CategoriesPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { API_BASE, getCategoryImage, type Category } from '@homaroma/types' import { Edit3, Package, Trash2, Plus, X, Image as ImageIcon, UploadCloud, Loader2, ClipboardPaste, LayoutDashboard } from 'lucide-react' +import MediaLibraryModal from '../components/MediaLibraryModal' export default function CategoriesPage() { const [categories, setCategories] = useState([]) @@ -10,6 +11,9 @@ export default function CategoriesPage() { const [editingCategory, setEditingCategory] = useState | null>(null) const [activeLang, setActiveLang] = useState('th') const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'] + + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) + const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null) const fetchData = async () => { setLoading(true) @@ -246,7 +250,10 @@ export default function CategoriesPage() {
document.getElementById('category-upload')?.click()} + onClick={() => { + setOnSelectMedia(() => (url: string) => setEditingCategory(prev => ({ ...prev, imageUrl: url }))) + setIsMediaModalOpen(true) + }} > {editingCategory.imageUrl ? ( <> @@ -267,16 +274,6 @@ export default function CategoriesPage() {
)} - { - const file = e.target.files?.[0] - if (file) handleUpload(file) - }} - /> @@ -335,6 +332,14 @@ export default function CategoriesPage() { ) )} + + setIsMediaModalOpen(false)} + onSelect={(url) => { + if (onSelectMedia) onSelectMedia(url) + }} + /> ) } diff --git a/apps/admin/src/pages/HeroSettingsPage.tsx b/apps/admin/src/pages/HeroSettingsPage.tsx index 88a7e13..9ff9a09 100644 --- a/apps/admin/src/pages/HeroSettingsPage.tsx +++ b/apps/admin/src/pages/HeroSettingsPage.tsx @@ -15,6 +15,7 @@ import { ClipboardPaste } from 'lucide-react' import { API_BASE, type Hero } from '@homaroma/types' +import MediaLibraryModal from '../components/MediaLibraryModal' export default function HeroSettingsPage() { const [heroes, setHeroes] = useState([]) @@ -22,6 +23,9 @@ export default function HeroSettingsPage() { const [isModalOpen, setIsModalOpen] = useState(false) const [isUploading, setIsUploading] = useState(false) const [editingHero, setEditingHero] = useState | null>(null) + + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) + const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null) const fetchData = async () => { setLoading(true) @@ -310,7 +314,10 @@ export default function HeroSettingsPage() {
document.getElementById('hero-upload')?.click()} + onClick={() => { + setOnSelectMedia(() => (url: string) => setEditingHero(prev => ({ ...prev, imageUrl: url }))) + setIsMediaModalOpen(true) + }} > {editingHero.imageUrl ? ( <> @@ -331,16 +338,6 @@ export default function HeroSettingsPage() {
)} - { - const file = e.target.files?.[0] - if (file) handleUpload(file) - }} - />
@@ -431,6 +428,14 @@ export default function HeroSettingsPage() {
) )} + + setIsMediaModalOpen(false)} + onSelect={(url) => { + if (onSelectMedia) onSelectMedia(url) + }} + /> ) } diff --git a/apps/admin/src/pages/MediaLibraryPage.tsx b/apps/admin/src/pages/MediaLibraryPage.tsx new file mode 100644 index 0000000..f3632d5 --- /dev/null +++ b/apps/admin/src/pages/MediaLibraryPage.tsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react'; +import { API_BASE } from '@homaroma/types'; +import { Upload, Loader2, Image as ImageIcon, Trash2 } from 'lucide-react'; + +interface MediaItem { + id?: string; + url: string; + filename?: string; +} + +export default function MediaLibraryPage() { + const [media, setMedia] = useState([]); + const [loading, setLoading] = useState(false); + const [uploading, setUploading] = useState(false); + const [deletingId, setDeletingId] = useState(null); + + const fetchMedia = async () => { + try { + setLoading(true); + const res = await fetch(`${API_BASE}/media`); + if (res.ok) { + const data = await res.json(); + setMedia(data); + } + } catch (error) { + console.error('Failed to fetch media', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchMedia(); + }, []); + + const handleUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); + + try { + setUploading(true); + const res = await fetch(`${API_BASE}/upload`, { + method: 'POST', + body: formData, + }); + if (res.ok) { + await fetchMedia(); // Refresh list + } + } catch (error) { + console.error('Failed to upload', error); + } finally { + setUploading(false); + e.target.value = ''; // Reset input + } + }; + + const handleDelete = async (id: string | undefined, e: React.MouseEvent) => { + e.stopPropagation(); + if (!id) return; + + if (!window.confirm('Are you sure you want to delete this image?')) return; + + try { + setDeletingId(id); + const res = await fetch(`${API_BASE}/media/${id}`, { + method: 'DELETE', + }); + if (res.ok) { + await fetchMedia(); + } else { + alert('Failed to delete image'); + } + } catch (error) { + console.error('Failed to delete', error); + alert('An error occurred while deleting'); + } finally { + setDeletingId(null); + } + }; + + const copyToClipboard = (url: string, e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(url); + alert('Image URL copied to clipboard!'); + }; + + return ( +
+
+
+

+ + Media Library +

+

Manage your uploaded images

+
+ + +
+ +
+ {loading ? ( +
+ +

Loading Media...

+
+ ) : ( +
+ {media.map((item, idx) => ( +
copyToClipboard(item.url, e)} + > +
+ {item.filename + {/* Hover Overlay */} +
+ Copy URL +
+
+ {item.filename && ( +
+ {item.filename} +
+ )} + {item.id && ( + + )} +
+ ))} + {media.length === 0 && !loading && ( +
+ +

No images found

+

Upload your first image to get started.

+
+ )} +
+ )} +
+
+ ); +} diff --git a/apps/admin/src/pages/PageBuilderPage.tsx b/apps/admin/src/pages/PageBuilderPage.tsx index c14afa7..c45cb29 100644 --- a/apps/admin/src/pages/PageBuilderPage.tsx +++ b/apps/admin/src/pages/PageBuilderPage.tsx @@ -19,6 +19,7 @@ import { } from 'lucide-react'; import { API_BASE, type Page, type PageSection, type Category } from '@homaroma/types'; import { getAdminDisplayString } from '../utils/localization'; +import MediaLibraryModal from '../components/MediaLibraryModal'; interface Scent { id: number; @@ -38,6 +39,25 @@ export default function PageBuilderPage() { const [uploadTarget, setUploadTarget] = useState<{ type: 'main' | 'slide'; index?: number } | null>(null); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false); + + const handleMediaSelect = (url: string) => { + if (uploadTarget?.type === 'slide' && typeof uploadTarget.index === 'number') { + const idx = uploadTarget.index; + setEditingSection(prev => { + if (!prev) return null; + const currentSlides = [...(prev.payload?.slides || [])]; + currentSlides[idx] = { ...currentSlides[idx], imageUrl: url }; + return { ...prev, payload: { ...(prev.payload || {}), slides: currentSlides } }; + }); + } else { + setEditingSection(prev => { + if (!prev) return null; + return { ...prev, payload: { ...(prev.payload || {}), imageUrl: url } }; + }); + } + }; + const [editingLang, setEditingLang] = useState('th'); // Helper to read localized string safely @@ -933,7 +953,7 @@ export default function PageBuilderPage() { className="relative aspect-video rounded-2xl overflow-hidden border-2 border-dashed border-white/10 bg-night-950/50 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all" onClick={() => { setUploadTarget({ type: 'main' }); - document.getElementById('section-upload')?.click(); + setIsMediaModalOpen(true); }} > {editingSection.payload?.imageUrl ? ( @@ -949,11 +969,6 @@ export default function PageBuilderPage() {

Select Image

)} - {isUploading && ( -
- -
- )} @@ -1120,7 +1135,7 @@ export default function PageBuilderPage() { className="relative aspect-video rounded-xl overflow-hidden border border-white/10 bg-night-950/30 flex flex-col items-center justify-center group cursor-pointer hover:border-cyan-glow/30 transition-all" onClick={() => { setUploadTarget({ type: 'slide', index: idx }); - document.getElementById('section-upload')?.click(); + setIsMediaModalOpen(true); }} > {slide.imageUrl ? ( @@ -1394,7 +1409,10 @@ export default function PageBuilderPage() {
document.getElementById('section-upload')?.click()} + onClick={() => { + setUploadTarget({ type: 'main' }); + setIsMediaModalOpen(true); + }} > {editingSection.payload?.imageUrl ? ( <> @@ -1409,21 +1427,6 @@ export default function PageBuilderPage() {

Select Image

)} - {isUploading && ( -
- -
- )} - { - const file = e.target.files?.[0]; - if (file) handleUpload(file); - }} - /> @@ -1701,7 +1704,10 @@ export default function PageBuilderPage() {
document.getElementById('section-upload')?.click()} + onClick={() => { + setUploadTarget({ type: 'main' }); + setIsMediaModalOpen(true); + }} > {editingSection.payload?.imageUrl ? ( <> @@ -1716,21 +1722,6 @@ export default function PageBuilderPage() {

Select Image

)} - {isUploading && ( -
- -
- )} - { - const file = e.target.files?.[0]; - if (file) handleUpload(file); - }} - /> @@ -2111,6 +2102,12 @@ export default function PageBuilderPage() { )} + + setIsMediaModalOpen(false)} + onSelect={handleMediaSelect} + /> ); } diff --git a/apps/admin/src/pages/ProductEditPage.tsx b/apps/admin/src/pages/ProductEditPage.tsx index 1a5505d..b460db3 100644 --- a/apps/admin/src/pages/ProductEditPage.tsx +++ b/apps/admin/src/pages/ProductEditPage.tsx @@ -19,6 +19,7 @@ import { import { API_BASE, getCategoryImage, formatPrice, type Category, type Product, type Variant, type ProductImage, type RawMaterial, type BomItem } from '@homaroma/types' import ReactQuill from 'react-quill-new' import 'react-quill-new/dist/quill.snow.css' +import MediaLibraryModal from '../components/MediaLibraryModal' export default function ProductEditPage() { const { id } = useParams<{ id: string }>() @@ -43,6 +44,9 @@ export default function ProductEditPage() { const [variants, setVariants] = useState[]>([]) const [images, setImages] = useState[]>([]) + const [isMediaModalOpen, setIsMediaModalOpen] = useState(false) + const [onSelectMedia, setOnSelectMedia] = useState<((url: string) => void) | null>(null) + const quillRef = useRef(null) const fixUrl = (url: string) => { @@ -50,40 +54,19 @@ export default function ProductEditPage() { return url.replace('/public/uploads/', '/uploads/') } + const openMediaModal = (onSelect: (url: string) => void) => { + setOnSelectMedia(() => onSelect) + setIsMediaModalOpen(true) + } + const imageHandler = () => { - const url = prompt('Please enter the Image URL: \n(Or leave blank and click OK to upload from your computer)') - - // If they typed something and clicked OK - if (url) { + openMediaModal((url) => { const quill = quillRef.current?.getEditor() if (quill) { const range = quill.getSelection(true) quill.insertEmbed(range?.index || 0, 'image', url) } - } - // If they left it blank and clicked OK - else if (url === "") { - const input = document.createElement('input') - input.setAttribute('type', 'file') - input.setAttribute('accept', 'image/*') - input.click() - - input.onchange = () => { - const file = input.files?.[0] - if (file) { - // Use standard base64 embedding for normal file uploads - const reader = new FileReader() - reader.onload = (e) => { - const quill = quillRef.current?.getEditor() - if (quill) { - const range = quill.getSelection(true) - quill.insertEmbed(range?.index || 0, 'image', e.target?.result) - } - } - reader.readAsDataURL(file) - } - } - } + }) } const modules = useMemo(() => ({ @@ -413,25 +396,13 @@ export default function ProductEditPage() {
- -
@@ -666,6 +637,14 @@ export default function ProductEditPage() { + + setIsMediaModalOpen(false)} + onSelect={(url) => { + if (onSelectMedia) onSelectMedia(url) + }} + /> ) } diff --git a/apps/backend/.env.example b/apps/backend/.env.example new file mode 100644 index 0000000..59c0184 --- /dev/null +++ b/apps/backend/.env.example @@ -0,0 +1 @@ +BACKEND_PUBLIC_URL="https://api.homaroma.shop" diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 8897e78..d5891a5 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -617,3 +617,12 @@ model MenuItemTranslation { @@unique([menuItemId, langCode]) } + +model Media { + id Int @id @default(autoincrement()) + filename String + path String + mimeType String + sizeBytes Int + createdAt DateTime @default(now()) +} diff --git a/apps/spa/public/images/hero.png b/apps/backend/public/assets/hero.png similarity index 100% rename from apps/spa/public/images/hero.png rename to apps/backend/public/assets/hero.png diff --git a/apps/spa/public/images/oil_ozone.png b/apps/backend/public/assets/oil_ozone.png similarity index 100% rename from apps/spa/public/images/oil_ozone.png rename to apps/backend/public/assets/oil_ozone.png diff --git a/apps/spa/public/images/reed_diffuser.png b/apps/backend/public/assets/reed_diffuser.png similarity index 100% rename from apps/spa/public/images/reed_diffuser.png rename to apps/backend/public/assets/reed_diffuser.png diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 18d042a..f73a699 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -26,6 +26,7 @@ import { profileRoute } from "./routes/profile"; import { lineRoutes } from "./routes/line"; import { menusRoute } from "./routes/menus"; import { newsletterRoute } from "./routes/newsletter"; +import { mediaRoute } from "./routes/media"; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173"; const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent"; @@ -99,7 +100,7 @@ const app = new Elysia() return file; }) .group("/api", (app) => - app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute) + app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute).use(mediaRoute) ) .listen({ port: PORT, hostname: "0.0.0.0" }); diff --git a/apps/backend/src/routes/media.ts b/apps/backend/src/routes/media.ts new file mode 100644 index 0000000..603765f --- /dev/null +++ b/apps/backend/src/routes/media.ts @@ -0,0 +1,42 @@ +import { Elysia } from "elysia"; +import prisma from "../lib/prisma"; + +export const mediaRoute = new Elysia({ prefix: "/media" }) + .get("/", async () => { + const media = await prisma.media.findMany({ + orderBy: { createdAt: "desc" } + }); + + const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || "http://localhost:3051"; + + return media.map(m => ({ + ...m, + url: `${BACKEND_PUBLIC_URL}${m.path}` + })); + }) + .delete("/:id", async ({ params, set }) => { + try { + const mediaId = parseInt(params.id); + + const media = await prisma.media.findUnique({ + where: { id: mediaId } + }); + + if (!media) { + set.status = 404; + return { error: "Media not found" }; + } + + await prisma.media.delete({ + where: { id: mediaId } + }); + + // We are not removing the physical file right now, but could be added later if needed. + + return { success: true, message: "Media deleted successfully" }; + } catch (err) { + console.error("[Media] Delete Error:", err); + set.status = 500; + return { error: "Failed to delete media" }; + } + }); diff --git a/apps/backend/src/routes/upload.ts b/apps/backend/src/routes/upload.ts index d58f8b3..b010a10 100644 --- a/apps/backend/src/routes/upload.ts +++ b/apps/backend/src/routes/upload.ts @@ -1,6 +1,7 @@ import { Elysia, t } from "elysia"; import { join } from "path"; import { mkdir } from "fs/promises"; +import prisma from "../lib/prisma"; export const uploadRoute = new Elysia({ prefix: "/upload" }) .post("/", async ({ body, set }) => { @@ -32,15 +33,29 @@ export const uploadRoute = new Elysia({ prefix: "/upload" }) // Write file using Bun.write await Bun.write(filePath, await file.arrayBuffer()); - const APP_URL = process.env.APP_URL || "http://localhost:3000"; - const publicUrl = `${APP_URL}/uploads/${filename}`; + const relativePath = `/uploads/${filename}`; + const mimeType = file.type; + const sizeBytes = file.size; + + await prisma.media.create({ + data: { + filename, + path: relativePath, + mimeType, + sizeBytes, + } + }); + + const BACKEND_PUBLIC_URL = process.env.BACKEND_PUBLIC_URL || "http://localhost:3051"; + const publicUrl = `${BACKEND_PUBLIC_URL}${relativePath}`; console.log(`[Upload] Success: ${filename} -> ${publicUrl}`); return { success: true, url: publicUrl, - filename + filename, + path: relativePath }; } catch (err) { console.error("[Upload] Error:", err); diff --git a/apps/spa/src/components/CartDrawer.tsx b/apps/spa/src/components/CartDrawer.tsx index 311c260..e96fc50 100644 --- a/apps/spa/src/components/CartDrawer.tsx +++ b/apps/spa/src/components/CartDrawer.tsx @@ -68,7 +68,7 @@ export default function CartDrawer() {
{item.productName} diff --git a/apps/spa/src/components/DynamicHero.tsx b/apps/spa/src/components/DynamicHero.tsx index 25f5d0f..de9cda6 100644 --- a/apps/spa/src/components/DynamicHero.tsx +++ b/apps/spa/src/components/DynamicHero.tsx @@ -38,7 +38,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
Hero Background { diff --git a/apps/spa/src/pages/OrderSuccessPage.tsx b/apps/spa/src/pages/OrderSuccessPage.tsx index 274f911..a2656bb 100644 --- a/apps/spa/src/pages/OrderSuccessPage.tsx +++ b/apps/spa/src/pages/OrderSuccessPage.tsx @@ -264,7 +264,7 @@ export default function OrderSuccessPage() { {order.items.map(item => (
- Product + Product

{item.productName}

diff --git a/apps/spa/src/pages/ProfilePage.tsx b/apps/spa/src/pages/ProfilePage.tsx index c613165..c469d6a 100644 --- a/apps/spa/src/pages/ProfilePage.tsx +++ b/apps/spa/src/pages/ProfilePage.tsx @@ -481,7 +481,7 @@ export default function ProfilePage() { {order.items.map(item => (
- Product + Product

{item.productName}