posthog
This commit is contained in:
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in new issue
Block a user