diff --git a/admin-spa/src/components/ui/image-upload.tsx b/admin-spa/src/components/ui/image-upload.tsx index ae1e7fb..bb13d39 100644 --- a/admin-spa/src/components/ui/image-upload.tsx +++ b/admin-spa/src/components/ui/image-upload.tsx @@ -2,6 +2,7 @@ import React, { useState, useRef } from 'react'; import { Upload, X, Image as ImageIcon } from 'lucide-react'; import { Button } from './button'; import { cn } from '@/lib/utils'; +import { openWPMediaImage, openWPMediaLogo, openWPMediaFavicon } from '@/lib/wp-media'; interface ImageUploadProps { value?: string; @@ -12,6 +13,7 @@ interface ImageUploadProps { accept?: string; maxSize?: number; // in MB className?: string; + mediaType?: 'image' | 'logo' | 'favicon'; // Type for WordPress Media Modal } export function ImageUpload({ @@ -23,6 +25,7 @@ export function ImageUpload({ accept = 'image/*', maxSize = 2, className, + mediaType = 'image', }: ImageUploadProps) { const [isDragging, setIsDragging] = useState(false); const [isUploading, setIsUploading] = useState(false); @@ -117,6 +120,16 @@ export function ImageUpload({ fileInputRef.current?.click(); }; + const handleWPMedia = () => { + const openMedia = mediaType === 'logo' ? openWPMediaLogo : + mediaType === 'favicon' ? openWPMediaFavicon : + openWPMediaImage; + + openMedia((file) => { + onChange(file.url); + }); + }; + return (
{label && ( @@ -191,6 +204,20 @@ export function ImageUpload({ Max size: {maxSize}MB

+
+ +
)} diff --git a/admin-spa/src/components/ui/rich-text-editor.tsx b/admin-spa/src/components/ui/rich-text-editor.tsx index 72d4620..a7589cf 100644 --- a/admin-spa/src/components/ui/rich-text-editor.tsx +++ b/admin-spa/src/components/ui/rich-text-editor.tsx @@ -6,6 +6,7 @@ import Link from '@tiptap/extension-link'; import TextAlign from '@tiptap/extension-text-align'; import Image from '@tiptap/extension-image'; import { ButtonExtension } from './tiptap-button-extension'; +import { openWPMediaImage } from '@/lib/wp-media'; import { Bold, Italic, @@ -113,10 +114,13 @@ export function RichTextEditor({ const [buttonStyle, setButtonStyle] = useState<'solid' | 'outline'>('solid'); const addImage = () => { - const url = window.prompt(__('Enter image URL:')); - if (url) { - editor.chain().focus().setImage({ src: url }).run(); - } + openWPMediaImage((file) => { + editor.chain().focus().setImage({ + src: file.url, + alt: file.alt || file.title, + title: file.title, + }).run(); + }); }; const openButtonDialog = () => { diff --git a/admin-spa/src/lib/wp-media.ts b/admin-spa/src/lib/wp-media.ts new file mode 100644 index 0000000..3a6bedf --- /dev/null +++ b/admin-spa/src/lib/wp-media.ts @@ -0,0 +1,161 @@ +/** + * WordPress Media Library Integration + * + * Provides a clean interface to WordPress's native media modal. + * Respects WordPress conventions and user familiarity. + */ + +declare global { + interface Window { + wp: { + media: (options: any) => { + on: (event: string, callback: (...args: any[]) => void) => void; + open: () => void; + state: () => { + get: (key: string) => { + first: () => { + toJSON: () => { + url: string; + id: number; + title: string; + filename: string; + alt: string; + width: number; + height: number; + }; + }; + }; + }; + }; + }; + } +} + +export interface WPMediaFile { + url: string; + id: number; + title: string; + filename: string; + alt?: string; + width?: number; + height?: number; +} + +export interface WPMediaOptions { + title?: string; + button?: { + text: string; + }; + multiple?: boolean; + library?: { + type?: string | string[]; + }; +} + +/** + * Open WordPress Media Modal + * + * @param options - Configuration for the media modal + * @param onSelect - Callback when media is selected + * @returns Promise that resolves when modal is closed + */ +export function openWPMedia( + options: WPMediaOptions = {}, + onSelect: (file: WPMediaFile) => void +): void { + // Check if WordPress media is available + if (typeof window.wp === 'undefined' || typeof window.wp.media === 'undefined') { + console.error('WordPress media library is not available'); + alert('WordPress media library is not loaded. Please refresh the page.'); + return; + } + + // Default options + const defaultOptions: WPMediaOptions = { + title: 'Select or Upload Media', + button: { + text: 'Use this media', + }, + multiple: false, + }; + + // Merge options + const modalOptions = { ...defaultOptions, ...options }; + + // Create media frame + const frame = window.wp.media(modalOptions); + + // Handle selection + frame.on('select', () => { + const attachment = frame.state().get('selection').first().toJSON(); + + const file: WPMediaFile = { + url: attachment.url, + id: attachment.id, + title: attachment.title || attachment.filename, + filename: attachment.filename, + alt: attachment.alt || '', + width: attachment.width, + height: attachment.height, + }; + + onSelect(file); + }); + + // Open modal + frame.open(); +} + +/** + * Open WordPress Media Modal for Images Only + */ +export function openWPMediaImage(onSelect: (file: WPMediaFile) => void): void { + openWPMedia( + { + title: 'Select or Upload Image', + button: { + text: 'Use this image', + }, + library: { + type: 'image', + }, + }, + onSelect + ); +} + +/** + * Open WordPress Media Modal for Logo/Icon + */ +export function openWPMediaLogo(onSelect: (file: WPMediaFile) => void): void { + openWPMedia( + { + title: 'Select or Upload Logo', + button: { + text: 'Use this logo', + }, + library: { + type: ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'], + }, + }, + onSelect + ); +} + +/** + * Open WordPress Media Modal for Favicon + */ +export function openWPMediaFavicon(onSelect: (file: WPMediaFile) => void): void { + openWPMedia( + { + title: 'Select or Upload Favicon', + button: { + text: 'Use this favicon', + }, + library: { + type: ['image/png', 'image/x-icon', 'image/vnd.microsoft.icon'], + }, + }, + onSelect + ); +} diff --git a/admin-spa/src/routes/Settings/Store.tsx b/admin-spa/src/routes/Settings/Store.tsx index 9afdb3e..20dc4f2 100644 --- a/admin-spa/src/routes/Settings/Store.tsx +++ b/admin-spa/src/routes/Settings/Store.tsx @@ -335,6 +335,7 @@ export default function StoreDetailsPage() { onChange={(url) => updateSetting('storeLogo', url)} onRemove={() => updateSetting('storeLogo', '')} maxSize={2} + mediaType="logo" /> @@ -348,6 +349,7 @@ export default function StoreDetailsPage() { onChange={(url) => updateSetting('storeLogoDark', url)} onRemove={() => updateSetting('storeLogoDark', '')} maxSize={2} + mediaType="logo" /> @@ -357,6 +359,7 @@ export default function StoreDetailsPage() { onChange={(url) => updateSetting('storeIcon', url)} onRemove={() => updateSetting('storeIcon', '')} maxSize={1} + mediaType="favicon" />