diff --git a/.vscode/settings.json b/.vscode/settings.json index 99438eb..16915e3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "typescript.tsdk": "../../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "cSpell.words": ["posthog"] } diff --git a/README.md b/README.md index 230850c..9a5ba16 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,12 @@ pnpm run dev - Tailwind CSS class sorting, merging and linting. - Next js Server Side Components (SSC) & Client Side Components (CSC) - Next js Form Actions +- Analytics with Posthog ## Roadmap - [✅] Add Login and Register -- [ ] Add Analytics +- [✅] Add Analytics - [ ] Add Installation Instructions Documentation - [ ] Add Dashboard Page - [ ] Add Simple Blog diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 5fdd85a..70e6973 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,9 +1,10 @@ -import Link from "next/link" +import posthog from "posthog-js" import { env } from "@/env.mjs" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" +import { AdvancedLink } from "@/components/advanced/advanced-link" async function getGitHubStars(): Promise { try { @@ -39,16 +40,18 @@ export default async function IndexPage() { <>
- social.name === "Twitter") ?.url ?? "#" } className="rounded-2xl bg-muted px-4 py-1.5 text-sm font-medium" target="_blank" + analyticsValue="clicked_follow_on_twitter" + analyticsProperties={{ source: "home_page" }} > Follow along on Twitter - +

Next.js 13 & Directus CMS Starter 🚀

@@ -60,15 +63,23 @@ export default async function IndexPage() { updates.

- + Get Started - - + + Features - +
@@ -195,7 +206,7 @@ export default async function IndexPage() {

The main objective is to help developers who use Directus and Next Js to build their apps faster. - social.name === "Github") ?.url ?? "#" @@ -203,17 +214,21 @@ export default async function IndexPage() { target="_blank" rel="noreferrer" className="underline underline-offset-4" + analyticsValue="github" + analyticsProperties={{ source: "home_page" }} > GitHub - + .{" "}

{stars && ( -
- + )} diff --git a/app/layout.tsx b/app/layout.tsx index 05b7d75..e27a318 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,6 +5,7 @@ import { RootLayoutProps } from "@/types/general" import { siteConfig } from "@/config/site" import { fontSans } from "@/lib/fonts" import { cn } from "@/lib/utils" +import { CustomPostHogProvider } from "@/components/misc/posthog-provider" import { ProgressBar } from "@/components/misc/progress" import CustomProvider from "@/components/misc/state-provider" // import Seo from "@/components/misc/seo" @@ -76,11 +77,13 @@ export default function RootLayout({ children }: RootLayoutProps) { > {/* */} - -
- {children} -
-
+ + +
+ {children} +
+
+
{/*
*/} diff --git a/components/advanced/advanced-link.tsx b/components/advanced/advanced-link.tsx new file mode 100644 index 0000000..7991f7c --- /dev/null +++ b/components/advanced/advanced-link.tsx @@ -0,0 +1,47 @@ +"use client" + +import { ReactNode, useEffect } from "react" +import Link from "next/link" +import posthog from "posthog-js" + +export function AdvancedLink({ + children, + href, // Extract href directly + className, // Extract className directly + onClick, // Extract onClick directly + analyticsValue, // Extract analyticsValue directly + analyticsProperties, // Extract analyticsProperties directly + // any other props that come through + ...props +}: { + children: ReactNode + href: string + className?: string + onClick?: () => void + analyticsValue?: string + analyticsProperties?: object | null + [key: string]: any +}) { + const captureEvent = () => { + // Add Posthog tracking when the component is clicked. + if (analyticsValue) { + posthog?.capture(`${analyticsValue ?? "clicked_link"}`, { + properties: analyticsProperties ?? {}, + }) + } + } + + return ( + { + captureEvent() + onClick && onClick() + }} + {...props} + > + {children} + + ) +} diff --git a/components/auth/forgot-password/ForgotPasswordAuthForm.tsx b/components/auth/forgot-password/ForgotPasswordAuthForm.tsx index a8f2dcb..8c5eb13 100644 --- a/components/auth/forgot-password/ForgotPasswordAuthForm.tsx +++ b/components/auth/forgot-password/ForgotPasswordAuthForm.tsx @@ -3,6 +3,7 @@ import { useRouter } from "next/navigation" import { forgotPassword } from "@/actions/authForms" import { zodResolver } from "@hookform/resolvers/zod" +import posthog from "posthog-js" import { useForm } from "react-hook-form" import * as z from "zod" @@ -36,6 +37,7 @@ export function ForgotPasswordAuthForm() { const onSubmit = async (data: AccountFormValues) => { const { email } = data + posthog?.capture("user_forgot_password") const result = await forgotPassword(email) console.log(result) toast({ diff --git a/components/auth/login/LoginAuthForm.tsx b/components/auth/login/LoginAuthForm.tsx index 61a5fac..93e062e 100644 --- a/components/auth/login/LoginAuthForm.tsx +++ b/components/auth/login/LoginAuthForm.tsx @@ -1,17 +1,19 @@ "use client" +import { useEffect } from "react" import Link from "next/link" import { useRouter } from "next/navigation" import { login } from "@/actions/authForms" -import { logoutUser, setAuthState } from "@/store/slices/auth" +import { logoutUser, setAuthState, storedUser } from "@/store/slices/auth" import { zodResolver } from "@hookform/resolvers/zod" import { format } from "date-fns" +import { usePostHog } from "posthog-js/react" import { useDirectus } from "react-directus" import { useForm } from "react-hook-form" import * as z from "zod" import { cn } from "@/lib/utils" -import { useStoreDispatch } from "@/hooks/useStore" +import { useStoreDispatch, useStoreSelector } from "@/hooks/useStore" import { Button } from "@/components/ui/button" import { Form, @@ -61,8 +63,10 @@ export function LoginAuthForm() { localStorage.removeItem("logged-out") } } + const posthog = usePostHog() const router = useRouter() + let user = useStoreSelector(storedUser) const form = useForm({ resolver: zodResolver(accountFormSchema), defaultValues, @@ -86,6 +90,11 @@ export function LoginAuthForm() { if (loginResult.success) { dispatch(setAuthState(loginResult.data)) + posthog?.identify(loginResult.data.user.id, { + email: loginResult.data.user.email, + }) + posthog?.group("role", loginResult.data.user.role) + posthog?.capture("user_log_in") // await 2 seconds to allow the auth state to be set await new Promise((resolve) => setTimeout(resolve, 2000)) router.push("/") diff --git a/components/auth/signIn/SignInAuthForm.tsx b/components/auth/signIn/SignInAuthForm.tsx index cb52078..8e2f91b 100644 --- a/components/auth/signIn/SignInAuthForm.tsx +++ b/components/auth/signIn/SignInAuthForm.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation" import { register } from "@/actions/authForms" import { setAuthState } from "@/store/slices/auth" import { zodResolver } from "@hookform/resolvers/zod" +import { usePostHog } from "posthog-js/react" import { useForm } from "react-hook-form" import * as z from "zod" @@ -56,6 +57,7 @@ export function SignInAuthForm() { resolver: zodResolver(accountFormSchema), defaultValues, }) + const posthog = usePostHog() const onSubmit = async (data: AccountFormValues) => { const { email, password, first_name, last_name } = data @@ -79,6 +81,11 @@ export function SignInAuthForm() { if (registerResult.success) { dispatch(setAuthState(registerResult.data)) + posthog?.identify(registerResult.data.user.id, { + email: registerResult.data.user.email, + }) + posthog?.group("role", registerResult.data.user.role) + posthog?.capture("user_sign_in") // wait for 2 seconds await new Promise((resolve) => setTimeout(resolve, 2000)) router.push("/") diff --git a/components/misc/posthog-provider.tsx b/components/misc/posthog-provider.tsx new file mode 100644 index 0000000..77b76e7 --- /dev/null +++ b/components/misc/posthog-provider.tsx @@ -0,0 +1,21 @@ +"use client" + +import posthog from "posthog-js" +import { PostHogProvider } from "posthog-js/react" + +export function CustomPostHogProvider({ children }: any) { + // Check that PostHog is client-side (used to handle Next.js SSR) + if (typeof window !== "undefined") { + posthog.init(process.env.POSTHOG_KEY || "", { + api_host: process.env.POSTHOG_HOST_URL || "https://app.posthog.com", + // Enable debug mode in development + loaded: (posthog) => { + if (process.env.NODE_ENV === "development") posthog.debug() + console.log("PostHog debug enabled") + }, + capture_pageview: true, + }) + } + + return {children} +} diff --git a/components/misc/profile-icon.tsx b/components/misc/profile-icon.tsx index c1be928..1b10436 100644 --- a/components/misc/profile-icon.tsx +++ b/components/misc/profile-icon.tsx @@ -6,6 +6,7 @@ import { storedToken, storedUser, } from "@/store/slices/auth" +import { usePostHog } from "posthog-js/react" import { useStoreDispatch, useStoreSelector } from "@/hooks/useStore" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" @@ -27,6 +28,7 @@ export function ProfileIcon() { const user = useStoreSelector(storedUser) const auth = useStoreSelector(storedAuth) const token = useStoreSelector(storedToken) + const posthog = usePostHog() // we can not get the user or auth from the store, so we will log them out and we return null if (!user || !auth || !token) { @@ -38,6 +40,10 @@ export function ProfileIcon() { dispatch(logoutUser()) // wait 1 second to show the toast setTimeout(() => {}, 1000) + posthog?.identify(user.id, { + email: user.email, + }) + posthog?.capture("user_log_out") toast({ title: "Logged Out 👋 ", variant: "default", diff --git a/components/misc/theme-toggle.tsx b/components/misc/theme-toggle.tsx index 01e846c..7cc74f2 100644 --- a/components/misc/theme-toggle.tsx +++ b/components/misc/theme-toggle.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { Moon, Sun } from "lucide-react" import { useTheme } from "next-themes" +import posthog from "posthog-js" import { Button } from "@/components/ui/button" @@ -13,7 +14,14 @@ export function ThemeToggle() {