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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
VITE_PUBLIC_POSTHOG_PROJECT_TOKEN=
VITE_PUBLIC_POSTHOG_HOST=
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
import { createContext, useContext, useState, useEffect, useRef, type ReactNode } from 'react'
import type { FakeUser } from '~/lib/utils/auth'
import { getCurrentUser, setCurrentUser, fakeLogin, fakeSignup, fakeLogout } from '~/lib/utils/auth'

Expand All @@ -14,12 +14,41 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<FakeUser | null>(null)
const identifiedUserId = useRef<string | null>(null)

useEffect(() => {
const currentUser = getCurrentUser()
setUser(currentUser)
}, [])

useEffect(() => {
let cancelled = false

if (!user) {
return () => {
cancelled = true
}
}

void import('~/lib/posthog.client').then(({ default: posthog, isPostHogEnabled }) => {
if (cancelled || !isPostHogEnabled) return

if (identifiedUserId.current && identifiedUserId.current !== user.id) {
posthog.reset()
}

posthog.identify(user.id, {
email: user.email,
username: user.username,
})
identifiedUserId.current = user.id
})

return () => {
cancelled = true
}
}, [user?.id])

const login = (username: string, password: string): boolean => {
const loggedInUser = fakeLogin(username, password)
if (loggedInUser) {
Expand All @@ -41,6 +70,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}

const logout = () => {
void import('~/lib/posthog.client').then(({ default: posthog, isPostHogEnabled }) => {
if (isPostHogEnabled) {
posthog.reset()
}
})
identifiedUserId.current = null
fakeLogout()
setUser(null)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import "./lib/posthog.client";

startTransition(() => {
hydrateRoot(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import posthog, { isPostHogEnabled } from "~/lib/posthog.client";

type LogAttributes = Record<string, string | number | boolean>;

export const posthogLogger = {
info(message: string, attributes: LogAttributes) {
if (isPostHogEnabled) {
posthog.logger.info(message, attributes);
}
},
warn(message: string, attributes: LogAttributes) {
if (isPostHogEnabled) {
posthog.logger.warn(message, attributes);
}
},
error(message: string, attributes: LogAttributes) {
if (isPostHogEnabled) {
posthog.logger.error(message, attributes);
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import posthog from "posthog-js";

const projectToken = import.meta.env.VITE_PUBLIC_POSTHOG_PROJECT_TOKEN;
const host = import.meta.env.VITE_PUBLIC_POSTHOG_HOST;
export const isPostHogEnabled = Boolean(projectToken && host);

if (!projectToken) {
if (import.meta.env.DEV) {
throw new Error(
"VITE_PUBLIC_POSTHOG_PROJECT_TOKEN variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once VITE_PUBLIC_POSTHOG_PROJECT_TOKEN is configured",
);
}
} else if (!host) {
if (import.meta.env.DEV) {
throw new Error(
"VITE_PUBLIC_POSTHOG_HOST variable required by PostHog is missing or un-configured, this causes events to be silently missed. This error stops appearing once VITE_PUBLIC_POSTHOG_HOST is configured",
);
}
} else {
posthog.init(projectToken, {
api_host: host,
defaults: "2026-08-30",
logs: {
serviceName: "rest-explorer-web",
environment: import.meta.env.MODE,
},
capture_exceptions: {
capture_unhandled_errors: true,
capture_unhandled_rejections: true,
capture_console_errors: false,
},
});
}

export default posthog;
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,28 @@ export async function clientLoader() {
// Check if API returned an error object
if (data.status === 404 || (data.message && !Array.isArray(data))) {
console.error("API Error:", data.message || data.status);
const { posthogLogger } = await import("~/lib/posthog-logger.client");
posthogLogger.warn("countries_catalog_api_rejected", {
source: "rest_countries",
});
return [];
}

// Ensure we return an array
const countries = Array.isArray(data) ? data : [];
console.log(`Loaded ${countries.length} countries from API`);
const { posthogLogger } = await import("~/lib/posthog-logger.client");
posthogLogger.info("countries_catalog_loaded", {
country_count: countries.length,
source: "rest_countries",
});
return countries;
} catch (error) {
console.error("Error loading countries:", error);
const { posthogLogger } = await import("~/lib/posthog-logger.client");
posthogLogger.error("countries_catalog_fetch_failed", {
source: "rest_countries",
});
// Return empty array on error to prevent crashes
return [];
}
Expand All @@ -50,6 +63,16 @@ export default function Countries({ loaderData }: Route.ComponentProps) {
setRegion(newRegion);
};

const captureCountryAction = async (
event: "country_claimed" | "country_liked" | "country_visited",
countryName: string,
) => {
const { default: posthog, isPostHogEnabled } = await import("~/lib/posthog.client");
if (isPostHogEnabled) {
posthog.capture(event, { country_name: countryName });
}
};

// Ensure loaderData is an array, fallback to empty array
const countries = Array.isArray(loaderData) ? loaderData : [];

Expand Down Expand Up @@ -110,6 +133,7 @@ export default function Countries({ loaderData }: Route.ComponentProps) {
const countryName = country.name.common;
const isClaimed = user?.claimedCountries.includes(countryName);
const isLiked = user?.likedCountries.includes(countryName);
const isVisited = user?.visitedCountries.includes(countryName);

return (
<li
Expand All @@ -136,8 +160,11 @@ export default function Countries({ loaderData }: Route.ComponentProps) {
{user ? (
<div className="flex gap-2 mt-3">
<button
onClick={() => {
claimCountry(countryName);
onClick={async () => {
if (!isClaimed) {
claimCountry(countryName);
await captureCountryAction("country_claimed", countryName);
}
window.location.reload();
}}
className={`flex-1 px-3 py-2 text-xs rounded-lg font-medium transition ${
Expand All @@ -149,8 +176,11 @@ export default function Countries({ loaderData }: Route.ComponentProps) {
{isClaimed ? '👑 Claimed' : '🏴 Claim'}
</button>
<button
onClick={() => {
likeCountry(countryName);
onClick={async () => {
if (!isLiked) {
likeCountry(countryName);
await captureCountryAction("country_liked", countryName);
}
window.location.reload();
}}
className={`px-3 py-2 text-xs rounded-lg font-medium transition ${
Expand All @@ -162,8 +192,11 @@ export default function Countries({ loaderData }: Route.ComponentProps) {
{isLiked ? '❤️' : '🤍'}
</button>
<button
onClick={() => {
visitCountry(countryName);
onClick={async () => {
if (!isVisited) {
visitCountry(countryName);
await captureCountryAction("country_visited", countryName);
}
window.location.reload();
}}
className="px-3 py-2 text-xs rounded-lg font-medium bg-blue-100 text-blue-700 hover:bg-blue-200 transition"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ export default function Login() {
setIsLoading(false)

if (success) {
const loggedInUser = getCurrentUser()
void import('~/lib/posthog.client').then(({ default: posthog, isPostHogEnabled }) => {
if (isPostHogEnabled && loggedInUser) {
posthog.identify(loggedInUser.id, {
email: loggedInUser.email,
username: loggedInUser.username,
})
posthog.capture('user_logged_in')
}
})
navigate('/profile')
} else {
setError('Invalid credentials! (But this is fake, so any password works if the username exists)')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export default function Profile() {

const currentUser = getCurrentUser() || user

const handleLogout = async () => {
const { default: posthog, isPostHogEnabled } = await import('~/lib/posthog.client')
if (isPostHogEnabled) {
posthog.capture('user_logged_out')
}
logout()
}

return (
<div className="min-h-screen bg-gradient-to-br from-indigo-50 to-purple-50 p-6">
<div className="container mx-auto max-w-4xl">
Expand Down Expand Up @@ -64,7 +72,7 @@ export default function Profile() {
View Stats
</Link>
<button
onClick={logout}
onClick={handleLogout}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition"
>
Logout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export default function Signup() {
setIsLoading(false)

if (newUser) {
void import('~/lib/posthog.client').then(({ default: posthog, isPostHogEnabled }) => {
if (isPostHogEnabled) {
posthog.identify(newUser.id, {
email: newUser.email,
username: newUser.username,
})
posthog.capture('user_signed_up')
}
})
navigate('/profile')
} else {
setError('Signup failed! (But this is fake, so it should always work)')
Expand Down
Loading
Loading