+
+ {entries.map(([month, count]) => {
+ const height = count > 0 ? Math.max((count / maxVal) * 100, 6) : 3;
+ return (
+
+
0
+ ? 'bg-brand-orange/60 hover:bg-brand-orange'
+ : 'bg-foreground-light/20 dark:bg-foreground-dark/20'
+ }`}
+ style={{ height: `${height}%` }}
+ title={`${month}: ${count}`}
+ />
+
+ );
+ })}
+
+
+ {entries[0]?.[0]}
+ {entries[entries.length - 1]?.[0]}
+
+
+ );
+};
+
+const TIME_RANGE_OPTIONS: { label: string; value: TimeRange }[] = [
+ { label: 'Default', value: '' },
+ { label: '24h', value: '24h' },
+ { label: '48h', value: '48h' },
+ { label: '7d', value: '7d' },
+ { label: '30d', value: '30d' },
+ { label: '90d', value: '90d' },
+ { label: '1y', value: '1y' },
+];
+
+// Section wrapper
+const StatSection = ({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) => (
+
+
+ {title}
+
+ {children}
+
+);
+
+// Top-level stat card
+const StatCard = ({
+ label,
+ value,
+}: {
+ label: string;
+ value: string | number;
+}) => (
+
+);
+
+const AnalyticsContent = ({
+ analytics,
+ readCount,
+}: {
+ analytics: BlogAnalytics;
+ readCount: number;
+}) => {
+ const countries = useMemo(
+ () => filterLocal(normalizeCountries(analytics.countries)),
+ [analytics.countries]
+ );
+
+ const sortedCountries = useMemo(
+ () => Object.entries(countries).sort(([, a], [, b]) => b - a),
+ [countries]
+ );
+
+ const sortedCities = useMemo(
+ () =>
+ Object.entries(filterLocal(analytics.cities))
+ .filter(([key]) => key.trim())
+ .sort(([, a], [, b]) => b - a),
+ [analytics.cities]
+ );
+
+ const sortedReferrers = useMemo(
+ () =>
+ Object.entries(filterLocal(analytics.referrers)).sort(
+ ([, a], [, b]) => b - a
+ ),
+ [analytics.referrers]
+ );
+
+ const sortedPlatforms = useMemo(
+ () =>
+ Object.entries(analytics.platforms)
+ .map(([k, v]) => [k.replace('PLATFORM_', ''), v] as [string, number])
+ .sort(([, a], [, b]) => b - a),
+ [analytics.platforms]
+ );
+
+ const countryMax = sortedCountries[0]?.[1] ?? 1;
+ const cityMax = sortedCities[0]?.[1] ?? 1;
+ const referrerMax = sortedReferrers[0]?.[1] ?? 1;
+ const platformMax = sortedPlatforms[0]?.[1] ?? 1;
+
+ return (
+
+ {/* Top-level stats grid */}
+
+
+
+
+
+
+
+ {/* Daily Activity */}
+ {analytics.daily_activity &&
+ Object.keys(analytics.daily_activity).length > 0 && (
+
+
+
+ )}
+
+ {/* Two-column layout for geo + referrers */}
+
+ {/* Countries */}
+ {sortedCountries.length > 0 && (
+
+
+ {sortedCountries.map(([country, count]) => (
+
+ ))}
+
+
+ )}
+
+ {/* Cities */}
+ {sortedCities.length > 0 && (
+
+
+ {sortedCities.map(([city, count]) => (
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Platforms */}
+ {sortedPlatforms.length > 0 && (
+
+
+ {sortedPlatforms.map(([platform, count]) => (
+
+ ))}
+
+
+ )}
+
+ {/* Read Time Distribution */}
+ {analytics.read_time_distribution && (
+
+
+ {Object.entries(analytics.read_time_distribution)
+ .sort(([, a], [, b]) => b - a)
+ .map(([bucket, count]) => (
+
+ ))}
+
+
+ )}
+
+
+ {/* Monthly Activity (all-time) */}
+ {analytics.monthly_activity &&
+ Object.keys(analytics.monthly_activity).length > 0 && (
+
+
+
+ )}
+
+ {/* Referrers */}
+ {sortedReferrers.length > 0 && (
+
+
+ {sortedReferrers.map(([referrer, count]) => (
+
+ ))}
+
+
+ )}
+
+ {/* Bottom stats */}
+
+
+
+
+
+
+ );
+};
+
+export const BlogAnalyticsDashboard = ({ blogId }: { blogId?: string }) => {
+ const [open, setOpen] = useState(false);
+ const [timeRange, setTimeRange] = useState
('');
+ const contentRef = useRef(null);
+ const { stats, statsLoading, statsError } = useGetBlogStats(
+ blogId,
+ timeRange
+ );
+
+ const toggle = useCallback(() => setOpen((prev) => !prev), []);
+
+ if (statsLoading || statsError || !stats?.analytics) return null;
+
+ const { read_count, analytics } = stats;
+
+ return (
+
+ {/* Trigger bar */}
+
+
+ {/* Collapsible panel */}
+
+
+
+ {/* Time range selector */}
+
+
Analytics
+
+ {TIME_RANGE_OPTIONS.map((opt) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/the_monkeys/src/app/blog/components/BlogStats.tsx b/apps/the_monkeys/src/app/blog/components/BlogStats.tsx
new file mode 100644
index 00000000..237640fc
--- /dev/null
+++ b/apps/the_monkeys/src/app/blog/components/BlogStats.tsx
@@ -0,0 +1,17 @@
+import Icon from '@/components/icon';
+import useGetBlogStats from '@/hooks/blog/useGetBlogStats';
+
+export const BlogStats = ({ blogId }: { blogId?: string }) => {
+ const { stats, statsLoading, statsError } = useGetBlogStats(blogId);
+
+ if (statsLoading || statsError) return null;
+
+ return (
+
+
+
+ {stats?.read_count ?? 0} {stats?.read_count === 1 ? 'read' : 'reads'}
+
+
+ );
+};
diff --git a/apps/the_monkeys/src/hooks/blog/useGetBlogStats.ts b/apps/the_monkeys/src/hooks/blog/useGetBlogStats.ts
new file mode 100644
index 00000000..efa32b61
--- /dev/null
+++ b/apps/the_monkeys/src/hooks/blog/useGetBlogStats.ts
@@ -0,0 +1,51 @@
+import { authFetcherV2 } from '@/services/fetcher';
+import { useQuery } from '@tanstack/react-query';
+
+export const BLOG_STATS_QUERY_KEY = 'blog-stats';
+
+export type TimeRange = '24h' | '48h' | '7d' | '30d' | '90d' | '1y' | '';
+
+export interface BlogAnalytics {
+ avg_read_time_ms: number;
+ bounces: number;
+ cities: Record;
+ countries: Record;
+ daily_activity: Record;
+ engagement_rate: number;
+ hourly_activity: Record;
+ monthly_activity: Record;
+ platforms: Record;
+ read_time_distribution: Record;
+ realtime_views: Record;
+ referrers: Record;
+ total_likes: number;
+ unique_readers: number;
+ valid_views: number;
+}
+
+export interface BlogStatsResponse {
+ blog_id: string;
+ read_count: number;
+ analytics: BlogAnalytics | null;
+}
+
+const useGetBlogStats = (
+ blogId: string | undefined,
+ timeRange: TimeRange = ''
+) => {
+ const params = timeRange ? `?time_range=${timeRange}` : '';
+ const { data, isLoading, isError } = useQuery({
+ queryKey: [BLOG_STATS_QUERY_KEY, blogId, timeRange],
+ queryFn: () => authFetcherV2(`/blog/${blogId}/stats${params}`),
+ enabled: !!blogId,
+ staleTime: 60 * 1000,
+ });
+
+ return {
+ stats: data,
+ statsLoading: isLoading,
+ statsError: isError,
+ };
+};
+
+export default useGetBlogStats;