Fix logo/favicon upload, badge colors, and page title issues

Issue 1 - Logo/Favicon Upload:
- Add preview before upload (user must confirm)
- Show selected file preview with confirm/cancel buttons
- Fallback to current image if preview cancelled
- File size validation (2MB logo, 1MB favicon)
- Add STORAGE_RLS_FIX.sql for storage policy setup

Issue 2 - Badge Colors:
- Already implemented correctly in all files
- All "Lunas" badges use bg-brand-accent class
- Verified: OrderDetail, MemberOrders, Dashboard, MemberDashboard

Issue 3 - Page Title Error:
- Change .single() to .maybeSingle() in useBranding hook
- Handle error case gracefully with default branding
- Set default title even when platform_settings is empty
- This fixes the "JSON object requested, multiple (or no) rows returned" error

🤖 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 12:31:48 +07:00
parent 3af2787d03
commit 7a8f9cb9a9
3 changed files with 250 additions and 8 deletions

120
STORAGE_RLS_FIX.sql Normal file
View File

@@ -0,0 +1,120 @@
-- =====================================================
-- STORAGE RLS POLICIES FOR LOGO/FAVICON UPLOAD
-- =====================================================
-- This fixes the "new row violates row-level security policy" error
-- when uploading logo/favicon to Supabase Storage
-- =====================================================
-- Step 1: Verify the 'content' bucket exists
SELECT * FROM storage.buckets WHERE name = 'content';
-- If no rows returned, create the bucket:
-- INSERT INTO storage.buckets (id, name, public)
-- VALUES ('content', 'content', true);
-- Step 2: Drop existing policies (if any) on brand-assets
DROP POLICY IF EXISTS "Authenticated users can upload brand assets" ON storage.objects;
DROP POLICY IF EXISTS "Authenticated users can delete brand assets" ON storage.objects;
DROP POLICY IF EXISTS "Public can view brand assets" ON storage.objects;
-- Step 3: Create policies for brand-assets upload
-- Policy 1: Allow authenticated users to INSERT (upload) files to brand-assets folder
CREATE POLICY "Authenticated users can upload brand assets"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%')
);
-- Policy 2: Allow authenticated users to UPDATE (replace) files in brand-assets folder
CREATE POLICY "Authenticated users can update brand assets"
ON storage.objects FOR UPDATE
TO authenticated
USING (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%')
)
WITH CHECK (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%')
);
-- Policy 3: Allow authenticated users to DELETE files in brand-assets folder
CREATE POLICY "Authenticated users can delete brand assets"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%')
);
-- Policy 4: Allow public SELECT (view) on brand-assets (for displaying images)
CREATE POLICY "Public can view brand assets"
ON storage.objects FOR SELECT
TO public
USING (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%')
);
-- Step 5: Allow LIST operation for authenticated users (needed for auto-delete)
CREATE POLICY "Authenticated users can list brand assets"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'content'
AND (name LIKE 'brand-assets/logo%' OR name LIKE 'brand-assets/favicon%')
);
-- =====================================================
-- VERIFICATION QUERIES
-- =====================================================
-- Check all policies on storage.objects
SELECT
schemaname,
tablename,
policyname,
permissive,
roles,
cmd,
qual,
with_check
FROM pg_policies
WHERE tablename = 'objects'
AND schemaname = 'storage';
-- Test if you can access the bucket
SELECT * FROM storage.objects WHERE bucket_id = 'content' LIMIT 5;
-- =====================================================
-- TROUBLESHOOTING
-- =====================================================
-- If still getting RLS errors, check:
-- 1. Are you logged in?
SELECT auth.uid();
-- 2. Check RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'storage'
AND tablename = 'objects';
-- 3. Check bucket is public
SELECT * FROM storage.buckets WHERE name = 'content';
-- =====================================================
-- ALTERNATIVE: Less restrictive policies (NOT RECOMMENDED for production)
-- =====================================================
-- Only use these if you trust all authenticated users completely
-- -- Allow full access to content bucket for authenticated users
-- CREATE POLICY "Authenticated users have full access to content bucket"
-- ON storage.objects FOR ALL
-- TO authenticated
-- USING (bucket_id = 'content')
-- WITH CHECK (bucket_id = 'content');

View File

@@ -54,6 +54,10 @@ export function BrandingTab() {
const [uploadingLogo, setUploadingLogo] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false);
const [uploadingFavicon, setUploadingFavicon] = useState(false); const [uploadingFavicon, setUploadingFavicon] = useState(false);
// Preview states for selected files
const [logoPreview, setLogoPreview] = useState<string | null>(null);
const [faviconPreview, setFaviconPreview] = useState<string | null>(null);
const logoInputRef = useRef<HTMLInputElement>(null); const logoInputRef = useRef<HTMLInputElement>(null);
const faviconInputRef = useRef<HTMLInputElement>(null); const faviconInputRef = useRef<HTMLInputElement>(null);
@@ -241,20 +245,64 @@ export function BrandingTab() {
const handleLogoSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const handleLogoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) handleLogoUpload(file); if (!file) return;
// Validate file size (2MB max)
if (file.size > 2 * 1024 * 1024) {
toast({ title: 'Error', description: 'Ukuran file maksimal 2MB', variant: 'destructive' });
return;
}
// Show preview first
const reader = new FileReader();
reader.onloadend = () => {
setLogoPreview(reader.result as string);
};
reader.readAsDataURL(file);
}; };
const handleFaviconSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFaviconSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) handleFaviconUpload(file); if (!file) return;
// Validate file size (1MB max)
if (file.size > 1 * 1024 * 1024) {
toast({ title: 'Error', description: 'Ukuran file maksimal 1MB', variant: 'destructive' });
return;
}
// Show preview first
const reader = new FileReader();
reader.onloadend = () => {
setFaviconPreview(reader.result as string);
};
reader.readAsDataURL(file);
};
const handleConfirmLogoUpload = async () => {
const file = logoInputRef.current?.files?.[0];
if (file) {
await handleLogoUpload(file);
setLogoPreview(null); // Clear preview after upload
}
};
const handleConfirmFaviconUpload = async () => {
const file = faviconInputRef.current?.files?.[0];
if (file) {
await handleFaviconUpload(file);
setFaviconPreview(null); // Clear preview after upload
}
}; };
const handleRemoveLogo = () => { const handleRemoveLogo = () => {
setSettings({ ...settings, brand_logo_url: '' }); setSettings({ ...settings, brand_logo_url: '' });
setLogoPreview(null);
}; };
const handleRemoveFavicon = () => { const handleRemoveFavicon = () => {
setSettings({ ...settings, brand_favicon_url: '' }); setSettings({ ...settings, brand_favicon_url: '' });
setFaviconPreview(null);
}; };
if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />; if (loading) return <div className="animate-pulse h-64 bg-muted rounded-md" />;
@@ -314,7 +362,39 @@ export function BrandingTab() {
/> />
<div className="space-y-2"> <div className="space-y-2">
{settings.brand_logo_url ? ( {/* Show preview if file selected, otherwise show current logo or upload button */}
{logoPreview ? (
<div className="relative">
<div className="p-4 bg-muted rounded-md flex items-center justify-center">
<img
src={logoPreview}
alt="Logo preview"
className="h-16 object-contain"
/>
</div>
<div className="flex gap-2 mt-2">
<Button
type="button"
variant="default"
size="sm"
onClick={handleConfirmLogoUpload}
disabled={uploadingLogo}
className="flex-1 border-2"
>
{uploadingLogo ? 'Mengupload...' : 'Konfirmasi Upload'}
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setLogoPreview(null)}
disabled={uploadingLogo}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
) : settings.brand_logo_url ? (
<div className="relative"> <div className="relative">
<div className="p-4 bg-muted rounded-md flex items-center justify-center"> <div className="p-4 bg-muted rounded-md flex items-center justify-center">
<img <img
@@ -384,7 +464,39 @@ export function BrandingTab() {
/> />
<div className="space-y-2"> <div className="space-y-2">
{settings.brand_favicon_url ? ( {/* Show preview if file selected, otherwise show current favicon or upload button */}
{faviconPreview ? (
<div className="relative">
<div className="p-4 bg-muted rounded-md flex items-center justify-center">
<img
src={faviconPreview}
alt="Favicon preview"
className="h-12 w-12 object-contain"
/>
</div>
<div className="flex gap-2 mt-2">
<Button
type="button"
variant="default"
size="sm"
onClick={handleConfirmFaviconUpload}
disabled={uploadingFavicon}
className="flex-1 border-2"
>
{uploadingFavicon ? 'Mengupload...' : 'Konfirmasi Upload'}
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setFaviconPreview(null)}
disabled={uploadingFavicon}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
) : settings.brand_favicon_url ? (
<div className="relative"> <div className="relative">
<div className="p-4 bg-muted rounded-md flex items-center justify-center"> <div className="p-4 bg-muted rounded-md flex items-center justify-center">
<img <img

View File

@@ -50,7 +50,14 @@ export function BrandingProvider({ children }: { children: ReactNode }) {
const { data, error } = await supabase const { data, error } = await supabase
.from('platform_settings') .from('platform_settings')
.select('*') .select('*')
.single(); .maybeSingle();
if (error) {
console.error('Error fetching branding settings:', error);
// Keep default branding on error - still set title
document.title = defaultBranding.brand_name;
return;
}
if (data) { if (data) {
let features = defaultBranding.homepage_features; let features = defaultBranding.homepage_features;
@@ -94,6 +101,9 @@ export function BrandingProvider({ children }: { children: ReactNode }) {
if (data.brand_name) { if (data.brand_name) {
document.title = data.brand_name; document.title = data.brand_name;
} }
} else {
// No data found - use defaults and set title
document.title = defaultBranding.brand_name;
} }
}; };