mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
feat: landing analytics goals, daily bar chart, referrer tracking, contact persistence
- Add per-landing analytics goals (view/click) with admin editor toggle - Add sticky pay button option for mobile landing pages - Add daily purchases bar chart (created vs paid) to landing stats - Replace single purchase count with created/paid split in stats summary - Add referrer tracking to purchases with hostname display in stats - Add time display to purchase cards alongside date - Pass user timezone to stats API for correct daily grouping - Clamp referrer (500 chars) and subid (255 chars) to backend limits - Persist contact value per-landing-slug in localStorage - Fire buy_success analytics goal on successful delivery - Export USER_TIMEZONE from format utils - Add analytics/stats translations for fa.json and zh.json locales Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,7 @@ export default function AdminLandingEditor() {
|
||||
methods: false,
|
||||
gifts: false,
|
||||
background: false,
|
||||
analytics: false,
|
||||
footer: false,
|
||||
});
|
||||
|
||||
@@ -137,6 +138,13 @@ export default function AdminLandingEditor() {
|
||||
enabled: false,
|
||||
});
|
||||
|
||||
// Analytics goals state
|
||||
const [analyticsViewEnabled, setAnalyticsViewEnabled] = useState(false);
|
||||
const [analyticsViewGoal, setAnalyticsViewGoal] = useState('landing_view');
|
||||
const [analyticsClickEnabled, setAnalyticsClickEnabled] = useState(false);
|
||||
const [analyticsClickGoal, setAnalyticsClickGoal] = useState('landing_pay');
|
||||
const [stickyPayButton, setStickyPayButton] = useState(false);
|
||||
|
||||
// Discount state
|
||||
const [discountPercent, setDiscountPercent] = useState<number | null>(null);
|
||||
const [discountOverrides, setDiscountOverrides] = useState<Record<string, number>>({});
|
||||
@@ -267,6 +275,11 @@ export default function AdminLandingEditor() {
|
||||
landingData.discount_ends_at ? isoToDatetimeLocal(landingData.discount_ends_at) : '',
|
||||
);
|
||||
setDiscountBadgeText(toLocaleDict(landingData.discount_badge_text));
|
||||
setAnalyticsViewEnabled(landingData.analytics_view_enabled ?? false);
|
||||
setAnalyticsViewGoal(landingData.analytics_view_goal || 'landing_view');
|
||||
setAnalyticsClickEnabled(landingData.analytics_click_enabled ?? false);
|
||||
setAnalyticsClickGoal(landingData.analytics_click_goal || 'landing_pay');
|
||||
setStickyPayButton(landingData.sticky_pay_button ?? false);
|
||||
}, [landingData]);
|
||||
|
||||
// Create mutation
|
||||
@@ -378,6 +391,11 @@ export default function AdminLandingEditor() {
|
||||
discount_badge_text:
|
||||
discountPercent !== null ? (nonEmptyDict(discountBadgeText) ?? null) : null,
|
||||
background_config: backgroundConfig.enabled ? backgroundConfig : null,
|
||||
analytics_view_enabled: analyticsViewEnabled,
|
||||
analytics_view_goal: analyticsViewGoal,
|
||||
analytics_click_enabled: analyticsClickEnabled,
|
||||
analytics_click_goal: analyticsClickGoal,
|
||||
sticky_pay_button: stickyPayButton,
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
@@ -1079,6 +1097,72 @@ export default function AdminLandingEditor() {
|
||||
<BackgroundConfigEditor value={backgroundConfig} onChange={setBackgroundConfig} />
|
||||
</Section>
|
||||
|
||||
{/* Analytics Goals Section */}
|
||||
<Section
|
||||
title={t('admin.landings.analytics', 'Analytics')}
|
||||
open={openSections.analytics}
|
||||
onToggle={() => toggleSection('analytics')}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* View Goal */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-sm text-dark-400">
|
||||
{t('admin.landings.viewGoal', 'View goal')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={analyticsViewGoal}
|
||||
onChange={(e) => setAnalyticsViewGoal(e.target.value)}
|
||||
disabled={!analyticsViewEnabled}
|
||||
className="w-full rounded-lg border border-dark-700 bg-dark-800 px-3 py-2 text-sm text-dark-100 outline-none focus:border-accent-500 disabled:opacity-50"
|
||||
placeholder="landing_view"
|
||||
/>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={analyticsViewEnabled}
|
||||
onChange={() => setAnalyticsViewEnabled((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Click Goal */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-sm text-dark-400">
|
||||
{t('admin.landings.clickGoal', 'Payment click goal')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={analyticsClickGoal}
|
||||
onChange={(e) => setAnalyticsClickGoal(e.target.value)}
|
||||
disabled={!analyticsClickEnabled}
|
||||
className="w-full rounded-lg border border-dark-700 bg-dark-800 px-3 py-2 text-sm text-dark-100 outline-none focus:border-accent-500 disabled:opacity-50"
|
||||
placeholder="landing_pay"
|
||||
/>
|
||||
</div>
|
||||
<Toggle
|
||||
checked={analyticsClickEnabled}
|
||||
onChange={() => setAnalyticsClickEnabled((v) => !v)}
|
||||
/>
|
||||
</div>
|
||||
{/* Sticky pay button on mobile */}
|
||||
<div className="flex items-center justify-between gap-4 border-t border-dark-800 pt-4">
|
||||
<div>
|
||||
<p className="text-sm text-dark-300">
|
||||
{t('admin.landings.stickyPayButton', 'Sticky pay button (mobile)')}
|
||||
</p>
|
||||
<p className="text-xs text-dark-500">
|
||||
{t(
|
||||
'admin.landings.stickyPayButtonHint',
|
||||
'Button pinned to bottom of screen on mobile',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Toggle checked={stickyPayButton} onChange={() => setStickyPayButton((v) => !v)} />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Footer & Custom CSS Section */}
|
||||
<Section
|
||||
title={t('admin.landings.content')}
|
||||
|
||||
Reference in New Issue
Block a user