diff --git a/.env.example b/.env.example
index e9b7b09..d297e7c 100644
--- a/.env.example
+++ b/.env.example
@@ -22,3 +22,7 @@ CLERK_SECRET_KEY=some-secret-key
# Uploadthing
UPLOADTHING_TOKEN='uploadthing-token'
+
+# Posthog
+NEXT_PUBLIC_POSTHOG_KEY=some-posthog-key
+NEXT_PUBLIC_POSTHOG_HOST=the-posthog-host
diff --git a/README.md b/README.md
index a7dea1f..354734d 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ Tracking progress on key features and tasks for the project.
- [x] 🔗 Sync folder open state with the URL
- [x] 🔐 Implement user authentication
- [x] 📁 Enable file upload functionality
-- [ ] 📊 Add analytics tracking
+- [x] 📊 Add analytics tracking
### 📝 Note from 5-28-2025
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..fa67400
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,13 @@
+[[redirects]]
+ from = "/ingest/static/*"
+ to = "https://us-assets.i.posthog.com/static/:splat"
+ host = "us-assets.i.posthog.com"
+ status = 200
+ force = true
+
+[[redirects]]
+ from = "/ingest/*"
+ to = "https://us.i.posthog.com/:splat"
+ host = "us.i.posthog.com"
+ status = 200
+ force = true
diff --git a/package.json b/package.json
index 527b754..db12f6d 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"lucide-react": "^0.511.0",
"mysql2": "^3.14.1",
"next": "^15.2.3",
+ "posthog-js": "^1.257.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0de454c..9ebd051 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
next:
specifier: ^15.2.3
version: 15.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ posthog-js:
+ specifier: ^1.257.0
+ version: 1.257.0
react:
specifier: ^19.0.0
version: 19.1.0
@@ -1281,6 +1284,9 @@ packages:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
+ core-js@3.44.0:
+ resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==}
+
cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'}
@@ -1717,6 +1723,9 @@ packages:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
+ fflate@0.4.8:
+ resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -2374,6 +2383,20 @@ packages:
resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
engines: {node: ^10 || ^12 || >=14}
+ posthog-js@1.257.0:
+ resolution: {integrity: sha512-Ujg9RGtWVCu+4tmlRpALSy2ZOZI6JtieSYXIDDdgMWm167KYKvTtbMPHdoBaPWcNu0Km+1hAIBnQFygyn30KhA==}
+ peerDependencies:
+ '@rrweb/types': 2.0.0-alpha.17
+ rrweb-snapshot: 2.0.0-alpha.17
+ peerDependenciesMeta:
+ '@rrweb/types':
+ optional: true
+ rrweb-snapshot:
+ optional: true
+
+ preact@10.26.9:
+ resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==}
+
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2835,6 +2858,9 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
+ web-vitals@4.2.4:
+ resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
+
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -3879,6 +3905,8 @@ snapshots:
cookie@1.0.2: {}
+ core-js@3.44.0: {}
+
cors@2.8.5:
dependencies:
object-assign: 4.1.1
@@ -4441,6 +4469,8 @@ snapshots:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
+ fflate@0.4.8: {}
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -5106,6 +5136,15 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
+ posthog-js@1.257.0:
+ dependencies:
+ core-js: 3.44.0
+ fflate: 0.4.8
+ preact: 10.26.9
+ web-vitals: 4.2.4
+
+ preact@10.26.9: {}
+
prelude-ls@1.2.1: {}
prettier-plugin-tailwindcss@0.6.11(prettier@3.5.3):
@@ -5607,6 +5646,8 @@ snapshots:
web-streams-polyfill@3.3.3: {}
+ web-vitals@4.2.4: {}
+
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
diff --git a/src/app/page.tsx b/src/app/(home)/page.tsx
similarity index 100%
rename from src/app/page.tsx
rename to src/app/(home)/page.tsx
diff --git a/src/app/_providers/posthog-provider.tsx b/src/app/_providers/posthog-provider.tsx
new file mode 100644
index 0000000..7177df8
--- /dev/null
+++ b/src/app/_providers/posthog-provider.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import posthog from "posthog-js";
+import { PostHogProvider as PHProvider } from "posthog-js/react";
+import { useEffect } from "react";
+
+import UserIdentification from "./user-identification";
+
+import { env } from "~/env";
+
+export function PostHogProvider({ children }: { children: React.ReactNode }) {
+ useEffect(() => {
+ posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
+ api_host: env.NEXT_PUBLIC_POSTHOG_HOST,
+ person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well
+ defaults: "2025-05-24",
+ });
+ }, []);
+
+ return (
+
+
+ {children}
+
+ );
+}
diff --git a/src/app/_providers/user-identification.tsx b/src/app/_providers/user-identification.tsx
new file mode 100644
index 0000000..5c1d02b
--- /dev/null
+++ b/src/app/_providers/user-identification.tsx
@@ -0,0 +1,20 @@
+import { useUser } from "@clerk/nextjs";
+import { usePostHog } from "posthog-js/react";
+import { useEffect } from "react";
+
+export default function UserIdentification() {
+ const posthog = usePostHog();
+ const { user } = useUser();
+
+ useEffect(() => {
+ if (user) {
+ posthog.identify(user.id, {
+ email: user.emailAddresses[0]?.emailAddress,
+ });
+ } else {
+ posthog.reset();
+ }
+ }, [posthog, user]);
+
+ return null; // Do not render anything
+}
diff --git a/src/app/f/[folderId]/page.tsx b/src/app/f/[folderId]/page.tsx
index f788a9b..2b189b9 100644
--- a/src/app/f/[folderId]/page.tsx
+++ b/src/app/f/[folderId]/page.tsx
@@ -2,7 +2,7 @@ import { z } from "zod";
import * as queries from "~/server/db/queries";
-import DriveContents from "../../drive-contents";
+import DriveContents from "../drive-contents";
export default async function GoogleDriveClone(props: {
params: Promise<{ folderId: number }>;
diff --git a/src/app/drive-contents.tsx b/src/app/f/drive-contents.tsx
similarity index 100%
rename from src/app/drive-contents.tsx
rename to src/app/f/drive-contents.tsx
diff --git a/src/app/file-row.tsx b/src/app/f/file-row.tsx
similarity index 100%
rename from src/app/file-row.tsx
rename to src/app/f/file-row.tsx
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d442c0e..7aa0a8a 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,9 +1,11 @@
-import "~/styles/globals.css";
-
import { ClerkProvider } from "@clerk/nextjs";
import { type Metadata } from "next";
import { Geist } from "next/font/google";
+import { PostHogProvider } from "./_providers/posthog-provider";
+
+import "~/styles/globals.css";
+
export const metadata: Metadata = {
title: "Drive Tutorial",
description: "It's like Google Drive, but worse!",
@@ -20,9 +22,11 @@ export default function RootLayout({
}: Readonly<{ children: React.ReactNode }>) {
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/src/env.js b/src/env.js
index 9f7c80d..1aedd71 100644
--- a/src/env.js
+++ b/src/env.js
@@ -23,7 +23,8 @@ export const env = createEnv({
* `NEXT_PUBLIC_`.
*/
client: {
- // NEXT_PUBLIC_CLIENTVAR: z.string(),
+ NEXT_PUBLIC_POSTHOG_KEY: z.string(),
+ NEXT_PUBLIC_POSTHOG_HOST: z.string(),
},
/**
@@ -37,7 +38,8 @@ export const env = createEnv({
SINGLESTORE_HOST: process.env.SINGLESTORE_HOST,
SINGLESTORE_PORT: process.env.SINGLESTORE_PORT,
SINGLESTORE_DATABASE: process.env.SINGLESTORE_DATABASE,
- // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
+ NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
+ NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially