Implement post-implementation refinements
Features implemented: 1. Expired QRIS order handling with dual-path approach - Product orders: QR regeneration button - Consulting orders: Immediate cancellation with slot release 2. Standardized status badge wording to "Pending" 3. Fixed TypeScript error in MemberDashboard 4. Dynamic badge colors from branding settings 5. Dynamic page title from branding settings 6. Logo/favicon file upload with auto-delete 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { Palette, Image, Mail, Home, Plus, Trash2 } from 'lucide-react';
|
||||
import { Palette, Image, Mail, Home, Plus, Trash2, Upload, X } from 'lucide-react';
|
||||
|
||||
interface HomepageFeature {
|
||||
icon: string;
|
||||
@@ -51,6 +51,11 @@ export function BrandingTab() {
|
||||
const [settings, setSettings] = useState<PlatformSettings>(emptySettings);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [uploadingLogo, setUploadingLogo] = useState(false);
|
||||
const [uploadingFavicon, setUploadingFavicon] = useState(false);
|
||||
|
||||
const logoInputRef = useRef<HTMLInputElement>(null);
|
||||
const faviconInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings();
|
||||
@@ -150,6 +155,108 @@ export function BrandingTab() {
|
||||
setSettings({ ...settings, homepage_features: newFeatures });
|
||||
};
|
||||
|
||||
// Handle logo upload with auto-delete
|
||||
const handleLogoUpload = async (file: File) => {
|
||||
setUploadingLogo(true);
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const filePath = `brand-assets/logo/logo-current.${fileExt}`;
|
||||
|
||||
// Step 1: Delete old logo if exists
|
||||
const { data: existingFiles } = await supabase.storage
|
||||
.from('content')
|
||||
.list('brand-assets/logo/');
|
||||
|
||||
if (existingFiles?.length > 0) {
|
||||
const oldFile = existingFiles.find(f => f.name.startsWith('logo-current'));
|
||||
if (oldFile) {
|
||||
await supabase.storage.from('content').remove([`brand-assets/logo/${oldFile.name}`]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Upload new logo
|
||||
const { data, error } = await supabase.storage
|
||||
.from('content')
|
||||
.upload(filePath, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Step 3: Get public URL and update settings
|
||||
const { data: urlData } = supabase.storage.from('content').getPublicUrl(filePath);
|
||||
setSettings({ ...settings, brand_logo_url: urlData.publicUrl });
|
||||
|
||||
toast({ title: 'Berhasil', description: 'Logo berhasil diupload' });
|
||||
} catch (error) {
|
||||
console.error('Logo upload error:', error);
|
||||
toast({ title: 'Error', description: 'Gagal upload logo', variant: 'destructive' });
|
||||
} finally {
|
||||
setUploadingLogo(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle favicon upload with auto-delete
|
||||
const handleFaviconUpload = async (file: File) => {
|
||||
setUploadingFavicon(true);
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const filePath = `brand-assets/favicon/favicon-current.${fileExt}`;
|
||||
|
||||
// Step 1: Delete old favicon if exists
|
||||
const { data: existingFiles } = await supabase.storage
|
||||
.from('content')
|
||||
.list('brand-assets/favicon/');
|
||||
|
||||
if (existingFiles?.length > 0) {
|
||||
const oldFile = existingFiles.find(f => f.name.startsWith('favicon-current'));
|
||||
if (oldFile) {
|
||||
await supabase.storage.from('content').remove([`brand-assets/favicon/${oldFile.name}`]);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Upload new favicon
|
||||
const { data, error } = await supabase.storage
|
||||
.from('content')
|
||||
.upload(filePath, file, {
|
||||
cacheControl: '3600',
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Step 3: Get public URL and update settings
|
||||
const { data: urlData } = supabase.storage.from('content').getPublicUrl(filePath);
|
||||
setSettings({ ...settings, brand_favicon_url: urlData.publicUrl });
|
||||
|
||||
toast({ title: 'Berhasil', description: 'Favicon berhasil diupload' });
|
||||
} catch (error) {
|
||||
console.error('Favicon upload error:', error);
|
||||
toast({ title: 'Error', description: 'Gagal upload favicon', variant: 'destructive' });
|
||||
} finally {
|
||||
setUploadingFavicon(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleLogoUpload(file);
|
||||
};
|
||||
|
||||
const handleFaviconSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) handleFaviconUpload(file);
|
||||
};
|
||||
|
||||
const handleRemoveLogo = () => {
|
||||
setSettings({ ...settings, brand_logo_url: '' });
|
||||
};
|
||||
|
||||
const handleRemoveFavicon = () => {
|
||||
setSettings({ ...settings, brand_favicon_url: '' });
|
||||
};
|
||||
|
||||
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
|
||||
|
||||
return (
|
||||
@@ -195,47 +302,141 @@ export function BrandingTab() {
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Image className="w-4 h-4" />
|
||||
Logo Utama (URL)
|
||||
Logo Utama
|
||||
</Label>
|
||||
<Input
|
||||
value={settings.brand_logo_url}
|
||||
onChange={(e) => setSettings({ ...settings, brand_logo_url: e.target.value })}
|
||||
placeholder="https://example.com/logo.png"
|
||||
className="border-2"
|
||||
|
||||
<input
|
||||
ref={logoInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/svg+xml,image/jpeg,image/webp"
|
||||
onChange={handleLogoSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
{settings.brand_logo_url && (
|
||||
<div className="mt-2 p-2 bg-muted rounded-md">
|
||||
<img
|
||||
src={settings.brand_logo_url}
|
||||
alt="Logo preview"
|
||||
className="h-12 object-contain"
|
||||
onError={(e) => (e.currentTarget.style.display = 'none')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{settings.brand_logo_url ? (
|
||||
<div className="relative">
|
||||
<div className="p-4 bg-muted rounded-md flex items-center justify-center">
|
||||
<img
|
||||
src={settings.brand_logo_url}
|
||||
alt="Logo preview"
|
||||
className="h-16 object-contain"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
toast({ title: 'Error', description: 'Gagal memuat logo', variant: 'destructive' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => logoInputRef.current?.click()}
|
||||
disabled={uploadingLogo}
|
||||
className="flex-1 border-2"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{uploadingLogo ? 'Mengupload...' : 'Ganti'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveLogo}
|
||||
disabled={uploadingLogo}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => logoInputRef.current?.click()}
|
||||
disabled={uploadingLogo}
|
||||
className="w-full border-2"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{uploadingLogo ? 'Mengupload...' : 'Upload Logo'}
|
||||
</Button>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
PNG, SVG, JPG, atau WebP. Maks 2MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Image className="w-4 h-4" />
|
||||
Favicon (URL)
|
||||
Favicon
|
||||
</Label>
|
||||
<Input
|
||||
value={settings.brand_favicon_url}
|
||||
onChange={(e) => setSettings({ ...settings, brand_favicon_url: e.target.value })}
|
||||
placeholder="https://example.com/favicon.ico"
|
||||
className="border-2"
|
||||
|
||||
<input
|
||||
ref={faviconInputRef}
|
||||
type="file"
|
||||
accept="image/png,image/svg+xml,image/jpeg,image/x-icon"
|
||||
onChange={handleFaviconSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
{settings.brand_favicon_url && (
|
||||
<div className="mt-2 p-2 bg-muted rounded-md">
|
||||
<img
|
||||
src={settings.brand_favicon_url}
|
||||
alt="Favicon preview"
|
||||
className="h-8 w-8 object-contain"
|
||||
onError={(e) => (e.currentTarget.style.display = 'none')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{settings.brand_favicon_url ? (
|
||||
<div className="relative">
|
||||
<div className="p-4 bg-muted rounded-md flex items-center justify-center">
|
||||
<img
|
||||
src={settings.brand_favicon_url}
|
||||
alt="Favicon preview"
|
||||
className="h-12 w-12 object-contain"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
toast({ title: 'Error', description: 'Gagal memuat favicon', variant: 'destructive' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => faviconInputRef.current?.click()}
|
||||
disabled={uploadingFavicon}
|
||||
className="flex-1 border-2"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{uploadingFavicon ? 'Mengupload...' : 'Ganti'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRemoveFavicon}
|
||||
disabled={uploadingFavicon}
|
||||
className="text-destructive hover:text-destructive"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => faviconInputRef.current?.click()}
|
||||
disabled={uploadingFavicon}
|
||||
className="w-full border-2"
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{uploadingFavicon ? 'Mengupload...' : 'Upload Favicon'}
|
||||
</Button>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
PNG, SVG, JPG, atau ICO. Maks 1MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user