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:
dwindown
2025-12-24 11:42:20 +07:00
parent 4b8765885b
commit fb24e77e42
15 changed files with 779 additions and 149 deletions

View File

@@ -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>