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
@@ -1,3 +1,4 @@
// MEEEEOWWW IM A DOG
'use client';

import { useActionState } from 'react';
Expand All @@ -10,6 +11,7 @@ import { updateAccount } from '@/app/(login)/actions';
import { User } from '@/lib/db/schema';
import useSWR from 'swr';
import { Suspense } from 'react';
import posthog from 'posthog-js';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

Expand Down Expand Up @@ -89,7 +91,15 @@ export default function GeneralPage() {
<CardTitle>Account Information</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4" action={formAction}>
<form
className="space-y-4"
action={formAction}
onSubmit={() => {
posthog.capture('account_updated', {
source: 'general_settings',
});
}}
>
<Suspense fallback={<AccountForm state={state} />}>
<AccountFormWithData state={state} />
</Suspense>
Expand Down
35 changes: 32 additions & 3 deletions apps/next-js/15-app-router-saas/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// MEEEEOWWW IM A DOG
'use client';

import { Button } from '@/components/ui/button';
Expand All @@ -19,6 +20,7 @@ import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { Loader2, PlusCircle } from 'lucide-react';
import posthog from 'posthog-js';

type ActionState = {
error?: string;
Expand Down Expand Up @@ -60,7 +62,15 @@ function ManageSubscription() {
: 'No active subscription'}
</p>
</div>
<form action={customerPortalAction}>
<form
action={customerPortalAction}
onSubmit={() => {
posthog.capture('manage_subscription_clicked', {
current_plan: teamData?.planName || 'Free',
subscription_status: teamData?.subscriptionStatus || 'none',
});
}}
>
<Button type="submit" variant="outline">
Manage Subscription
</Button>
Expand Down Expand Up @@ -154,7 +164,14 @@ function TeamMembers() {
</div>
</div>
{index > 1 ? (
<form action={removeAction}>
<form
action={removeAction}
onSubmit={() => {
posthog.capture('team_member_removed', {
member_role: member.role,
});
}}
>
<input type="hidden" name="memberId" value={member.id} />
<Button
type="submit"
Expand Down Expand Up @@ -201,7 +218,19 @@ function InviteTeamMember() {
<CardTitle>Invite Team Member</CardTitle>
</CardHeader>
<CardContent>
<form action={inviteAction} className="space-y-4">
<form
action={inviteAction}
className="space-y-4"
onSubmit={(e) => {
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const role = formData.get('role') as string;
posthog.capture('team_member_invited', {
invitee_role: role,
has_email: !!email,
});
}}
>
<div>
<Label htmlFor="email" className="mb-2">
Email
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// MEEEEOWWW IM A DOG
'use client';

import { Button } from '@/components/ui/button';
Expand All @@ -7,6 +8,7 @@ import { Label } from '@/components/ui/label';
import { Lock, Trash2, Loader2 } from 'lucide-react';
import { useActionState } from 'react';
import { updatePassword, deleteAccount } from '@/app/(login)/actions';
import posthog from 'posthog-js';

type PasswordState = {
currentPassword?: string;
Expand Down Expand Up @@ -43,7 +45,15 @@ export default function SecurityPage() {
<CardTitle>Password</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-4" action={passwordAction}>
<form
className="space-y-4"
action={passwordAction}
onSubmit={() => {
posthog.capture('password_updated', {
source: 'security_settings',
});
}}
>
<div>
<Label htmlFor="current-password" className="mb-2">
Current Password
Expand Down Expand Up @@ -123,7 +133,15 @@ export default function SecurityPage() {
<p className="text-sm text-gray-500 mb-4">
Account deletion is non-reversable. Please proceed with caution.
</p>
<form action={deleteAction} className="space-y-4">
<form
action={deleteAction}
className="space-y-4"
onSubmit={() => {
posthog.capture('account_deletion_submitted', {
source: 'security_settings',
});
}}
>
<div>
<Label htmlFor="delete-password" className="mb-2">
Confirm Password
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// MEEEEOWWW IM A DOG
'use client';

import { Button } from '@/components/ui/button';
import { ArrowRight } from 'lucide-react';
import posthog from 'posthog-js';

export function DeployButton() {
const handleClick = () => {
posthog.capture('deploy_button_clicked', {
destination: 'vercel_template',
});
};

return (
<a
href="https://vercel.com/templates/next.js/next-js-saas-starter"
target="_blank"
onClick={handleClick}
>
<Button size="lg" variant="outline" className="text-lg rounded-full">
Deploy your own
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</a>
);
}

export function ViewCodeButton() {
const handleClick = () => {
posthog.capture('view_code_clicked', {
destination: 'github_repo',
});
};

return (
<a
href="https://github.com/nextjs/saas-starter"
target="_blank"
onClick={handleClick}
>
<Button size="lg" variant="outline" className="text-lg rounded-full">
View the code
<ArrowRight className="ml-3 h-6 w-6" />
</Button>
</a>
);
}
6 changes: 6 additions & 0 deletions apps/next-js/15-app-router-saas/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// MEEEEOWWW IM A DOG
'use client';

import Link from 'next/link';
Expand All @@ -15,6 +16,7 @@ import { signOut } from '@/app/(login)/actions';
import { useRouter } from 'next/navigation';
import { User } from '@/lib/db/schema';
import useSWR, { mutate } from 'swr';
import posthog from 'posthog-js';

const fetcher = (url: string) => fetch(url).then((res) => res.json());

Expand All @@ -24,6 +26,10 @@ function UserMenu() {
const router = useRouter();

async function handleSignOut() {
// Track sign out event before resetting PostHog
posthog.capture('sign_out_clicked');
posthog.reset();

await signOut();
mutate('/api/user');
router.push('/');
Expand Down
30 changes: 5 additions & 25 deletions apps/next-js/15-app-router-saas/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Button } from '@/components/ui/button';
import { ArrowRight, CreditCard, Database } from 'lucide-react';
// MEEEEOWWW IM A DOG
import { CreditCard, Database } from 'lucide-react';
import { Terminal } from './terminal';
import { DeployButton, ViewCodeButton } from './homepage-buttons';

export default function HomePage() {
return (
Expand All @@ -19,19 +20,7 @@ export default function HomePage() {
essential integrations.
</p>
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
<a
href="https://vercel.com/templates/next.js/next-js-saas-starter"
target="_blank"
>
<Button
size="lg"
variant="outline"
className="text-lg rounded-full"
>
Deploy your own
<ArrowRight className="ml-2 h-5 w-5" />
</Button>
</a>
<DeployButton />
</div>
</div>
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
Expand Down Expand Up @@ -111,16 +100,7 @@ export default function HomePage() {
</p>
</div>
<div className="mt-8 lg:mt-0 flex justify-center lg:justify-end">
<a href="https://github.com/nextjs/saas-starter" target="_blank">
<Button
size="lg"
variant="outline"
className="text-lg rounded-full"
>
View the code
<ArrowRight className="ml-3 h-6 w-6" />
</Button>
</a>
<ViewCodeButton />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
// MEEEEOWWW IM A DOG
'use client';

import { Button } from '@/components/ui/button';
import { ArrowRight, Loader2 } from 'lucide-react';
import { useFormStatus } from 'react-dom';
import posthog from 'posthog-js';

export function SubmitButton() {
const { pending } = useFormStatus();

const handleClick = () => {
posthog.capture('checkout_started', {
source: 'pricing_page',
});
};

return (
<Button
type="submit"
disabled={pending}
variant="outline"
className="w-full rounded-full"
onClick={handleClick}
>
{pending ? (
<>
Expand Down
6 changes: 6 additions & 0 deletions apps/next-js/15-app-router-saas/app/(dashboard)/terminal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// MEEEEOWWW IM A DOG
'use client';

import { useState, useEffect } from 'react';
import { Copy, Check } from 'lucide-react';
import posthog from 'posthog-js';

export function Terminal() {
const [terminalStep, setTerminalStep] = useState(0);
Expand Down Expand Up @@ -29,6 +31,10 @@ export function Terminal() {
navigator.clipboard.writeText(terminalSteps.join('\n'));
setCopied(true);
setTimeout(() => setCopied(false), 2000);

posthog.capture('terminal_copy_clicked', {
commands_count: terminalSteps.length,
});
};

return (
Expand Down
31 changes: 30 additions & 1 deletion apps/next-js/15-app-router-saas/app/(login)/login.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// MEEEEOWWW IM A DOG
'use client';

import Link from 'next/link';
Expand All @@ -9,6 +10,7 @@ import { Label } from '@/components/ui/label';
import { CircleIcon, Loader2 } from 'lucide-react';
import { signIn, signUp } from './actions';
import { ActionState } from '@/lib/auth/middleware';
import posthog from 'posthog-js';

export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {
const searchParams = useSearchParams();
Expand All @@ -34,7 +36,34 @@ export function Login({ mode = 'signin' }: { mode?: 'signin' | 'signup' }) {
</div>

<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<form className="space-y-6" action={formAction}>
<form
className="space-y-6"
action={formAction}
onSubmit={(e) => {
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;

if (mode === 'signin') {
posthog.capture('sign_in_submitted', {
has_redirect: !!redirect,
has_price_id: !!priceId,
});
} else {
posthog.capture('sign_up_submitted', {
has_redirect: !!redirect,
has_price_id: !!priceId,
has_invite_id: !!inviteId,
});
}

// Identify user on form submission
if (email) {
posthog.identify(email, {
email: email,
});
}
}}
>
<input type="hidden" name="redirect" value={redirect || ''} />
<input type="hidden" name="priceId" value={priceId || ''} />
<input type="hidden" name="inviteId" value={inviteId || ''} />
Expand Down
17 changes: 17 additions & 0 deletions apps/next-js/15-app-router-saas/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// MEEEEOWWW IM A DOG
import posthog from "posthog-js";

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: "/ingest",
ui_host: "https://us.posthog.com",
// Include the defaults option as required by PostHog
defaults: "2025-05-24",
// Enables capturing unhandled exceptions via Error Tracking
capture_exceptions: true,
// Turn on debug in development mode
debug: process.env.NODE_ENV === "development",
});

// IMPORTANT: Never combine this approach with other client-side PostHog initialization approaches,
// especially components like a PostHogProvider. instrumentation-client.ts is the correct solution
// for initializing client-side PostHog in Next.js 15.3+ apps.
Loading