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:
120
STORAGE_RLS_FIX.sql
Normal file
120
STORAGE_RLS_FIX.sql
Normal 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');
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -50,16 +50,23 @@ 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;
|
||||||
|
|
||||||
// Parse homepage_features if it's a string
|
// Parse homepage_features if it's a string
|
||||||
if (data.homepage_features) {
|
if (data.homepage_features) {
|
||||||
try {
|
try {
|
||||||
features = typeof data.homepage_features === 'string'
|
features = typeof data.homepage_features === 'string'
|
||||||
? JSON.parse(data.homepage_features)
|
? JSON.parse(data.homepage_features)
|
||||||
: data.homepage_features;
|
: data.homepage_features;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error parsing homepage_features:', e);
|
console.error('Error parsing homepage_features:', e);
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user