This commit is contained in:
dimitar committed 2026-03-01 01:36:11 +01:00
1 parent e218ad57e8
commit 01a3c1a776
18 files changed
+1562 -53

No files matched your search

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { usePushStats, useSendPushNotification } from '@/queries/push';
import { Button } from '@/components/ui/button';
import { Bell, Send, Users, AlertCircle, CheckCircle } from 'lucide-react';
import { usePostHog } from '@posthog/react';
export function PushNotificationManager() {
const [title, setTitle] = useState('');
@@ -11,6 +12,7 @@ export function PushNotificationManager() {
const [result, setResult] = useState<{ sent: number; failed: number } | null>(
null,
);
const posthog = usePostHog();
const { data: stats, isLoading: loadingStats } = usePushStats();
const sendMutation = useSendPushNotification();
@@ -27,6 +29,13 @@ export function PushNotificationManager() {
setResult(result);
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 5000);
posthog.capture('push_notification_sent', {
notification_title: title.trim(),
has_url: !!url.trim(),
sent_count: result.sent,
failed_count: result.failed,
total_subscribers: stats?.totalSubscribers,
});
} catch (error) {
console.error('Failed to send notification:', error);
}
@@ -5,6 +5,7 @@ import { Button } from '../../ui/button';
import { Textarea } from '../../ui/textarea';
import { Card, CardContent } from '../../ui/card';
import { CommentItem } from './CommentItem';
import { usePostHog } from '@posthog/react';
interface CommentSectionProps {
articleId?: string;
@@ -15,6 +16,7 @@ export function CommentSection({ articleId, liveBlogId }: CommentSectionProps) {
const { isAuthenticated } = useAuth();
const [newComment, setNewComment] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const posthog = usePostHog();
const { data: commentsData, isLoading } = useComments({
articleId,
@@ -28,9 +30,9 @@ export function CommentSection({ articleId, liveBlogId }: CommentSectionProps) {
const handleSubmitComment = async (e: React.FormEvent) => {
e.preventDefault();
if (!newComment.trim() || !isAuthenticated) return;
setIsSubmitting(true);
try {
await createCommentMutation.mutateAsync({
@@ -38,6 +40,12 @@ export function CommentSection({ articleId, liveBlogId }: CommentSectionProps) {
articleId,
liveBlogId,
});
posthog.capture('comment_submitted', {
article_id: articleId,
live_blog_id: liveBlogId,
target_type: liveBlogId ? 'live_blog' : 'article',
comment_length: newComment.trim().length,
});
setNewComment('');
} catch (error) {
console.error('Failed to post comment:', error);
@@ -3,6 +3,7 @@ import { useAuth } from '../../../contexts/AuthContext';
import { useReactionCounts, useUserReaction, useAddReaction } from '../../../queries/comments';
import { Button } from '../../ui/button';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
import { usePostHog } from '@posthog/react';
interface ReactionButtonsProps {
articleId?: string;
@@ -11,26 +12,27 @@ interface ReactionButtonsProps {
compact?: boolean;
}
export function ReactionButtons({
articleId,
liveBlogId,
export function ReactionButtons({
articleId,
liveBlogId,
commentId,
compact = false
compact = false
}: ReactionButtonsProps) {
const { isAuthenticated } = useAuth();
const posthog = usePostHog();
const { data: counts } = useReactionCounts(articleId, liveBlogId, commentId);
const { data: userReaction } = useUserReaction(articleId, liveBlogId, commentId);
const addReactionMutation = useAddReaction();
const userReactionType = userReaction?.type;
const handleReaction = async (type: 'like' | 'dislike') => {
if (!isAuthenticated) {
window.location.href = '/auth';
return;
}
try {
await addReactionMutation.mutateAsync({
type,
@@ -38,6 +40,13 @@ export function ReactionButtons({
liveBlogId,
commentId,
});
posthog.capture('article_reaction_added', {
reaction_type: type,
article_id: articleId,
live_blog_id: liveBlogId,
comment_id: commentId,
target_type: commentId ? 'comment' : liveBlogId ? 'live_blog' : 'article',
});
} catch (error) {
console.error('Failed to add reaction:', error);
}
@@ -5,6 +5,7 @@ import type { LiveBlogUpdate as ApiLiveBlogUpdate } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { usePostHog } from '@posthog/react';
interface LiveBlogViewerProps {
slug: string;
@@ -15,23 +16,25 @@ export function LiveBlogViewer({ slug, className }: LiveBlogViewerProps) {
const [autoScroll, setAutoScroll] = useState(true);
const [newUpdatesCount, setNewUpdatesCount] = useState(0);
const [isScrolledUp, setIsScrolledUp] = useState(false);
const posthog = usePostHog();
const hasTrackedView = useRef(false);
const updatesContainerRef = useRef<HTMLDivElement>(null);
const lastUpdateCountRef = useRef(0);
const { data: liveBlog, isLoading: blogLoading, error: blogError } = useLiveBlog(slug);
const {
data: updatesData,
isLoading: updatesLoading,
refetch: refetchUpdates
const {
data: updatesData,
isLoading: updatesLoading,
refetch: refetchUpdates
} = useLiveBlogUpdates(liveBlog?.id || '', 1, 100);
const {
isConnected,
lastEvent,
const {
isConnected,
lastEvent,
connectionError,
reconnectAttempts,
connect
connect
} = useLiveBlogStream(liveBlog?.id || '');
const scrollToBottom = useCallback(() => {
@@ -95,16 +98,29 @@ export function LiveBlogViewer({ slug, className }: LiveBlogViewerProps) {
useEffect(() => {
if (updatesData?.data && updatesData.data.length > 0) {
const currentUpdateCount = updatesData.data.length;
// Only auto-scroll if this is the initial load or user is at bottom
if (currentUpdateCount > lastUpdateCountRef.current && !isScrolledUp) {
setTimeout(scrollToBottom, 100);
}
lastUpdateCountRef.current = currentUpdateCount;
}
}, [updatesData, scrollToBottom, isScrolledUp]);
// Track live blog view once data is loaded
useEffect(() => {
if (liveBlog && !hasTrackedView.current) {
hasTrackedView.current = true;
posthog.capture('live_blog_viewed', {
live_blog_id: liveBlog.id,
live_blog_slug: slug,
live_blog_title: liveBlog.title,
live_blog_status: liveBlog.status,
});
}
}, [liveBlog, slug, posthog]);
if (blogLoading) {
return (
<Card className={cn('w-full max-w-4xl mx-auto', className)}>
@@ -185,7 +201,14 @@ export function LiveBlogViewer({ slug, className }: LiveBlogViewerProps) {
<Button
variant="outline"
size="sm"
onClick={connect}
onClick={() => {
connect();
posthog.capture('live_blog_reconnected', {
live_blog_id: liveBlog.id,
live_blog_slug: slug,
reconnect_attempts: reconnectAttempts,
});
}}
>
Reconnect
</Button>
@@ -3,6 +3,7 @@ import { ShareButton } from './ShareButton';
import { CopyLinkButton } from './CopyLinkButton';
import { type SharePlatform, type ShareData, getShareUrl } from '@/lib/social-utils';
import { trackShare } from '@/lib/analytics';
import { usePostHog } from '@posthog/react';
export type SocialShareVariant = 'default' | 'compact' | 'footer' | 'floating';
@@ -28,6 +29,7 @@ export function SocialShareButtons({
}: SocialShareButtonsProps) {
const [isTracking, setIsTracking] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const posthog = usePostHog();
const shareData: ShareData = {
title,
@@ -48,6 +50,12 @@ export function SocialShareButtons({
// Note: We don't send IP address from frontend for privacy reasons
// Backend should extract it from the request if needed
});
posthog.capture('article_shared', {
article_id: articleId,
platform,
article_title: title,
article_url: url,
});
// Call the onShare callback if provided
if (onShare) {
+22
View File
@@ -3,6 +3,7 @@ import { createContext, useContext, useState, useEffect } from 'react';
import type { ReactNode } from 'react';
import * as api from '../lib/api';
import type { User } from '@/types';
import { usePostHog } from '@posthog/react';
interface AuthContextType {
user: User | null;
@@ -25,6 +26,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const posthog = usePostHog();
useEffect(() => {
const initializeAuth = () => {
@@ -56,6 +58,15 @@ export function AuthProvider({ children }: AuthProviderProps) {
setToken(response.access_token);
setUser(response.user);
setIsLoading(false);
posthog.identify(response.user.id, {
username: response.user.username,
email: response.user.email,
role: response.user.role,
});
posthog.capture('user_logged_in', {
username: response.user.username,
role: response.user.role,
});
};
const register = async (username: string, email: string, password: string) => {
@@ -66,9 +77,20 @@ export function AuthProvider({ children }: AuthProviderProps) {
setToken(response.access_token);
setUser(response.user);
setIsLoading(false);
posthog.identify(response.user.id, {
username: response.user.username,
email: response.user.email,
role: response.user.role,
});
posthog.capture('user_registered', {
username: response.user.username,
email: response.user.email,
});
};
const logout = () => {
posthog.capture('user_logged_out');
posthog.reset();
localStorage.removeItem('token');
localStorage.removeItem('user');
setToken(null);
+6 -9
View File
@@ -13,20 +13,17 @@ initializeTheme();
const queryClient = new QueryClient()
const posthogOptions = {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
api_host: '/ingest',
ui_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST || 'https://eu.posthog.com',
defaults: '2026-01-30',
capture_exceptions: true,
debug: import.meta.env.DEV,
} as const
// Debug PostHog configuration
console.log('PostHog Config:', {
apiKey: import.meta.env.VITE_PUBLIC_POSTHOG_KEY ? '✓ Set' : '✗ Missing',
apiHost: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<StrictMode>
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={posthogOptions}
>
<QueryClientProvider client={queryClient}>