diff --git a/components/AnalyticsPage.tsx b/components/AnalyticsPage.tsx index ab9adaf..9277c1b 100644 --- a/components/AnalyticsPage.tsx +++ b/components/AnalyticsPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { motion } from 'framer-motion'; import { ArrowLeft, @@ -33,6 +33,7 @@ type AnalyticsEvent = { user_agent: string | null; language: string | null; screen_w: number | null; + viewport_w?: number | null; screen_h: number | null; visitor_id: string | null; session_id: string | null; @@ -84,50 +85,53 @@ const AnalyticsPage: React.FC = () => { .finally(() => setInitialLoading(false)); }, []); - const fetchAnalytics = async (customDays?: number) => { - const daysToFetch = customDays ?? days; - if (!projectUrl || !dbPassword) { - setError('Please enter your Supabase URL and database password'); - return; - } + const fetchAnalytics = useCallback( + async (customDays?: number) => { + const daysToFetch = customDays ?? days; + if (!projectUrl || !dbPassword) { + setError('Please enter your Supabase URL and database password'); + return; + } - setLoading(true); - setError(null); - - try { - const res = await fetch('/__openbento/analytics/fetch', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ projectUrl, dbPassword, days: daysToFetch }), - }); - - const data = await res.json(); - if (data.ok) { - setEvents(data.events || []); - setIsConfigured(true); - } else { - setError(data.error || 'Failed to fetch analytics'); + setLoading(true); + setError(null); + + try { + const res = await fetch('/__openbento/analytics/fetch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectUrl, dbPassword, days: daysToFetch }), + }); + + const data = await res.json(); + if (data.ok) { + setEvents(data.events || []); + setIsConfigured(true); + } else { + setError(data.error || 'Failed to fetch analytics'); + } + } catch (e) { + setError('Network error: ' + (e as Error).message); + } finally { + setLoading(false); } - } catch (e) { - setError('Network error: ' + (e as Error).message); - } finally { - setLoading(false); - } - }; + }, + [days, projectUrl, dbPassword] + ); // Auto-fetch when config is ready useEffect(() => { if (!initialLoading && projectUrl && dbPassword && !isConfigured) { fetchAnalytics(); } - }, [initialLoading, projectUrl, dbPassword]); + }, [initialLoading, projectUrl, dbPassword, isConfigured, fetchAnalytics]); // Auto-refresh when days change (if already configured) useEffect(() => { if (isConfigured && projectUrl && dbPassword) { fetchAnalytics(days); } - }, [days]); + }, [days, isConfigured, projectUrl, dbPassword, fetchAnalytics]); // Compute analytics stats const stats = useMemo(() => { diff --git a/components/Builder.tsx b/components/Builder.tsx index fb4e2b8..a75e747 100644 --- a/components/Builder.tsx +++ b/components/Builder.tsx @@ -2229,7 +2229,7 @@ const Builder: React.FC = ({ onBack }) => { } }} blocks={blocks} - setBlocks={handleSetBlocks} + onBlocksChange={handleSetBlocks} /> {/* 4. AVATAR CROP MODAL */} diff --git a/components/EditorSidebar.tsx b/components/EditorSidebar.tsx index 948e525..24e8d00 100644 --- a/components/EditorSidebar.tsx +++ b/components/EditorSidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { BlockData, BlockType, SocialPlatform, UserProfile } from '../types'; import { BASE_COLORS } from '../constants'; import { @@ -52,6 +52,7 @@ const EditorSidebar: React.FC = ({ }) => { const [isFetching, setIsFetching] = useState(false); const [fetchError, setFetchError] = useState(null); + const blockImageInputRef = useRef(null); const handleBlockImageUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; @@ -59,11 +60,22 @@ const EditorSidebar: React.FC = ({ const reader = new FileReader(); reader.onloadend = () => { updateBlock({ ...editingBlock, imageUrl: reader.result as string }); + e.target.value = ''; }; reader.readAsDataURL(file); } }; + const handleRemoveBlockImage = () => { + if (!editingBlock) return; + + updateBlock({ ...editingBlock, imageUrl: undefined }); + + if (blockImageInputRef.current) { + blockImageInputRef.current.value = ''; + } + }; + const isYouTubeActive = editingBlock?.type === BlockType.SOCIAL && !!(editingBlock.channelId && editingBlock.channelId.length > 0); @@ -751,6 +763,7 @@ const EditorSidebar: React.FC = ({ = ({ )} + {editingBlock.imageUrl && ( +
+
+ {editingBlock.type === BlockType.MEDIA && + /\.(mp4|webm|ogg|mov)$/i.test(editingBlock.imageUrl) ? ( +
+ +
+ )} )} diff --git a/socialPlatforms.ts b/socialPlatforms.ts index e5a2812..07161ab 100644 --- a/socialPlatforms.ts +++ b/socialPlatforms.ts @@ -280,7 +280,7 @@ export const SOCIAL_PLATFORM_OPTIONS: SocialPlatformOption[] = [ { id: 'spotify', label: 'Spotify', - icon: SiSpotify, + icon: Globe, brandIcon: SiSpotify, brandColor: '#1DB954', placeholder: 'spotify username',