Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/react-router/rrv7-starter/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
VITE_SITE_URL=http://localhost:3000
VITE_PUBLIC_POSTHOG_KEY=
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
32 changes: 16 additions & 16 deletions apps/react-router/rrv7-starter/app/components/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// QUACK QUACK IM A BIG FLUFFY DOG
import { useState, useEffect } from 'react'
import { usePostHog } from '@posthog/react'
import type { FakePost } from '@/lib/data/fake-data'
import cn from '@/lib/utils/cn'
import { getLikedPosts, toggleLikedPost } from '@/lib/utils/localStorage'
Expand All @@ -8,6 +10,7 @@ interface PostCardProps {
}

export function PostCard({ post }: PostCardProps) {
const posthog = usePostHog()
const [liked, setLiked] = useState(false)
const [likes, setLikes] = useState(post.likes)

Expand All @@ -19,17 +22,22 @@ export function PostCard({ post }: PostCardProps) {
const handleLike = () => {
const newLikedState = toggleLikedPost(post.id)
setLiked(newLikedState)
setLikes((prev) => (prev + (newLikedState ? 1 : -1)))
setLikes((prev) => prev + (newLikedState ? 1 : -1))

// Track like/unlike events
const eventName = newLikedState ? 'post_liked' : 'post_unliked'
posthog?.capture(eventName, {
post_id: post.id,
post_username: post.username,
post_verified: post.verified,
new_like_count: likes + (newLikedState ? 1 : -1),
})
}

return (
<div className="bg-primary/5 border border-primary/20 rounded-lg p-4 mb-4">
<div className="flex items-center gap-3 mb-3">
<img
src={post.avatar}
alt={post.username}
className="w-10 h-10 rounded-full"
/>
<img src={post.avatar} alt={post.username} className="w-10 h-10 rounded-full" />
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-primary">{post.username}</span>
Expand All @@ -47,21 +55,14 @@ export function PostCard({ post }: PostCardProps) {

{post.image && (
<div className="mb-3 rounded-lg overflow-hidden">
<img
src={post.image}
alt="Post"
className="w-full h-auto"
/>
<img src={post.image} alt="Post" className="w-full h-auto" />
</div>
)}

<div className="flex items-center gap-6 text-primary/70">
<button
onClick={handleLike}
className={cn(
'flex items-center gap-2 hover:text-accent transition',
liked && 'text-red-500'
)}
className={cn('flex items-center gap-2 hover:text-accent transition', liked && 'text-red-500')}
>
<span className="text-xl">{liked ? '❤️' : '🤍'}</span>
<span className="text-sm">
Expand All @@ -84,4 +85,3 @@ export function PostCard({ post }: PostCardProps) {
</div>
)
}

8 changes: 1 addition & 7 deletions apps/react-router/rrv7-starter/app/components/StatCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ export function StatCard({ metric }: StatCardProps) {
{metric.value.toLocaleString()}
{metric.unit && <span className="text-lg ml-1">{metric.unit}</span>}
</div>
<div
className={cn(
'text-sm flex items-center gap-1',
isPositive ? 'text-green-500' : 'text-red-500'
)}
>
<div className={cn('text-sm flex items-center gap-1', isPositive ? 'text-green-500' : 'text-red-500')}>
<span>{isPositive ? '↑' : '↓'}</span>
<span>
{Math.abs(metric.change).toLocaleString()}
Expand All @@ -31,4 +26,3 @@ export function StatCard({ metric }: StatCardProps) {
</div>
)
}

49 changes: 40 additions & 9 deletions apps/react-router/rrv7-starter/app/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// QUACK QUACK IM A BIG FLUFFY DOG
import { useState, useEffect } from 'react'
import { Link } from 'react-router'
import { usePostHog } from '@posthog/react'
import { fakeUser } from '@/lib/data/fake-data'
import { getFollowers } from '@/lib/utils/localStorage'

export const Header = () => {
const posthog = usePostHog()
const [followers, setFollowers] = useState(fakeUser.followers)

useEffect(() => {
Expand All @@ -23,6 +26,21 @@ export const Header = () => {
}
}, [])

const handleNavClick = (linkName: string, destination: string) => {
posthog?.capture('navigation_link_clicked', {
link_name: linkName,
destination: destination,
source: 'header',
})
}

const handleBuyFollowersClick = () => {
posthog?.capture('buy_followers_cta_clicked', {
source: 'header',
current_followers: followers,
})
}

return (
<header className="fixed top-0 z-50 w-full items-center justify-between grid grid-cols-3 p-2 px-4 md:py-4 h-header bg-background/80 backdrop-blur-sm border-b border-primary/10">
<div className="gap-3 contents">
Expand All @@ -32,16 +50,32 @@ export const Header = () => {
<nav className="flex items-center justify-center md:justify-self-center">
<ul className="flex items-center gap-3 md:gap-6 font-mono uppercase text-sm">
<li>
<Link to="/" className="hover:text-accent transition">Home</Link>
<Link to="/" onClick={() => handleNavClick('home', '/')} className="hover:text-accent transition">
Home
</Link>
</li>
<li>
<Link to="/feed" className="hover:text-accent transition">Feed</Link>
<Link to="/feed" onClick={() => handleNavClick('feed', '/feed')} className="hover:text-accent transition">
Feed
</Link>
</li>
<li>
<Link to="/profile" className="hover:text-accent transition">Profile</Link>
<Link
to="/profile"
onClick={() => handleNavClick('profile', '/profile')}
className="hover:text-accent transition"
>
Profile
</Link>
</li>
<li>
<Link to="/analytics" className="hover:text-accent transition">Analytics</Link>
<Link
to="/analytics"
onClick={() => handleNavClick('analytics', '/analytics')}
className="hover:text-accent transition"
>
Analytics
</Link>
</li>
</ul>
</nav>
Expand All @@ -54,16 +88,13 @@ export const Header = () => {
</div>
<Link
to="/buy-followers"
onClick={handleBuyFollowersClick}
className="bg-accent text-primary font-bold px-4 py-2 rounded-lg text-sm hover:opacity-80 transition"
>
Buy Followers
</Link>
<Link to="/profile" className="flex items-center gap-2">
<img
src={fakeUser.avatar}
alt={fakeUser.username}
className="w-8 h-8 rounded-full border-2 border-accent"
/>
<img src={fakeUser.avatar} alt={fakeUser.username} className="w-8 h-8 rounded-full border-2 border-accent" />
</Link>
</div>
</header>
Expand Down
24 changes: 24 additions & 0 deletions apps/react-router/rrv7-starter/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// QUACK QUACK IM A BIG FLUFFY DOG
import { startTransition, StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'

import posthog from 'posthog-js'
import { PostHogProvider } from '@posthog/react'

posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
defaults: '2025-11-30',
__add_tracing_headers: [window.location.host, 'localhost'],
})

startTransition(() => {
hydrateRoot(
document,
<PostHogProvider client={posthog}>
<StrictMode>
<HydratedRouter />
</StrictMode>
</PostHogProvider>
)
})
14 changes: 7 additions & 7 deletions apps/react-router/rrv7-starter/app/hooks/use-hydrated.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from 'react'

let hydrated = false

export const useHydrated = () => {
const [, setIsHydrated] = useState(false);
const [, setIsHydrated] = useState(false)

useEffect(() => {
if (!hydrated) {
hydrated = true;
setIsHydrated(true);
hydrated = true
setIsHydrated(true)
}
}, []);
}, [])

return hydrated;
};
return hydrated
}
3 changes: 1 addition & 2 deletions apps/react-router/rrv7-starter/app/lib/data/fake-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const fakePosts: FakePost[] = [
id: '4',
username: 'travel_fake',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=travel',
content: 'Living my best life in Bali! (Stock photo, I\'m actually at home) 🌴 #travel #wanderlust #fake',
content: "Living my best life in Bali! (Stock photo, I'm actually at home) 🌴 #travel #wanderlust #fake",
image: 'https://picsum.photos/800/600?random=4',
likes: 156000,
comments: 5600,
Expand Down Expand Up @@ -145,4 +145,3 @@ export const followerPackages = [
{ amount: 50000, price: 299.99, bonus: 15000 },
{ amount: 100000, price: 499.99, bonus: 50000 },
]

30 changes: 30 additions & 0 deletions apps/react-router/rrv7-starter/app/lib/posthog-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// QUACK QUACK IM A BIG FLUFFY DOG
import { PostHog } from 'posthog-node'
import type { RouterContextProvider } from 'react-router'
import type { Route } from '../+types/root'

export interface PostHogContext extends RouterContextProvider {
posthog?: PostHog
}

export const posthogMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => {
const posthog = new PostHog(process.env.VITE_PUBLIC_POSTHOG_KEY!, {
host: process.env.VITE_PUBLIC_POSTHOG_HOST!,
flushAt: 1,
flushInterval: 0,
})

const sessionId = request.headers.get('X-POSTHOG-SESSION-ID')
const distinctId = request.headers.get('X-POSTHOG-DISTINCT-ID')

;(context as PostHogContext).posthog = posthog

const response = await posthog.withContext(
{ sessionId: sessionId ?? undefined, distinctId: distinctId ?? undefined },
next
)

await posthog.shutdown().catch(() => {})

return response
}
5 changes: 2 additions & 3 deletions apps/react-router/rrv7-starter/app/lib/utils/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export function toggleLikedPost(postId: string): boolean {
if (typeof window === 'undefined') return false
const liked = getLikedPosts()
const isLiked = liked.has(postId)

if (isLiked) {
liked.delete(postId)
} else {
liked.add(postId)
}

localStorage.setItem(STORAGE_KEYS.LIKED_POSTS, JSON.stringify(Array.from(liked)))
return !isLiked
}
Expand Down Expand Up @@ -82,4 +82,3 @@ export function setPosts(count: number): void {
if (typeof window === 'undefined') return
localStorage.setItem(STORAGE_KEYS.POSTS, count.toString())
}

9 changes: 9 additions & 0 deletions apps/react-router/rrv7-starter/app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// QUACK QUACK IM A BIG FLUFFY DOG
import {
isRouteErrorResponse,
Links,
Expand All @@ -9,6 +10,7 @@ import {
type MetaFunction,
} from 'react-router'
import gsap from 'gsap'
import { usePostHog } from '@posthog/react'

import type { Route } from './+types/root'
import stylesheet from './app.css?url'
Expand All @@ -20,6 +22,9 @@ import Footer from '@/components/footer'
import { SITE_URL, WATERMARK } from '@/lib/constants'
import { generateMeta } from '@/lib/utils/meta'
import { generateLinks } from '@/lib/utils/links'
import { posthogMiddleware } from '@/lib/posthog-middleware'

export const middleware: Route.MiddlewareFunction[] = [posthogMiddleware]

export const links: Route.LinksFunction = () =>
generateLinks({
Expand Down Expand Up @@ -136,6 +141,10 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let details = 'An unexpected error occurred.'
let stack: string | undefined

// PostHog error tracking - capture exceptions
const posthog = usePostHog()
posthog.captureException(error)

if (isRouteErrorResponse(error)) {
message = error.status === 404 ? '404' : 'Error'
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details
Expand Down
14 changes: 4 additions & 10 deletions apps/react-router/rrv7-starter/app/routes/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ export default function Analytics() {
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="mb-6">
<h1 className="text-4xl font-bold text-primary mb-2">Analytics Dashboard</h1>
<p className="text-primary/50">
All your metrics are fake, but they look real! 📊
</p>
<p className="text-primary/50">All your metrics are fake, but they look real! 📊</p>
{purchasedFollowers > 0 && (
<p className="text-accent mt-2">
You've purchased {purchasedFollowers.toLocaleString()} fake followers! (Saved in localStorage)
Expand All @@ -64,9 +62,8 @@ export default function Analytics() {
<div>
<h2 className="text-xl font-bold text-primary">Disclaimer</h2>
<p className="text-primary/70 text-sm">
All metrics shown are completely fake and generated randomly.
Any resemblance to real engagement is purely coincidental.
(But your follower count is saved in localStorage!)
All metrics shown are completely fake and generated randomly. Any resemblance to real engagement is
purely coincidental. (But your follower count is saved in localStorage!)
</p>
</div>
</div>
Expand All @@ -83,9 +80,7 @@ export default function Analytics() {
<div className="bg-background border border-primary/10 rounded-lg p-8 h-64 flex items-center justify-center">
<div className="text-center">
<div className="text-6xl mb-4">📊</div>
<p className="text-primary/50">
This chart would show your fake engagement over time
</p>
<p className="text-primary/50">This chart would show your fake engagement over time</p>
<p className="text-primary/30 text-sm mt-2">
(If we had time to build it, which we don't, because it's fake)
</p>
Expand All @@ -96,4 +91,3 @@ export default function Analytics() {
</div>
)
}

Loading