feat: Add customer avatar upload and product downloadable files
Customer Avatar Upload: - Add /account/avatar endpoint for upload/delete - Add /account/avatar-settings endpoint for settings - Update AccountDetails.tsx with avatar upload UI - Support base64 image upload with validation Product Downloadable Files: - Create DownloadsTab component for file management - Add downloads state to ProductFormTabbed - Show Downloads tab when 'downloadable' is checked - Support file name, URL, download limit, and expiry
This commit is contained in:
@@ -4,12 +4,13 @@ import { api } from '@/lib/api';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { VerticalTabForm, FormSection } from '@/components/VerticalTabForm';
|
||||
import { Package, DollarSign, Layers, Tag } from 'lucide-react';
|
||||
import { Package, DollarSign, Layers, Tag, Download } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { GeneralTab } from './tabs/GeneralTab';
|
||||
import { InventoryTab } from './tabs/InventoryTab';
|
||||
import { VariationsTab, ProductVariant } from './tabs/VariationsTab';
|
||||
import { OrganizationTab } from './tabs/OrganizationTab';
|
||||
import { DownloadsTab, DownloadableFile } from './tabs/DownloadsTab';
|
||||
|
||||
// Types
|
||||
export type ProductFormData = {
|
||||
@@ -32,6 +33,9 @@ export type ProductFormData = {
|
||||
virtual?: boolean;
|
||||
downloadable?: boolean;
|
||||
featured?: boolean;
|
||||
downloads?: DownloadableFile[];
|
||||
download_limit?: string;
|
||||
download_expiry?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -75,6 +79,9 @@ export function ProductFormTabbed({
|
||||
const [virtual, setVirtual] = useState(initial?.virtual || false);
|
||||
const [downloadable, setDownloadable] = useState(initial?.downloadable || false);
|
||||
const [featured, setFeatured] = useState(initial?.featured || false);
|
||||
const [downloads, setDownloads] = useState<DownloadableFile[]>(initial?.downloads || []);
|
||||
const [downloadLimit, setDownloadLimit] = useState(initial?.download_limit || '');
|
||||
const [downloadExpiry, setDownloadExpiry] = useState(initial?.download_expiry || '');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
// Update form state when initial data changes (for edit mode)
|
||||
@@ -99,6 +106,9 @@ export function ProductFormTabbed({
|
||||
setVirtual(initial.virtual || false);
|
||||
setDownloadable(initial.downloadable || false);
|
||||
setFeatured(initial.featured || false);
|
||||
setDownloads(initial.downloads || []);
|
||||
setDownloadLimit(initial.download_limit || '');
|
||||
setDownloadExpiry(initial.download_expiry || '');
|
||||
}
|
||||
}, [initial, mode]);
|
||||
|
||||
@@ -155,6 +165,9 @@ export function ProductFormTabbed({
|
||||
virtual,
|
||||
downloadable,
|
||||
featured,
|
||||
downloads: downloadable ? downloads : undefined,
|
||||
download_limit: downloadable ? downloadLimit : undefined,
|
||||
download_expiry: downloadable ? downloadExpiry : undefined,
|
||||
};
|
||||
|
||||
await onSubmit(payload);
|
||||
@@ -169,6 +182,7 @@ export function ProductFormTabbed({
|
||||
const tabs = [
|
||||
{ id: 'general', label: __('General'), icon: <Package className="w-4 h-4" /> },
|
||||
{ id: 'inventory', label: __('Inventory'), icon: <Layers className="w-4 h-4" /> },
|
||||
...(downloadable ? [{ id: 'downloads', label: __('Downloads'), icon: <Download className="w-4 h-4" /> }] : []),
|
||||
...(type === 'variable' ? [{ id: 'variations', label: __('Variations'), icon: <Layers className="w-4 h-4" /> }] : []),
|
||||
{ id: 'organization', label: __('Organization'), icon: <Tag className="w-4 h-4" /> },
|
||||
];
|
||||
@@ -218,6 +232,20 @@ export function ProductFormTabbed({
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
{/* Downloads Tab (only for downloadable products) */}
|
||||
{downloadable && (
|
||||
<FormSection id="downloads">
|
||||
<DownloadsTab
|
||||
downloads={downloads}
|
||||
setDownloads={setDownloads}
|
||||
downloadLimit={downloadLimit}
|
||||
setDownloadLimit={setDownloadLimit}
|
||||
downloadExpiry={downloadExpiry}
|
||||
setDownloadExpiry={setDownloadExpiry}
|
||||
/>
|
||||
</FormSection>
|
||||
)}
|
||||
|
||||
{/* Variations Tab (only for variable products) */}
|
||||
{type === 'variable' && (
|
||||
<FormSection id="variations">
|
||||
|
||||
195
admin-spa/src/routes/Products/partials/tabs/DownloadsTab.tsx
Normal file
195
admin-spa/src/routes/Products/partials/tabs/DownloadsTab.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import React from 'react';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Upload, Trash2, FileIcon, Plus, GripVertical } from 'lucide-react';
|
||||
import { openWPMediaGallery } from '@/lib/wp-media';
|
||||
|
||||
export interface DownloadableFile {
|
||||
id?: string;
|
||||
name: string;
|
||||
file: string; // URL
|
||||
}
|
||||
|
||||
type DownloadsTabProps = {
|
||||
downloads: DownloadableFile[];
|
||||
setDownloads: (files: DownloadableFile[]) => void;
|
||||
downloadLimit: string;
|
||||
setDownloadLimit: (value: string) => void;
|
||||
downloadExpiry: string;
|
||||
setDownloadExpiry: (value: string) => void;
|
||||
};
|
||||
|
||||
export function DownloadsTab({
|
||||
downloads,
|
||||
setDownloads,
|
||||
downloadLimit,
|
||||
setDownloadLimit,
|
||||
downloadExpiry,
|
||||
setDownloadExpiry,
|
||||
}: DownloadsTabProps) {
|
||||
|
||||
const addFile = () => {
|
||||
openWPMediaGallery((files) => {
|
||||
const newDownloads = files.map(file => ({
|
||||
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: file.name || file.title || 'Untitled',
|
||||
file: file.url,
|
||||
}));
|
||||
setDownloads([...downloads, ...newDownloads]);
|
||||
});
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
const newDownloads = downloads.filter((_, i) => i !== index);
|
||||
setDownloads(newDownloads);
|
||||
};
|
||||
|
||||
const updateFileName = (index: number, name: string) => {
|
||||
const newDownloads = [...downloads];
|
||||
newDownloads[index] = { ...newDownloads[index], name };
|
||||
setDownloads(newDownloads);
|
||||
};
|
||||
|
||||
const updateFileUrl = (index: number, file: string) => {
|
||||
const newDownloads = [...downloads];
|
||||
newDownloads[index] = { ...newDownloads[index], file };
|
||||
setDownloads(newDownloads);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{__('Downloadable Files')}</CardTitle>
|
||||
<CardDescription>
|
||||
{__('Add files that customers can download after purchase')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Downloadable Files List */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{__('Files')}</Label>
|
||||
<Button type="button" variant="outline" size="sm" onClick={addFile}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{__('Add File')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{downloads.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{downloads.map((download, index) => (
|
||||
<div
|
||||
key={download.id || index}
|
||||
className="flex items-center gap-3 p-3 border rounded-lg bg-muted/30"
|
||||
>
|
||||
<GripVertical className="w-4 h-4 text-muted-foreground cursor-move" />
|
||||
<FileIcon className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
|
||||
<div className="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">{__('File Name')}</Label>
|
||||
<Input
|
||||
value={download.name}
|
||||
onChange={(e) => updateFileName(index, e.target.value)}
|
||||
placeholder={__('My Downloadable File')}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-muted-foreground">{__('File URL')}</Label>
|
||||
<div className="flex gap-2 mt-1">
|
||||
<Input
|
||||
value={download.file}
|
||||
onChange={(e) => updateFileUrl(index, e.target.value)}
|
||||
placeholder="https://..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
openWPMediaGallery((files) => {
|
||||
if (files.length > 0) {
|
||||
updateFileUrl(index, files[0].url);
|
||||
if (!download.name || download.name === 'Untitled') {
|
||||
updateFileName(index, files[0].name || files[0].title || 'Untitled');
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Upload className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-red-500 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => removeFile(index)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-2 border-dashed rounded-lg p-8 text-center text-muted-foreground">
|
||||
<FileIcon className="mx-auto h-12 w-12 mb-2 opacity-50" />
|
||||
<p className="text-sm">{__('No downloadable files added yet')}</p>
|
||||
<Button type="button" variant="outline" size="sm" className="mt-3" onClick={addFile}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{__('Choose files from Media Library')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Download Settings */}
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="font-medium mb-4">{__('Download Settings')}</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="download_limit">{__('Download Limit')}</Label>
|
||||
<Input
|
||||
id="download_limit"
|
||||
type="number"
|
||||
min="0"
|
||||
value={downloadLimit}
|
||||
onChange={(e) => setDownloadLimit(e.target.value)}
|
||||
placeholder={__('Unlimited')}
|
||||
className="mt-1.5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Leave blank for unlimited downloads.')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="download_expiry">{__('Download Expiry (days)')}</Label>
|
||||
<Input
|
||||
id="download_expiry"
|
||||
type="number"
|
||||
min="0"
|
||||
value={downloadExpiry}
|
||||
onChange={(e) => setDownloadExpiry(e.target.value)}
|
||||
placeholder={__('Never expires')}
|
||||
className="mt-1.5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{__('Leave blank for downloads that never expire.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { api } from '@/lib/api/client';
|
||||
|
||||
interface AvatarSettings {
|
||||
allow_custom_avatar: boolean;
|
||||
current_avatar: string | null;
|
||||
gravatar_url: string;
|
||||
}
|
||||
|
||||
export default function AccountDetails() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -16,8 +22,14 @@ export default function AccountDetails() {
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
// Avatar state
|
||||
const [avatarSettings, setAvatarSettings] = useState<AvatarSettings | null>(null);
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile();
|
||||
loadAvatarSettings();
|
||||
}, []);
|
||||
|
||||
const loadProfile = async () => {
|
||||
@@ -36,6 +48,82 @@ export default function AccountDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadAvatarSettings = async () => {
|
||||
try {
|
||||
const data = await api.get<AvatarSettings>('/account/avatar-settings');
|
||||
setAvatarSettings(data);
|
||||
} catch (error) {
|
||||
console.error('Load avatar settings error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
toast.error('Please upload a valid image (JPG, PNG, GIF, or WebP)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (max 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
toast.error('Image size must be less than 2MB');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploadingAvatar(true);
|
||||
|
||||
try {
|
||||
// Convert to base64
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const result = await api.post<{ avatar_url: string }>('/account/avatar', {
|
||||
avatar: reader.result,
|
||||
});
|
||||
|
||||
setAvatarSettings(prev => prev ? {
|
||||
...prev,
|
||||
current_avatar: result.avatar_url,
|
||||
} : null);
|
||||
|
||||
toast.success('Avatar uploaded successfully');
|
||||
} catch (error: any) {
|
||||
console.error('Upload avatar error:', error);
|
||||
toast.error(error.message || 'Failed to upload avatar');
|
||||
} finally {
|
||||
setUploadingAvatar(false);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} catch (error) {
|
||||
console.error('Read file error:', error);
|
||||
toast.error('Failed to read image file');
|
||||
setUploadingAvatar(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAvatar = async () => {
|
||||
setUploadingAvatar(true);
|
||||
|
||||
try {
|
||||
await api.delete('/account/avatar');
|
||||
setAvatarSettings(prev => prev ? {
|
||||
...prev,
|
||||
current_avatar: null,
|
||||
} : null);
|
||||
toast.success('Avatar removed');
|
||||
} catch (error: any) {
|
||||
console.error('Remove avatar error:', error);
|
||||
toast.error(error.message || 'Failed to remove avatar');
|
||||
} finally {
|
||||
setUploadingAvatar(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
@@ -91,6 +179,8 @@ export default function AccountDetails() {
|
||||
}
|
||||
};
|
||||
|
||||
const currentAvatarUrl = avatarSettings?.current_avatar || avatarSettings?.gravatar_url;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
@@ -103,6 +193,57 @@ export default function AccountDetails() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-6">Account Details</h1>
|
||||
|
||||
{/* Avatar Section */}
|
||||
{avatarSettings?.allow_custom_avatar && (
|
||||
<div className="mb-8 pb-8 border-b">
|
||||
<h2 className="text-xl font-semibold mb-4">Profile Photo</h2>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={currentAvatarUrl || '/placeholder-avatar.png'}
|
||||
alt="Profile"
|
||||
className="w-24 h-24 rounded-full object-cover border-2 border-gray-200"
|
||||
/>
|
||||
{uploadingAvatar && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50 rounded-full">
|
||||
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploadingAvatar}
|
||||
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{uploadingAvatar ? 'Uploading...' : 'Upload Photo'}
|
||||
</button>
|
||||
{avatarSettings?.current_avatar && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveAvatar}
|
||||
disabled={uploadingAvatar}
|
||||
className="px-4 py-2 border border-red-500 text-red-500 rounded-lg hover:bg-red-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
Remove Photo
|
||||
</button>
|
||||
)}
|
||||
<p className="text-xs text-gray-500">
|
||||
JPG, PNG, GIF or WebP. Max 2MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
||||
@@ -87,6 +87,27 @@ class AccountController {
|
||||
'callback' => [__CLASS__, 'get_downloads'],
|
||||
'permission_callback' => [__CLASS__, 'check_customer_permission'],
|
||||
]);
|
||||
|
||||
// Avatar upload
|
||||
register_rest_route($namespace, '/account/avatar', [
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [__CLASS__, 'upload_avatar'],
|
||||
'permission_callback' => [__CLASS__, 'check_customer_permission'],
|
||||
],
|
||||
[
|
||||
'methods' => 'DELETE',
|
||||
'callback' => [__CLASS__, 'delete_avatar'],
|
||||
'permission_callback' => [__CLASS__, 'check_customer_permission'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Get avatar settings (check if custom avatars are enabled)
|
||||
register_rest_route($namespace, '/account/avatar-settings', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [__CLASS__, 'get_avatar_settings'],
|
||||
'permission_callback' => [__CLASS__, 'check_customer_permission'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,6 +230,143 @@ class AccountController {
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload customer avatar
|
||||
*/
|
||||
public static function upload_avatar(WP_REST_Request $request) {
|
||||
// Check if custom avatars are enabled
|
||||
$settings = get_option('woonoow_customer_settings', []);
|
||||
$allow_custom_avatar = $settings['allow_custom_avatar'] ?? false;
|
||||
|
||||
if (!$allow_custom_avatar) {
|
||||
return new WP_Error('avatar_disabled', 'Custom avatars are not enabled', ['status' => 403]);
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check for file data (base64 or URL)
|
||||
$avatar_data = $request->get_param('avatar');
|
||||
$avatar_url = $request->get_param('avatar_url');
|
||||
|
||||
if ($avatar_url) {
|
||||
// Avatar URL provided (from media library)
|
||||
update_user_meta($user_id, 'woonoow_custom_avatar', esc_url_raw($avatar_url));
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => 'Avatar updated successfully',
|
||||
'avatar_url' => $avatar_url,
|
||||
], 200);
|
||||
}
|
||||
|
||||
if (!$avatar_data) {
|
||||
return new WP_Error('no_avatar', 'No avatar data provided', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Handle base64 image upload
|
||||
if (strpos($avatar_data, 'data:image') === 0) {
|
||||
// Extract base64 data
|
||||
$parts = explode(',', $avatar_data);
|
||||
if (count($parts) !== 2) {
|
||||
return new WP_Error('invalid_data', 'Invalid image data format', ['status' => 400]);
|
||||
}
|
||||
|
||||
$image_data = base64_decode($parts[1]);
|
||||
|
||||
// Determine file extension from mime type
|
||||
preg_match('/data:image\/(\w+);/', $parts[0], $matches);
|
||||
$extension = $matches[1] ?? 'png';
|
||||
|
||||
// Validate extension
|
||||
$allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
if (!in_array(strtolower($extension), $allowed)) {
|
||||
return new WP_Error('invalid_type', 'Invalid image type. Allowed: jpg, png, gif, webp', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Create upload directory
|
||||
$upload_dir = wp_upload_dir();
|
||||
$avatar_dir = $upload_dir['basedir'] . '/woonoow-avatars';
|
||||
|
||||
if (!file_exists($avatar_dir)) {
|
||||
wp_mkdir_p($avatar_dir);
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$filename = 'avatar-' . $user_id . '-' . time() . '.' . $extension;
|
||||
$filepath = $avatar_dir . '/' . $filename;
|
||||
|
||||
// Delete old avatar if exists
|
||||
$old_avatar = get_user_meta($user_id, 'woonoow_custom_avatar', true);
|
||||
if ($old_avatar) {
|
||||
$old_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $old_avatar);
|
||||
if (file_exists($old_path)) {
|
||||
unlink($old_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Save new avatar
|
||||
if (file_put_contents($filepath, $image_data) === false) {
|
||||
return new WP_Error('upload_failed', 'Failed to save avatar', ['status' => 500]);
|
||||
}
|
||||
|
||||
// Get URL
|
||||
$avatar_url = $upload_dir['baseurl'] . '/woonoow-avatars/' . $filename;
|
||||
|
||||
// Save to user meta
|
||||
update_user_meta($user_id, 'woonoow_custom_avatar', $avatar_url);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => 'Avatar uploaded successfully',
|
||||
'avatar_url' => $avatar_url,
|
||||
], 200);
|
||||
}
|
||||
|
||||
return new WP_Error('invalid_data', 'Invalid avatar data', ['status' => 400]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete customer avatar
|
||||
*/
|
||||
public static function delete_avatar(WP_REST_Request $request) {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Get current avatar
|
||||
$avatar_url = get_user_meta($user_id, 'woonoow_custom_avatar', true);
|
||||
|
||||
if ($avatar_url) {
|
||||
// Try to delete the file
|
||||
$upload_dir = wp_upload_dir();
|
||||
$filepath = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $avatar_url);
|
||||
|
||||
if (file_exists($filepath)) {
|
||||
unlink($filepath);
|
||||
}
|
||||
|
||||
// Remove from user meta
|
||||
delete_user_meta($user_id, 'woonoow_custom_avatar');
|
||||
}
|
||||
|
||||
return new WP_REST_Response([
|
||||
'success' => true,
|
||||
'message' => 'Avatar removed successfully',
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get avatar settings
|
||||
*/
|
||||
public static function get_avatar_settings(WP_REST_Request $request) {
|
||||
$user_id = get_current_user_id();
|
||||
$settings = get_option('woonoow_customer_settings', []);
|
||||
|
||||
return new WP_REST_Response([
|
||||
'allow_custom_avatar' => $settings['allow_custom_avatar'] ?? false,
|
||||
'current_avatar' => get_user_meta($user_id, 'woonoow_custom_avatar', true) ?: null,
|
||||
'gravatar_url' => get_avatar_url($user_id),
|
||||
], 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update password
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user