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"
/>