From 2b34be95d30317c3149011bbb4bda0c2fbc1b2a8 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sat, 9 Nov 2024 16:41:48 -0500
Subject: [PATCH 01/31] fix: improper account dropdown

---
 .../core/components/page-wrapper/TopNavRight.tsx    | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx b/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
index a67b78ae..73560756 100644
--- a/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
+++ b/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
@@ -13,10 +13,11 @@ import {
 const TOP_NAV_RIGHT_SECTION_CLASSES = "ml-auto flex items-center space-x-6";
 
 export const TopNavRight = () => {
-  const currentUser = api.user.getCurrentUser.useQuery();
+  const currentUserQuery = api.user.getCurrentUser.useQuery();
+  const currentUser = currentUserQuery.data;
   const { setMenu, openMenuName } = React.useContext(MenuContext);
 
-  const handleCommunityClick = (e) => {
+  const handleCommunityClick = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.preventDefault();
     if (openMenuName === "community") {
       setMenu(null, "");
@@ -25,16 +26,16 @@ export const TopNavRight = () => {
     }
   };
 
-  const handleAccountClick = (e) => {
+  const handleAccountClick = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.preventDefault();
     if (openMenuName === "account") {
       setMenu(null, "");
-    } else if (currentUser.data) {
-      setMenu(<AccountMenuItems currentUser={currentUser.data} />, "account");
+    } else if (currentUser) {
+      setMenu(<AccountMenuItems currentUser={currentUser} />, "account");
     }
   };
 
-  const handleLeaderboardClick = (e) => {
+  const handleLeaderboardClick = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.preventDefault();
     if (openMenuName === "leaderboard") {
       setMenu(null, "");

From 1500162dcabd8fe3454b8ae27a3fde66b94aa78d Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 16:18:00 -0500
Subject: [PATCH 02/31] feat: migrate community page

---
 .../src/app/community/ClientCommunityPage.tsx | 82 +++++++++++++++++++
 ladderly-io/src/app/community/page.tsx        | 24 ++++++
 ladderly-io/src/server/api/routers/user.ts    | 46 +++++++++++
 3 files changed, 152 insertions(+)
 create mode 100644 ladderly-io/src/app/community/ClientCommunityPage.tsx
 create mode 100644 ladderly-io/src/app/community/page.tsx

diff --git a/ladderly-io/src/app/community/ClientCommunityPage.tsx b/ladderly-io/src/app/community/ClientCommunityPage.tsx
new file mode 100644
index 00000000..ef7e2723
--- /dev/null
+++ b/ladderly-io/src/app/community/ClientCommunityPage.tsx
@@ -0,0 +1,82 @@
+// src/app/community/ClientCommunityPage.tsx
+
+"use client";
+
+import Link from "next/link";
+import { useSearchParams, useRouter } from "next/navigation";
+import React from "react";
+import { api } from "~/trpc/react";
+
+const ITEMS_PER_PAGE = 100;
+
+interface User {
+  id: number;
+  uuid: string;
+  createdAt: Date;
+  nameFirst: string | null;
+  nameLast: string | null;
+  hasPublicProfileEnabled: boolean;
+  hasShoutOutsEnabled: boolean;
+  hasOpenToWork: boolean;
+  profileBlurb: string | null;
+  profileContactEmail: string | null;
+  profileGitHubUri: string | null;
+  profileHomepageUri: string | null;
+  profileLinkedInUri: string | null;
+  totalContributions: number;
+}
+
+export default function ClientCommunityPage() {
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const page = Number(searchParams?.get("page") ?? "0");
+
+  const { data, isLoading } = api.user.getPaginatedUsers.useQuery({
+    skip: ITEMS_PER_PAGE * page,
+    take: ITEMS_PER_PAGE,
+  });
+
+  const goToPreviousPage = () => {
+    const params = new URLSearchParams(searchParams?.toString() ?? "");
+    params.set("page", String(page - 1));
+    router.push(`?${params.toString()}`);
+  };
+
+  const goToNextPage = () => {
+    const params = new URLSearchParams(searchParams?.toString() ?? "");
+    params.set("page", String(page + 1));
+    router.push(`?${params.toString()}`);
+  };
+
+  if (isLoading) {
+    return <div>Loading...</div>;
+  }
+
+  const { users, hasMore } = data ?? { users: [], hasMore: false };
+
+  return (
+    <div>
+      <ul className="my-4">
+        {users.map((user: User) => (
+          <li key={user.id}>
+            <Link href={`/community/${user.id}`}>
+              {user.nameFirst || `User ${user.id}`}
+            </Link>
+            {user.hasOpenToWork && (
+              <span className="mx-3 inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
+                Open to Work
+              </span>
+            )}
+          </li>
+        ))}
+      </ul>
+
+      <button disabled={page === 0} onClick={goToPreviousPage}>
+        Previous
+      </button>
+      <button disabled={!hasMore} onClick={goToNextPage} className="pl-4">
+        Next
+      </button>
+    </div>
+  );
+}
diff --git a/ladderly-io/src/app/community/page.tsx b/ladderly-io/src/app/community/page.tsx
new file mode 100644
index 00000000..ea9c5174
--- /dev/null
+++ b/ladderly-io/src/app/community/page.tsx
@@ -0,0 +1,24 @@
+// src/app/community/page.tsx
+
+import { Suspense } from "react";
+import { LargeCard } from "~/app/core/components/LargeCard";
+import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
+import ClientCommunityPage from "./ClientCommunityPage";
+
+export const metadata = {
+  title: "Community",
+};
+
+export default function CommunityPage() {
+  return (
+    <LadderlyPageWrapper>
+      <LargeCard>
+        <h1 className="text-2xl font-bold text-gray-800">Member Profiles</h1>
+        <h3>Sorted by Signup Date</h3>
+        <Suspense fallback="Loading...">
+          <ClientCommunityPage />
+        </Suspense>
+      </LargeCard>
+    </LadderlyPageWrapper>
+  );
+}
diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index 10f58b5c..e0876eb6 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -4,6 +4,7 @@ import {
   publicProcedure,
 } from "~/server/api/trpc";
 import { PaymentTierEnum } from "@prisma/client";
+import { z } from "zod";
 
 const tiersOrder = {
   FREE: 0,
@@ -72,4 +73,49 @@ export const userRouter = createTRPCRouter({
 
     return { tier: minTier };
   }),
+
+  // TODO: should this be protected?
+  getPaginatedUsers: publicProcedure
+    .input(
+      z.object({
+        skip: z.number(),
+        take: z.number(),
+      })
+    )
+    .query(async ({ ctx, input }) => {
+      const { skip, take } = input;
+
+      const users = await ctx.db.user.findMany({
+        where: {
+          hasPublicProfileEnabled: true,
+        },
+        orderBy: { createdAt: 'desc' },
+        skip,
+        take: take + 1,
+        select: {
+          id: true,
+          uuid: true,
+          createdAt: true,
+          nameFirst: true,
+          nameLast: true,
+          hasPublicProfileEnabled: true,
+          hasShoutOutsEnabled: true,
+          hasOpenToWork: true,
+          profileBlurb: true,
+          profileContactEmail: true,
+          profileGitHubUri: true,
+          profileHomepageUri: true,
+          profileLinkedInUri: true,
+          totalContributions: true,
+        },
+      });
+
+      const hasMore = users.length > take;
+      const paginatedUsers = hasMore ? users.slice(0, -1) : users;
+
+      return {
+        users: paginatedUsers,
+        hasMore,
+      };
+    }),
 });

From b0328af5bb3a93e6077855042735977745ec3d7b Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 16:37:59 -0500
Subject: [PATCH 03/31] feat: migrate public profiles

---
 .../src/app/community/[userId]/page.tsx       | 135 ++++++++++++++++++
 ladderly-io/src/app/users/queries/getUser.ts  |  50 -------
 ladderly-io/src/app/users/queries/getUsers.ts |  58 --------
 ladderly-io/src/server/api/routers/user.ts    |  61 ++++++++
 4 files changed, 196 insertions(+), 108 deletions(-)
 create mode 100644 ladderly-io/src/app/community/[userId]/page.tsx
 delete mode 100644 ladderly-io/src/app/users/queries/getUser.ts
 delete mode 100644 ladderly-io/src/app/users/queries/getUsers.ts

diff --git a/ladderly-io/src/app/community/[userId]/page.tsx b/ladderly-io/src/app/community/[userId]/page.tsx
new file mode 100644
index 00000000..514441e3
--- /dev/null
+++ b/ladderly-io/src/app/community/[userId]/page.tsx
@@ -0,0 +1,135 @@
+import { Suspense } from "react";
+import { LargeCard } from "~/app/core/components/LargeCard";
+import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
+import { api } from "~/trpc/server";
+import { TRPCError } from "@trpc/server";
+
+// TODO: can we include user name in the title?
+export const metadata = {
+  title: "Public Profile",
+};
+
+interface ChecklistType {
+  id: number;
+  checklist: {
+    name: string;
+    version: string;
+  };
+  createdAt: Date;
+  isComplete: boolean;
+  updatedAt: Date;
+}
+
+async function UserProfile({ userId }: { userId: number }) {
+  try {
+    const user = await api.user.getUser({ id: userId });
+
+    return (
+      <main>
+        <h1 className="text-2xl">
+          {user.nameFirst} {user.nameLast}
+        </h1>
+
+        <div className="my-4">
+          <p>Blurb: {user.profileBlurb}</p>
+          <p>
+            Contact Email:{" "}
+            <a
+              href={`mailto:${user.profileContactEmail}`}
+              className="text-blue-600 hover:text-blue-700"
+            >
+              {user.profileContactEmail}
+            </a>
+          </p>
+          {user.profileGitHubUri ? (
+            <p>
+              GitHub:{" "}
+              <a
+                href={user.profileGitHubUri}
+                target="_blank"
+                rel="noopener noreferrer"
+                className="text-blue-600 hover:text-blue-700"
+              >
+                {user.profileGitHubUri}
+              </a>
+            </p>
+          ) : null}
+          {user.profileHomepageUri ? (
+            <p>
+              Homepage:{" "}
+              <a
+                href={user.profileHomepageUri}
+                target="_blank"
+                rel="noopener noreferrer"
+                className="text-blue-600 hover:text-blue-700"
+              >
+                {user.profileHomepageUri}
+              </a>
+            </p>
+          ) : null}
+          {user.profileLinkedInUri ? (
+            <p>
+              LinkedIn:{" "}
+              <a
+                href={user.profileLinkedInUri}
+                target="_blank"
+                rel="noopener noreferrer"
+                className="text-blue-600 hover:text-blue-700"
+              >
+                {user.profileLinkedInUri}
+              </a>
+            </p>
+          ) : null}
+        </div>
+
+        {user.userChecklists.length > 0 ? (
+          <>
+            <h2 className="text-lg">Completed Checklists:</h2>
+            <ul>
+              {user.userChecklists.map((checklist: ChecklistType) => (
+                <li className="my-1" key={checklist.id}>
+                  <p>Checklist: {checklist.checklist.name}</p>
+                  <p>Version: {checklist.checklist.version}</p>
+                  <p>
+                    Completed at:{" "}
+                    {new Date(checklist.updatedAt).toLocaleString()}
+                  </p>
+                </li>
+              ))}
+            </ul>
+          </>
+        ) : (
+          <p>This user has not completed any checklists.</p>
+        )}
+      </main>
+    );
+  } catch (error) {
+    if (error instanceof TRPCError) {
+      if (error.code === "UNAUTHORIZED") {
+        return <div>This profile is private.</div>;
+      }
+      if (error.code === "NOT_FOUND") {
+        return <div>User not found.</div>;
+      }
+    }
+    return <div>An error occurred while loading this profile.</div>;
+  }
+}
+
+export default async function ShowUserPage({
+  params,
+}: {
+  params: { userId: string };
+}) {
+  const userId = parseInt(params.userId);
+
+  return (
+    <LadderlyPageWrapper>
+      <LargeCard>
+        <Suspense fallback={<div>Loading...</div>}>
+          <UserProfile userId={userId} />
+        </Suspense>
+      </LargeCard>
+    </LadderlyPageWrapper>
+  );
+}
diff --git a/ladderly-io/src/app/users/queries/getUser.ts b/ladderly-io/src/app/users/queries/getUser.ts
deleted file mode 100644
index 81b45f86..00000000
--- a/ladderly-io/src/app/users/queries/getUser.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-// TODO: migrate to trpc
-
-import { Ctx } from '@blitzjs/next'
-import { resolver } from '@blitzjs/rpc'
-import { AuthorizationError, NotFoundError } from 'blitz'
-import db from 'db'
-
-export default resolver.pipe(async ({ id }: { id: number }, ctx: Ctx) => {
-  if (id !== parseInt(id.toString())) throw new NotFoundError()
-
-  const isOwnData = ctx.session.userId === id
-  const user = await db.user.findUnique({
-    where: { id },
-    select: {
-      id: true,
-      uuid: true,
-      nameFirst: true,
-      nameLast: true,
-      hasPublicProfileEnabled: true,
-      hasShoutOutsEnabled: true,
-      hasOpenToWork: true,
-      profileBlurb: true,
-      profileContactEmail: true,
-      profileGitHubUri: true,
-      profileHomepageUri: true,
-      profileLinkedInUri: true,
-      userChecklists: {
-        where: { isComplete: true },
-        take: 3,
-        orderBy: { createdAt: 'desc' },
-        select: {
-          id: true,
-          checklist: true,
-          createdAt: true,
-          isComplete: true,
-          updatedAt: true,
-        },
-      },
-    },
-  })
-
-  if (!user) throw new NotFoundError('User not found')
-  if (isOwnData || user.hasPublicProfileEnabled) {
-    return user
-  }
-
-  throw new AuthorizationError(
-    'You do not have permission to view this user data.'
-  )
-})
diff --git a/ladderly-io/src/app/users/queries/getUsers.ts b/ladderly-io/src/app/users/queries/getUsers.ts
deleted file mode 100644
index c51df881..00000000
--- a/ladderly-io/src/app/users/queries/getUsers.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// TODO: migrate to trpc
-
-import { paginate } from 'blitz'
-import { resolver } from '@blitzjs/rpc'
-import db, { Prisma } from 'db'
-
-interface GetUsersInput
-  extends Pick<
-    Prisma.UserFindManyArgs,
-    'where' | 'orderBy' | 'skip' | 'take'
-  > {}
-
-export default resolver.pipe(
-  async ({ where, orderBy, skip = 0, take = 100 }: GetUsersInput) => {
-    const {
-      items: users,
-      hasMore,
-      nextPage,
-      count,
-    } = await paginate({
-      skip,
-      take,
-      count: () => db.user.count({ where }),
-      query: (paginateArgs) =>
-        db.user.findMany({
-          ...paginateArgs,
-          where: {
-            ...where,
-            hasPublicProfileEnabled: true,
-          },
-          orderBy,
-          select: {
-            id: true,
-            uuid: true,
-            createdAt: true,
-            nameFirst: true,
-            nameLast: true,
-            hasPublicProfileEnabled: true,
-            hasShoutOutsEnabled: true,
-            hasOpenToWork: true,
-            profileBlurb: true,
-            profileContactEmail: true,
-            profileGitHubUri: true,
-            profileHomepageUri: true,
-            profileLinkedInUri: true,
-            totalContributions: true,
-          },
-        }),
-    })
-
-    return {
-      users,
-      nextPage,
-      hasMore,
-      count,
-    }
-  }
-)
diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index e0876eb6..47ca8c96 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -5,6 +5,7 @@ import {
 } from "~/server/api/trpc";
 import { PaymentTierEnum } from "@prisma/client";
 import { z } from "zod";
+import { TRPCError } from '@trpc/server';
 
 const tiersOrder = {
   FREE: 0,
@@ -118,4 +119,64 @@ export const userRouter = createTRPCRouter({
         hasMore,
       };
     }),
+
+  getUser: publicProcedure
+    .input(z.object({ id: z.number() }))
+    .query(async ({ ctx, input }) => {
+      // Validate ID is an integer
+      if (input.id !== parseInt(input.id.toString())) {
+        throw new TRPCError({ 
+          code: 'NOT_FOUND',
+          message: 'User not found'
+        });
+      }
+
+      const isOwnData = ctx.session?.user?.id === input.id.toString();
+
+      const user = await ctx.db.user.findUnique({
+        where: { id: input.id },
+        select: {
+          id: true,
+          uuid: true,
+          nameFirst: true,
+          nameLast: true,
+          hasPublicProfileEnabled: true,
+          hasShoutOutsEnabled: true,
+          hasOpenToWork: true,
+          profileBlurb: true,
+          profileContactEmail: true,
+          profileGitHubUri: true,
+          profileHomepageUri: true,
+          profileLinkedInUri: true,
+          userChecklists: {
+            where: { isComplete: true },
+            take: 3,
+            orderBy: { createdAt: 'desc' },
+            select: {
+              id: true,
+              checklist: true,
+              createdAt: true,
+              isComplete: true,
+              updatedAt: true,
+            },
+          },
+        },
+      });
+
+      if (!user) {
+        throw new TRPCError({ 
+          code: 'NOT_FOUND',
+          message: 'User not found'
+        });
+      }
+
+      if (!isOwnData && !user.hasPublicProfileEnabled) {
+        throw new TRPCError({ 
+          code: 'UNAUTHORIZED',
+          message: 'You do not have permission to view this user data.'
+        });
+      }
+
+      return user;
+    }),
 });

From 865060950d98d359cb92913cdb5effe443c4d1fb Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 16:50:05 -0500
Subject: [PATCH 04/31] chore: install react-final-form

---
 ladderly-io/package-lock.json | 35 +++++++++++++++++++++++++++++++++++
 ladderly-io/package.json      |  1 +
 2 files changed, 36 insertions(+)

diff --git a/ladderly-io/package-lock.json b/ladderly-io/package-lock.json
index a1b0ba0a..54380379 100644
--- a/ladderly-io/package-lock.json
+++ b/ladderly-io/package-lock.json
@@ -22,6 +22,7 @@
         "next-auth": "^4.24.7",
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
+        "react-final-form": "^6.5.9",
         "server-only": "^0.0.1",
         "superjson": "^2.2.1",
         "zod": "^3.23.3"
@@ -2787,6 +2788,23 @@
         "node": ">=8"
       }
     },
+    "node_modules/final-form": {
+      "version": "4.20.10",
+      "resolved": "https://registry.npmjs.org/final-form/-/final-form-4.20.10.tgz",
+      "integrity": "sha512-TL48Pi1oNHeMOHrKv1bCJUrWZDcD3DIG6AGYVNOnyZPr7Bd/pStN0pL+lfzF5BNoj/FclaoiaLenk4XUIFVYng==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@babel/runtime": "^7.10.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/final-form"
+      }
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -4895,6 +4913,23 @@
         "react": "^18.3.1"
       }
     },
+    "node_modules/react-final-form": {
+      "version": "6.5.9",
+      "resolved": "https://registry.npmjs.org/react-final-form/-/react-final-form-6.5.9.tgz",
+      "integrity": "sha512-x3XYvozolECp3nIjly+4QqxdjSSWfcnpGEL5K8OBT6xmGrq5kBqbA6+/tOqoom9NwqIPPbxPNsOViFlbKgowbA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.15.4"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/final-form"
+      },
+      "peerDependencies": {
+        "final-form": "^4.20.4",
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
     "node_modules/react-is": {
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/ladderly-io/package.json b/ladderly-io/package.json
index 9bb848e1..22db408a 100644
--- a/ladderly-io/package.json
+++ b/ladderly-io/package.json
@@ -28,6 +28,7 @@
     "next-auth": "^4.24.7",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
+    "react-final-form": "^6.5.9",
     "server-only": "^0.0.1",
     "superjson": "^2.2.1",
     "zod": "^3.23.3"

From 0f15bd7dcb083532cd6ee9cd397e9c865b8761e9 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 16:50:41 -0500
Subject: [PATCH 05/31] feat: SettingsForm

---
 .../settings/components/CountryDropdown.tsx   | 171 ++++++++++++++++++
 .../app/settings/components/SettingsForm.tsx  | 135 ++++++++++++++
 ladderly-io/src/app/settings/schemas.ts       |  61 +++++++
 3 files changed, 367 insertions(+)
 create mode 100644 ladderly-io/src/app/settings/components/CountryDropdown.tsx
 create mode 100644 ladderly-io/src/app/settings/components/SettingsForm.tsx
 create mode 100644 ladderly-io/src/app/settings/schemas.ts

diff --git a/ladderly-io/src/app/settings/components/CountryDropdown.tsx b/ladderly-io/src/app/settings/components/CountryDropdown.tsx
new file mode 100644
index 00000000..8e3f45fa
--- /dev/null
+++ b/ladderly-io/src/app/settings/components/CountryDropdown.tsx
@@ -0,0 +1,171 @@
+import { PropsWithoutRef } from "react";
+import LabeledAutocompleteField from "~/app/core/components/LabeledAutocompleteField";
+
+const countries = [
+  { value: "", label: "Select a Country" },
+  { value: "Afghanistan", label: "Afghanistan" },
+  { value: "Albania", label: "Albania" },
+  { value: "Algeria", label: "Algeria" },
+  { value: "Angola", label: "Angola" },
+  { value: "Argentina", label: "Argentina" },
+  { value: "Ascension Island", label: "Ascension Island" },
+  { value: "Australia", label: "Australia" },
+  { value: "Austria", label: "Austria" },
+  { value: "Bahrain", label: "Bahrain" },
+  { value: "Bangladesh", label: "Bangladesh" },
+  { value: "Belarus", label: "Belarus" },
+  { value: "Belgium", label: "Belgium" },
+  { value: "Belize", label: "Belize" },
+  { value: "Benin", label: "Benin" },
+  { value: "Bhutan", label: "Bhutan" },
+  { value: "Bolivia", label: "Bolivia" },
+  { value: "Bosnia and Herzegovina", label: "Bosnia and Herzegovina" },
+  { value: "Botswana", label: "Botswana" },
+  { value: "Brazil", label: "Brazil" },
+  { value: "Bulgaria", label: "Bulgaria" },
+  { value: "Burundi", label: "Burundi" },
+  { value: "Cameroon", label: "Cameroon" },
+  { value: "Canada", label: "Canada" },
+  { value: "Cape Verde", label: "Cape Verde" },
+  { value: "Central African Republic", label: "Central African Republic" },
+  { value: "Chad", label: "Chad" },
+  { value: "Chile", label: "Chile" },
+  { value: "China", label: "China" },
+  { value: "Colombia", label: "Colombia" },
+  { value: "Comoros", label: "Comoros" },
+  { value: "Congo", label: "Congo" },
+  { value: "Costa Rica", label: "Costa Rica" },
+  { value: "Croatia", label: "Croatia" },
+  { value: "Cyprus", label: "Cyprus" },
+  { value: "Czech Republic", label: "Czech Republic" },
+  {
+    value: "Democratic Republic of the Congo",
+    label: "Democratic Republic of the Congo",
+  },
+  { value: "Denmark", label: "Denmark" },
+  { value: "Djibouti", label: "Djibouti" },
+  { value: "Ecuador", label: "Ecuador" },
+  { value: "Egypt", label: "Egypt" },
+  { value: "El Salvador", label: "El Salvador" },
+  { value: "Equatorial Guinea", label: "Equatorial Guinea" },
+  { value: "Eritrea", label: "Eritrea" },
+  { value: "Estonia", label: "Estonia" },
+  { value: "Eswatini", label: "Eswatini" },
+  { value: "Ethiopia", label: "Ethiopia" },
+  { value: "Falkland Islands", label: "Falkland Islands" },
+  { value: "Faroe Islands", label: "Faroe Islands" },
+  { value: "Finland", label: "Finland" },
+  { value: "France", label: "France" },
+  { value: "French Guiana", label: "French Guiana" },
+  { value: "Gabon", label: "Gabon" },
+  { value: "Germany", label: "Germany" },
+  { value: "Ghana", label: "Ghana" },
+  { value: "Greece", label: "Greece" },
+  { value: "Greenland", label: "Greenland" },
+  { value: "Guatemala", label: "Guatemala" },
+  { value: "Guinea-Bissau", label: "Guinea-Bissau" },
+  { value: "Guinea", label: "Guinea" },
+  { value: "Guyana", label: "Guyana" },
+  { value: "Honduras", label: "Honduras" },
+  { value: "Hungary", label: "Hungary" },
+  { value: "Iceland", label: "Iceland" },
+  { value: "India", label: "India" },
+  { value: "Iran", label: "Iran" },
+  { value: "Iraq", label: "Iraq" },
+  { value: "Ireland", label: "Ireland" },
+  { value: "Israel", label: "Israel" },
+  { value: "Italy", label: "Italy" },
+  { value: "Ivory Coast", label: "Ivory Coast" },
+  { value: "Japan", label: "Japan" },
+  { value: "Jordan", label: "Jordan" },
+  { value: "Kenya", label: "Kenya" },
+  { value: "Kosovo", label: "Kosovo" },
+  { value: "Kuwait", label: "Kuwait" },
+  { value: "Latvia", label: "Latvia" },
+  { value: "Lebanon", label: "Lebanon" },
+  { value: "Lesotho", label: "Lesotho" },
+  { value: "Liberia", label: "Liberia" },
+  { value: "Libya", label: "Libya" },
+  { value: "Lithuania", label: "Lithuania" },
+  { value: "Luxembourg", label: "Luxembourg" },
+  { value: "Madagascar", label: "Madagascar" },
+  { value: "Malawi", label: "Malawi" },
+  { value: "Mali", label: "Mali" },
+  { value: "Malta", label: "Malta" },
+  { value: "Mauritania", label: "Mauritania" },
+  { value: "Mauritius", label: "Mauritius" },
+  { value: "Mexico", label: "Mexico" },
+  { value: "Moldova", label: "Moldova" },
+  { value: "Montenegro", label: "Montenegro" },
+  { value: "Morocco", label: "Morocco" },
+  { value: "Mozambique", label: "Mozambique" },
+  { value: "Namibia", label: "Namibia" },
+  { value: "Nepal", label: "Nepal" },
+  { value: "Netherlands", label: "Netherlands" },
+  { value: "New Zealand", label: "New Zealand" },
+  { value: "Nicaragua", label: "Nicaragua" },
+  { value: "Niger", label: "Niger" },
+  { value: "Nigeria", label: "Nigeria" },
+  { value: "North Macedonia", label: "North Macedonia" },
+  { value: "Norway", label: "Norway" },
+  { value: "Oman", label: "Oman" },
+  { value: "Pakistan", label: "Pakistan" },
+  { value: "Panama", label: "Panama" },
+  { value: "Paraguay", label: "Paraguay" },
+  { value: "Peru", label: "Peru" },
+  { value: "Poland", label: "Poland" },
+  { value: "Portugal", label: "Portugal" },
+  { value: "Qatar", label: "Qatar" },
+  { value: "Romania", label: "Romania" },
+  { value: "Russia", label: "Russia" },
+  { value: "Rwanda", label: "Rwanda" },
+  { value: "Saint Helena", label: "Saint Helena" },
+  { value: "Saudi Arabia", label: "Saudi Arabia" },
+  { value: "Senegal", label: "Senegal" },
+  { value: "Serbia", label: "Serbia" },
+  { value: "Seychelles", label: "Seychelles" },
+  { value: "Sierra Leone", label: "Sierra Leone" },
+  { value: "Slovakia", label: "Slovakia" },
+  { value: "Slovenia", label: "Slovenia" },
+  { value: "Somalia", label: "Somalia" },
+  { value: "South Africa", label: "South Africa" },
+  { value: "South Korea", label: "South Korea" },
+  { value: "South Sudan", label: "South Sudan" },
+  { value: "Spain", label: "Spain" },
+  { value: "Sri Lanka", label: "Sri Lanka" },
+  { value: "Sudan", label: "Sudan" },
+  { value: "Suriname", label: "Suriname" },
+  { value: "Sweden", label: "Sweden" },
+  { value: "Switzerland", label: "Switzerland" },
+  { value: "Syria", label: "Syria" },
+  { value: "Tanzania", label: "Tanzania" },
+  { value: "The Gambia", label: "The Gambia" },
+  { value: "Togo", label: "Togo" },
+  { value: "Tristan da Cunha", label: "Tristan da Cunha" },
+  { value: "Tunisia", label: "Tunisia" },
+  { value: "Turkey", label: "Turkey" },
+  { value: "Uganda", label: "Uganda" },
+  { value: "Ukraine", label: "Ukraine" },
+  { value: "United Arab Emirates", label: "United Arab Emirates" },
+  { value: "United Kingdom", label: "United Kingdom" },
+  { value: "United States", label: "United States" },
+  { value: "Uruguay", label: "Uruguay" },
+  { value: "Venezuela", label: "Venezuela" },
+  { value: "Western Sahara", label: "Western Sahara" },
+  { value: "Yemen", label: "Yemen" },
+  { value: "Zambia", label: "Zambia" },
+  { value: "Zimbabwe", label: "Zimbabwe" },
+];
+
+export const CountryDropdown = ({
+  outerProps,
+}: {
+  outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>;
+}) => (
+  <LabeledAutocompleteField
+    name="residenceCountry"
+    label="Country of Residence"
+    options={countries}
+    outerProps={outerProps}
+  />
+);
diff --git a/ladderly-io/src/app/settings/components/SettingsForm.tsx b/ladderly-io/src/app/settings/components/SettingsForm.tsx
new file mode 100644
index 00000000..4243f443
--- /dev/null
+++ b/ladderly-io/src/app/settings/components/SettingsForm.tsx
@@ -0,0 +1,135 @@
+"use client";
+
+import React from "react";
+import { UpdateSettingsSchema } from "src/app/settings/schemas";
+import { Form, FormProps } from "~/app/core/components/Form";
+import LabeledCheckboxField from "~/app/core/components/LabeledCheckboxField";
+import LabeledTextField from "~/app/core/components/LabeledTextField";
+import { z } from "zod";
+import { CountryDropdown } from "./CountryDropdown";
+import { USStateDropdown } from "./USStateDropdown";
+
+export { FORM_ERROR } from "~/app/core/components/Form";
+
+type SettingsFormProps = {
+  initialValues: z.infer<typeof UpdateSettingsSchema>;
+  onSubmit: FormProps<typeof UpdateSettingsSchema>["onSubmit"];
+};
+
+export function SettingsForm({ initialValues, onSubmit }: SettingsFormProps) {
+  return (
+    <Form
+      schema={UpdateSettingsSchema}
+      initialValues={initialValues}
+      onSubmit={onSubmit}
+    >
+      <section>
+        <h3 className="mt-8 text-xl">Authentication Data</h3>
+        <LabeledTextField
+          name="email"
+          label="Primary Email"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Primary Email"
+        />
+        <LabeledTextField
+          name="emailBackup"
+          label="Backup Email"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Backup Email"
+        />
+        <LabeledTextField
+          name="emailStripe"
+          label="Stripe Email"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Stripe Email"
+        />
+      </section>
+      <section>
+        <h3 className="mt-8 text-xl">Public Profile</h3>
+        <LabeledTextField
+          name="nameFirst"
+          label="First Name"
+          outerProps={{ className: "mt-2" }}
+          placeholder="First Name"
+        />
+        <LabeledTextField
+          name="nameLast"
+          label="Last Name"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Last Name"
+        />
+
+        <LabeledCheckboxField name="hasOpenToWork" label="Open To Work" />
+        <LabeledCheckboxField
+          label="Enable Shout Outs"
+          name="hasShoutOutsEnabled"
+        />
+
+        <LabeledTextField
+          name="profileBlurb"
+          label="Profile Blurb"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Profile Blurb"
+        />
+        <LabeledTextField
+          name="profileContactEmail"
+          label="Public Contact Email"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Public Contact Email"
+        />
+        <LabeledTextField
+          name="profileGitHubUri"
+          label="GitHub URL"
+          outerProps={{ className: "mt-2" }}
+          placeholder="GitHub URL"
+        />
+        <LabeledTextField
+          name="profileHomepageUri"
+          label="Homepage URL"
+          outerProps={{ className: "mt-2" }}
+          placeholder="Homepage URL"
+        />
+        <LabeledTextField
+          name="profileLinkedInUri"
+          label="LinkedIn URL"
+          outerProps={{ className: "mt-2" }}
+          placeholder="LinkedIn URL"
+        />
+
+        <CountryDropdown outerProps={{ className: "mt-2 items-baseline" }} />
+        <USStateDropdown outerProps={{ className: "mt-2 items-baseline" }} />
+      </section>
+      <section>
+        <h3 className="mt-8 text-xl">Features and Interests</h3>
+        <LabeledCheckboxField
+          label="Enable Public Profile"
+          name="hasPublicProfileEnabled"
+          outerProps={{ className: "mt-2 items-baseline" }}
+        />
+        <LabeledCheckboxField
+          name="hasSmallGroupInterest"
+          label="Interested in an Expert-Led Small Group"
+        />
+        <LabeledCheckboxField
+          name="hasLiveStreamInterest"
+          label="Interested in Joining a Live Stream"
+        />
+        <LabeledCheckboxField
+          name="hasOnlineEventInterest"
+          label="Interested in Online Hackathons and Events"
+        />
+        <LabeledCheckboxField
+          name="hasInPersonEventInterest"
+          label="Interested in In-Person Hackathons and Events"
+        />
+      </section>
+
+      <button
+        type="submit"
+        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
+      >
+        Submit
+      </button>
+    </Form>
+  );
+}
diff --git a/ladderly-io/src/app/settings/schemas.ts b/ladderly-io/src/app/settings/schemas.ts
new file mode 100644
index 00000000..b68dc1ea
--- /dev/null
+++ b/ladderly-io/src/app/settings/schemas.ts
@@ -0,0 +1,61 @@
+import { z } from 'zod'
+
+const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/
+export const isValidOptionalEmail = (value: string) =>
+  value === '' || emailRegex.test(value)
+
+export const optionalEmailValidator = z
+  .string()
+  .refine(isValidOptionalEmail, {
+    message: 'Invalid email',
+  })
+  .nullable()
+  .optional()
+
+export const uriValidator = z
+  .string()
+  .refine((value) => value === '' || value.startsWith('http'), {
+    message: 'Invalid URI',
+  })
+  .nullable()
+  .optional()
+
+const optionalGitHubUriValidator = z
+  .string()
+  .refine((value) => value === '' || value.includes('github'), {
+    message: 'Invalid GitHub URL',
+  })
+  .nullable()
+  .optional()
+
+const optionalLinkedInUriValidator = z
+  .string()
+  .refine((value) => value === '' || value.includes('linkedin'), {
+    message: 'Invalid LinkedIn URL',
+  })
+  .nullable()
+  .optional()
+
+export const UpdateSettingsSchema = z.object({
+  email: z.string().email(),
+  emailBackup: optionalEmailValidator,
+  emailStripe: optionalEmailValidator,
+
+  hasInPersonEventInterest: z.boolean().optional(),
+  hasLiveStreamInterest: z.boolean().optional(),
+  hasOnlineEventInterest: z.boolean().optional(),
+  hasOpenToWork: z.boolean().optional(),
+  hasPublicProfileEnabled: z.boolean().optional(),
+  hasShoutOutsEnabled: z.boolean().optional(),
+  hasSmallGroupInterest: z.boolean().optional(),
+
+  nameFirst: z.string().nullable().optional(),
+  nameLast: z.string().nullable().optional(),
+  profileBlurb: z.string().nullable().optional(),
+  profileContactEmail: optionalEmailValidator,
+  profileGitHubUri: optionalGitHubUriValidator,
+  profileHomepageUri: uriValidator,
+  profileLinkedInUri: optionalLinkedInUriValidator,
+  residenceCountry: z.string().optional(),
+  residenceUSState: z.string().optional(),
+})

From fa4525014178e6a9bf8a04daa47229f3b2379bd0 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 16:55:30 -0500
Subject: [PATCH 06/31] feat: settings trpc api

---
 .../settings/components/USStateDropdown.tsx   |  76 ++++++++++
 ladderly-io/src/server/api/routers/user.ts    | 137 ++++++++++++++++++
 2 files changed, 213 insertions(+)
 create mode 100644 ladderly-io/src/app/settings/components/USStateDropdown.tsx

diff --git a/ladderly-io/src/app/settings/components/USStateDropdown.tsx b/ladderly-io/src/app/settings/components/USStateDropdown.tsx
new file mode 100644
index 00000000..1efb2ce8
--- /dev/null
+++ b/ladderly-io/src/app/settings/components/USStateDropdown.tsx
@@ -0,0 +1,76 @@
+import React, { PropsWithoutRef } from "react";
+import LabeledAutocompleteField from "~/app/core/components/LabeledAutocompleteField";
+
+const usStates = [
+  { value: "", label: "Select a U.S. State" },
+  { value: "Not U.S. Resident", label: "Not U.S. Resident" },
+  { value: "Alabama", label: "Alabama" },
+  { value: "Alaska", label: "Alaska" },
+  { value: "American Samoa", label: "American Samoa" },
+  { value: "Arizona", label: "Arizona" },
+  { value: "Arkansas", label: "Arkansas" },
+  { value: "California", label: "California" },
+  { value: "Colorado", label: "Colorado" },
+  { value: "Connecticut", label: "Connecticut" },
+  { value: "Delaware", label: "Delaware" },
+  { value: "District of Columbia", label: "District of Columbia" },
+  { value: "Florida", label: "Florida" },
+  { value: "Georgia", label: "Georgia" },
+  { value: "Guam", label: "Guam" },
+  { value: "Hawaii", label: "Hawaii" },
+  { value: "Idaho", label: "Idaho" },
+  { value: "Illinois", label: "Illinois" },
+  { value: "Indiana", label: "Indiana" },
+  { value: "Iowa", label: "Iowa" },
+  { value: "Kansas", label: "Kansas" },
+  { value: "Kentucky", label: "Kentucky" },
+  { value: "Louisiana", label: "Louisiana" },
+  { value: "Maine", label: "Maine" },
+  { value: "Maryland", label: "Maryland" },
+  { value: "Massachusetts", label: "Massachusetts" },
+  { value: "Michigan", label: "Michigan" },
+  { value: "Minnesota", label: "Minnesota" },
+  { value: "Mississippi", label: "Mississippi" },
+  { value: "Missouri", label: "Missouri" },
+  { value: "Montana", label: "Montana" },
+  { value: "Nebraska", label: "Nebraska" },
+  { value: "Nevada", label: "Nevada" },
+  { value: "New Hampshire", label: "New Hampshire" },
+  { value: "New Jersey", label: "New Jersey" },
+  { value: "New Mexico", label: "New Mexico" },
+  { value: "New York", label: "New York" },
+  { value: "North Carolina", label: "North Carolina" },
+  { value: "North Dakota", label: "North Dakota" },
+  { value: "Northern Mariana Islands", label: "Northern Mariana Islands" },
+  { value: "Ohio", label: "Ohio" },
+  { value: "Oklahoma", label: "Oklahoma" },
+  { value: "Oregon", label: "Oregon" },
+  { value: "Pennsylvania", label: "Pennsylvania" },
+  { value: "Puerto Rico", label: "Puerto Rico" },
+  { value: "Rhode Island", label: "Rhode Island" },
+  { value: "South Carolina", label: "South Carolina" },
+  { value: "South Dakota", label: "South Dakota" },
+  { value: "Tennessee", label: "Tennessee" },
+  { value: "Texas", label: "Texas" },
+  { value: "U.S. Virgin Islands", label: "U.S. Virgin Islands" },
+  { value: "Utah", label: "Utah" },
+  { value: "Vermont", label: "Vermont" },
+  { value: "Virginia", label: "Virginia" },
+  { value: "Washington", label: "Washington" },
+  { value: "West Virginia", label: "West Virginia" },
+  { value: "Wisconsin", label: "Wisconsin" },
+  { value: "Wyoming", label: "Wyoming" },
+];
+
+export const USStateDropdown = ({
+  outerProps,
+}: {
+  outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>;
+}) => (
+  <LabeledAutocompleteField
+    name="residenceState"
+    label="State of Residence"
+    options={usStates}
+    outerProps={outerProps}
+  />
+);
diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index 47ca8c96..fc661898 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -13,6 +13,28 @@ const tiersOrder = {
   PREMIUM: 2,
 } as const;
 
+const UpdateSettingsSchema = z.object({
+  email: z.string(),
+  emailBackup: z.string().optional(),
+  emailStripe: z.string().optional(),
+  nameFirst: z.string().optional(),
+  nameLast: z.string().optional(),
+  hasOpenToWork: z.boolean(),
+  hasShoutOutsEnabled: z.boolean(),
+  profileBlurb: z.string().nullable(),
+  profileContactEmail: z.string().nullable(),
+  profileGitHubUri: z.string().nullable(),
+  profileHomepageUri: z.string().nullable(),
+  profileLinkedInUri: z.string().nullable(),
+  residenceCountry: z.string(),
+  residenceUSState: z.string(),
+  hasPublicProfileEnabled: z.boolean(),
+  hasSmallGroupInterest: z.boolean(),
+  hasLiveStreamInterest: z.boolean(),
+  hasOnlineEventInterest: z.boolean(),
+  hasInPersonEventInterest: z.boolean(),
+});
+
 export const userRouter = createTRPCRouter({
   getCurrentUser: publicProcedure.query(async ({ ctx }) => {
     if (!ctx.session) {
@@ -177,6 +199,121 @@ export const userRouter = createTRPCRouter({
         });
       }
 
+      return user;
+    }),
+
+  getSettings: protectedProcedure.query(async ({ ctx }) => {
+    const id = parseInt(ctx.session.user.id);
+    
+    const result = await ctx.db.$transaction(async (tx) => {
+      const user = await tx.user.findUnique({
+        where: { id },
+        include: {
+          subscriptions: {
+            where: { type: 'ACCOUNT_PLAN' },
+            select: { tier: true, type: true },
+          },
+        },
+      });
+
+      if (!user) {
+        throw new TRPCError({ 
+          code: 'NOT_FOUND',
+          message: 'User not found'
+        });
+      }
+
+      let subscription = user.subscriptions[0];
+
+      if (!subscription) {
+        subscription = await tx.subscription.create({
+          data: {
+            userId: id,
+            tier: PaymentTierEnum.FREE,
+            type: 'ACCOUNT_PLAN',
+          },
+        });
+      }
+
+      return {
+        id: user.id,
+        email: user.email,
+        emailBackup: user.emailBackup,
+        emailStripe: user.emailStripe,
+        nameFirst: user.nameFirst,
+        nameLast: user.nameLast,
+        hasOpenToWork: user.hasOpenToWork,
+        hasShoutOutsEnabled: user.hasShoutOutsEnabled,
+        profileBlurb: user.profileBlurb,
+        profileContactEmail: user.profileContactEmail,
+        profileGitHubUri: user.profileGitHubUri,
+        profileHomepageUri: user.profileHomepageUri,
+        profileLinkedInUri: user.profileLinkedInUri,
+        residenceCountry: user.residenceCountry,
+        residenceUSState: user.residenceUSState,
+        hasPublicProfileEnabled: user.hasPublicProfileEnabled,
+        hasSmallGroupInterest: user.hasSmallGroupInterest,
+        hasLiveStreamInterest: user.hasLiveStreamInterest,
+        hasOnlineEventInterest: user.hasOnlineEventInterest,
+        hasInPersonEventInterest: user.hasInPersonEventInterest,
+        subscription: {
+          tier: subscription.tier,
+          type: subscription.type,
+        },
+      };
+    });
+
+    return result;
+  }),
+
+  updateSettings: protectedProcedure
+    .input(UpdateSettingsSchema)
+    .mutation(async ({ ctx, input }) => {
+      const userId = parseInt(ctx.session.user.id);
+      const email = input.email.toLowerCase().trim();
+      const emailBackup = input.emailBackup?.toLowerCase().trim() || '';
+      const emailStripe = input.emailStripe?.toLowerCase().trim() || '';
+
+      // Basic email validation
+      const isValidEmail = (email: string) => 
+        email === '' || (email.includes('@') && email.includes('.'));
+
+      if (
+        !isValidEmail(email) ||
+        !isValidEmail(emailBackup) ||
+        !isValidEmail(emailStripe)
+      ) {
+        throw new TRPCError({
+          code: 'BAD_REQUEST',
+          message: 'Invalid email format'
+        });
+      }
+
+      const user = await ctx.db.user.update({
+        where: { id: userId },
+        data: {
+          email,
+          emailBackup,
+          emailStripe,
+          hasInPersonEventInterest: input.hasInPersonEventInterest,
+          hasOnlineEventInterest: input.hasOnlineEventInterest,
+          hasLiveStreamInterest: input.hasLiveStreamInterest,
+          hasOpenToWork: input.hasOpenToWork,
+          hasPublicProfileEnabled: input.hasPublicProfileEnabled,
+          hasShoutOutsEnabled: input.hasShoutOutsEnabled,
+          hasSmallGroupInterest: input.hasSmallGroupInterest,
+          nameFirst: input.nameFirst?.trim() || '',
+          nameLast: input.nameLast?.trim() || '',
+          profileBlurb: input.profileBlurb?.trim() || null,
+          profileContactEmail: input.profileContactEmail?.toLowerCase().trim() || null,
+          profileGitHubUri: input.profileGitHubUri?.trim() || null,
+          profileHomepageUri: input.profileHomepageUri?.trim() || null,
+          profileLinkedInUri: input.profileLinkedInUri?.trim() || null,
+          residenceCountry: input.residenceCountry?.trim() || '',
+          residenceUSState: input.residenceUSState?.trim() || '',
+        },
+      });
+
       return user;
     }),
 });

From 687d21a2dca83fb6baead59c6dc16facdb8feda7 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 17:03:08 -0500
Subject: [PATCH 07/31] feat: form doesn't use blitz

---
 ladderly-io/src/app/core/components/Form.tsx | 47 +++++++++++++-------
 1 file changed, 30 insertions(+), 17 deletions(-)

diff --git a/ladderly-io/src/app/core/components/Form.tsx b/ladderly-io/src/app/core/components/Form.tsx
index 6101f800..a7fa5a06 100644
--- a/ladderly-io/src/app/core/components/Form.tsx
+++ b/ladderly-io/src/app/core/components/Form.tsx
@@ -1,23 +1,37 @@
-import { ReactNode, PropsWithoutRef } from 'react'
+import { ReactNode, PropsWithoutRef } from "react";
 import {
   Form as FinalForm,
   FormProps as FinalFormProps,
-} from 'react-final-form'
-import { z } from 'zod'
-import { validateZodSchema } from 'blitz'
-export { FORM_ERROR } from 'final-form'
+} from "react-final-form";
+import { z } from "zod";
+import { FORM_ERROR } from "final-form";
 
 export interface FormProps<S extends z.ZodType<any, any>>
-  extends Omit<PropsWithoutRef<JSX.IntrinsicElements['form']>, 'onSubmit'> {
-  /** All your form fields */
-  children?: ReactNode
-  /** Text to display in the submit button */
-  submitText?: string
-  schema?: S
-  onSubmit: FinalFormProps<z.infer<S>>['onSubmit']
-  initialValues?: FinalFormProps<z.infer<S>>['initialValues']
+  extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
+  children?: ReactNode;
+  submitText?: string;
+  schema?: S;
+  onSubmit: FinalFormProps<z.infer<S>>["onSubmit"];
+  initialValues?: FinalFormProps<z.infer<S>>["initialValues"];
 }
 
+export const validateZodSchema = <T extends z.ZodType<any, any>>(
+  schema: T | undefined
+) => {
+  return (values: z.infer<T>) => {
+    if (!schema) return {};
+    try {
+      schema.parse(values);
+      return {};
+    } catch (error) {
+      if (error instanceof z.ZodError) {
+        return error.formErrors.fieldErrors;
+      }
+      return { [FORM_ERROR]: "Form validation failed" };
+    }
+  };
+};
+
 export function Form<S extends z.ZodType<any, any>>({
   children,
   submitText,
@@ -33,11 +47,10 @@ export function Form<S extends z.ZodType<any, any>>({
       onSubmit={onSubmit}
       render={({ handleSubmit, submitting, submitError }) => (
         <form onSubmit={handleSubmit} className="form" {...props}>
-          {/* Form fields supplied as children are rendered here */}
           {children}
 
           {submitError && (
-            <div role="alert" style={{ color: 'red' }}>
+            <div role="alert" style={{ color: "red" }}>
               {submitError}
             </div>
           )}
@@ -54,7 +67,7 @@ export function Form<S extends z.ZodType<any, any>>({
         </form>
       )}
     />
-  )
+  );
 }
 
-export default Form
+export { FORM_ERROR };

From bca9d8ac97828984594576a1576b7ebd31e191a0 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 17:03:28 -0500
Subject: [PATCH 08/31] feat: broken wip settings page migrate

---
 .../components/SettingsFormWrapper.tsx        | 61 +++++++++++++++++++
 ladderly-io/src/app/settings/page.tsx         | 45 ++++++++++++++
 2 files changed, 106 insertions(+)
 create mode 100644 ladderly-io/src/app/settings/components/SettingsFormWrapper.tsx
 create mode 100644 ladderly-io/src/app/settings/page.tsx

diff --git a/ladderly-io/src/app/settings/components/SettingsFormWrapper.tsx b/ladderly-io/src/app/settings/components/SettingsFormWrapper.tsx
new file mode 100644
index 00000000..ad4e00cb
--- /dev/null
+++ b/ladderly-io/src/app/settings/components/SettingsFormWrapper.tsx
@@ -0,0 +1,61 @@
+// app/settings/SettingsForm.tsx
+
+"use client";
+
+import { useState } from "react";
+import { api } from "~/trpc/react";
+import { SettingsForm } from "./SettingsForm";
+
+// Define the settings type based on the schema
+type UserSettings = {
+  id: number;
+  email: string;
+  emailBackup: string;
+  emailStripe: string;
+  nameFirst: string;
+  nameLast: string;
+  hasOpenToWork: boolean;
+  hasShoutOutsEnabled: boolean;
+  profileBlurb: string | null;
+  profileContactEmail: string | null;
+  profileGitHubUri: string | null;
+  profileHomepageUri: string | null;
+  profileLinkedInUri: string | null;
+  residenceCountry: string;
+  residenceUSState: string;
+  hasPublicProfileEnabled: boolean;
+  hasSmallGroupInterest: boolean;
+  hasLiveStreamInterest: boolean;
+  hasOnlineEventInterest: boolean;
+  hasInPersonEventInterest: boolean;
+  subscription: {
+    tier: string;
+    type: string;
+  };
+};
+
+interface SettingsFormWrapperProps {
+  initialSettings: UserSettings;
+}
+
+export function SettingsFormWrapper({
+  initialSettings,
+}: SettingsFormWrapperProps) {
+  const [settings, setSettings] = useState<UserSettings>(initialSettings);
+  const { mutate: updateSettings } = api.user.updateSettings.useMutation({
+    onSuccess: (updatedSettings) => {
+      setSettings((prev: UserSettings) => ({ ...prev, ...updatedSettings }));
+      alert("Updated successfully.");
+    },
+    onError: (error) => {
+      console.error("Failed to update settings:", error);
+      alert("Update failed: " + error.message);
+    },
+  });
+
+  const handleSubmit = async (values: UserSettings) => {
+    updateSettings(values);
+  };
+
+  return <SettingsForm initialValues={settings} onSubmit={handleSubmit} />;
+}
diff --git a/ladderly-io/src/app/settings/page.tsx b/ladderly-io/src/app/settings/page.tsx
new file mode 100644
index 00000000..021e4353
--- /dev/null
+++ b/ladderly-io/src/app/settings/page.tsx
@@ -0,0 +1,45 @@
+import Link from "next/link";
+import { Suspense } from "react";
+import { api } from "~/trpc/server";
+import { LargeCard } from "~/app/core/components/LargeCard";
+import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
+import { SettingsFormWrapper } from "./components/SettingsFormWrapper";
+import { redirect } from "next/navigation";
+
+export const metadata = {
+  title: "Settings",
+};
+
+export default async function SettingsPage() {
+  try {
+    const settings = await api.user.getSettings();
+
+    return (
+      <LadderlyPageWrapper>
+        <div className="flex items-center justify-center">
+          <LargeCard>
+            <h1 className="text-2xl font-bold text-gray-800">
+              Edit User Settings
+            </h1>
+            <p className="mt-4">
+              Please email john@ladderly.io to update your subscription tier.
+            </p>
+
+            <Link
+              className="mt-4 block text-ladderly-violet-700 underline"
+              href={`/blog/2024-02-16-user-settings` as any}
+            >
+              Learn More About User Settings
+            </Link>
+
+            <Suspense fallback={<div>Loading form...</div>}>
+              <SettingsFormWrapper initialSettings={settings} />
+            </Suspense>
+          </LargeCard>
+        </div>
+      </LadderlyPageWrapper>
+    );
+  } catch (error) {
+    redirect("/");
+  }
+}

From eae5349da6408573ee5b83528bbbc83e5b939908 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 18:41:35 -0500
Subject: [PATCH 09/31] chore: install react-select

and npm audit fix
---
 ladderly-io/package-lock.json | 645 ++++++++++++++++++++++++++++++----
 ladderly-io/package.json      |   1 +
 2 files changed, 575 insertions(+), 71 deletions(-)

diff --git a/ladderly-io/package-lock.json b/ladderly-io/package-lock.json
index 54380379..d8be6657 100644
--- a/ladderly-io/package-lock.json
+++ b/ladderly-io/package-lock.json
@@ -23,6 +23,7 @@
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-final-form": "^6.5.9",
+        "react-select": "^5.8.3",
         "server-only": "^0.0.1",
         "superjson": "^2.2.1",
         "zod": "^3.23.3"
@@ -57,16 +58,18 @@
       }
     },
     "node_modules/@auth/core": {
-      "version": "0.29.0",
-      "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.29.0.tgz",
-      "integrity": "sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==",
+      "version": "0.34.2",
+      "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz",
+      "integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==",
       "license": "ISC",
+      "optional": true,
+      "peer": true,
       "dependencies": {
         "@panva/hkdf": "^1.1.1",
         "@types/cookie": "0.6.0",
         "cookie": "0.6.0",
         "jose": "^5.1.3",
-        "oauth4webapi": "^2.4.0",
+        "oauth4webapi": "^2.10.4",
         "preact": "10.11.3",
         "preact-render-to-string": "5.2.3"
       },
@@ -99,6 +102,113 @@
         "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5"
       }
     },
+    "node_modules/@auth/prisma-adapter/node_modules/@auth/core": {
+      "version": "0.29.0",
+      "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.29.0.tgz",
+      "integrity": "sha512-MdfEjU6WRjUnPG1+XeBWrTIlAsLZU6V0imCIqVDDDPxLI6UZWldXVqAA2EsDazGofV78jqiCLHaN85mJITDqdg==",
+      "license": "ISC",
+      "dependencies": {
+        "@panva/hkdf": "^1.1.1",
+        "@types/cookie": "0.6.0",
+        "cookie": "0.6.0",
+        "jose": "^5.1.3",
+        "oauth4webapi": "^2.4.0",
+        "preact": "10.11.3",
+        "preact-render-to-string": "5.2.3"
+      },
+      "peerDependencies": {
+        "@simplewebauthn/browser": "^9.0.1",
+        "@simplewebauthn/server": "^9.0.2",
+        "nodemailer": "^6.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@simplewebauthn/browser": {
+          "optional": true
+        },
+        "@simplewebauthn/server": {
+          "optional": true
+        },
+        "nodemailer": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
+      "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.25.9",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz",
+      "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.26.2",
+        "@babel/types": "^7.26.0",
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+      "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.25.9",
+        "@babel/types": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.26.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
     "node_modules/@babel/runtime": {
       "version": "7.25.0",
       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
@@ -111,6 +221,174 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/template": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+      "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.25.9",
+        "@babel/parser": "^7.25.9",
+        "@babel/types": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
+      "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.25.9",
+        "@babel/generator": "^7.25.9",
+        "@babel/parser": "^7.25.9",
+        "@babel/template": "^7.25.9",
+        "@babel/types": "^7.25.9",
+        "debug": "^4.3.1",
+        "globals": "^11.1.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse/node_modules/globals": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+      "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@emotion/babel-plugin": {
+      "version": "11.12.0",
+      "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz",
+      "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.16.7",
+        "@babel/runtime": "^7.18.3",
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/serialize": "^1.2.0",
+        "babel-plugin-macros": "^3.1.0",
+        "convert-source-map": "^1.5.0",
+        "escape-string-regexp": "^4.0.0",
+        "find-root": "^1.1.0",
+        "source-map": "^0.5.7",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/cache": {
+      "version": "11.13.1",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
+      "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
+      "license": "MIT",
+      "dependencies": {
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/sheet": "^1.4.0",
+        "@emotion/utils": "^1.4.0",
+        "@emotion/weak-memoize": "^0.4.0",
+        "stylis": "4.2.0"
+      }
+    },
+    "node_modules/@emotion/hash": {
+      "version": "0.9.2",
+      "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+      "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/memoize": {
+      "version": "0.9.0",
+      "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+      "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/react": {
+      "version": "11.13.3",
+      "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
+      "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@emotion/babel-plugin": "^11.12.0",
+        "@emotion/cache": "^11.13.0",
+        "@emotion/serialize": "^1.3.1",
+        "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
+        "@emotion/utils": "^1.4.0",
+        "@emotion/weak-memoize": "^0.4.0",
+        "hoist-non-react-statics": "^3.3.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@emotion/serialize": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.2.tgz",
+      "integrity": "sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==",
+      "license": "MIT",
+      "dependencies": {
+        "@emotion/hash": "^0.9.2",
+        "@emotion/memoize": "^0.9.0",
+        "@emotion/unitless": "^0.10.0",
+        "@emotion/utils": "^1.4.1",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/@emotion/sheet": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+      "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+      "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz",
+      "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": ">=16.8.0"
+      }
+    },
+    "node_modules/@emotion/utils": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.1.tgz",
+      "integrity": "sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/weak-memoize": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+      "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+      "license": "MIT"
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -195,6 +473,31 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.6.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz",
+      "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.6.12",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz",
+      "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.6.0",
+        "@floating-ui/utils": "^0.2.8"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.8",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
+      "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==",
+      "license": "MIT"
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.14",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -350,9 +653,9 @@
       }
     },
     "node_modules/@next/env": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
-      "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.17.tgz",
+      "integrity": "sha512-MCgO7VHxXo8sYR/0z+sk9fGyJJU636JyRmkjc7ZJY8Hurl8df35qG5hoAh5KMs75FLjhlEo9bb2LGe89Y/scDA==",
       "license": "MIT"
     },
     "node_modules/@next/eslint-plugin-next": {
@@ -366,9 +669,9 @@
       }
     },
     "node_modules/@next/swc-darwin-arm64": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz",
-      "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.17.tgz",
+      "integrity": "sha512-WiOf5nElPknrhRMTipXYTJcUz7+8IAjOYw3vXzj3BYRcVY0hRHKWgTgQ5439EvzQyHEko77XK+yN9x9OJ0oOog==",
       "cpu": [
         "arm64"
       ],
@@ -382,9 +685,9 @@
       }
     },
     "node_modules/@next/swc-darwin-x64": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz",
-      "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.17.tgz",
+      "integrity": "sha512-29y425wYnL17cvtxrDQWC3CkXe/oRrdt8ie61S03VrpwpPRI0XsnTvtKO06XCisK4alaMnZlf8riwZIbJTaSHQ==",
       "cpu": [
         "x64"
       ],
@@ -398,9 +701,9 @@
       }
     },
     "node_modules/@next/swc-linux-arm64-gnu": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz",
-      "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.17.tgz",
+      "integrity": "sha512-SSHLZls3ZwNEHsc+d0ynKS+7Af0Nr8+KTUBAy9pm6xz9SHkJ/TeuEg6W3cbbcMSh6j4ITvrjv3Oi8n27VR+IPw==",
       "cpu": [
         "arm64"
       ],
@@ -414,9 +717,9 @@
       }
     },
     "node_modules/@next/swc-linux-arm64-musl": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz",
-      "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.17.tgz",
+      "integrity": "sha512-VFge37us5LNPatB4F7iYeuGs9Dprqe4ZkW7lOEJM91r+Wf8EIdViWHLpIwfdDXinvCdLl6b4VyLpEBwpkctJHA==",
       "cpu": [
         "arm64"
       ],
@@ -430,9 +733,9 @@
       }
     },
     "node_modules/@next/swc-linux-x64-gnu": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz",
-      "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.17.tgz",
+      "integrity": "sha512-aaQlpxUVb9RZ41adlTYVQ3xvYEfBPUC8+6rDgmQ/0l7SvK8S1YNJzPmDPX6a4t0jLtIoNk7j+nroS/pB4nx7vQ==",
       "cpu": [
         "x64"
       ],
@@ -446,9 +749,9 @@
       }
     },
     "node_modules/@next/swc-linux-x64-musl": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz",
-      "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.17.tgz",
+      "integrity": "sha512-HSyEiFaEY3ay5iATDqEup5WAfrhMATNJm8dYx3ZxL+e9eKv10XKZCwtZByDoLST7CyBmyDz+OFJL1wigyXeaoA==",
       "cpu": [
         "x64"
       ],
@@ -462,9 +765,9 @@
       }
     },
     "node_modules/@next/swc-win32-arm64-msvc": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz",
-      "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.17.tgz",
+      "integrity": "sha512-h5qM9Btqv87eYH8ArrnLoAHLyi79oPTP2vlGNSg4CDvUiXgi7l0+5KuEGp5pJoMhjuv9ChRdm7mRlUUACeBt4w==",
       "cpu": [
         "arm64"
       ],
@@ -478,9 +781,9 @@
       }
     },
     "node_modules/@next/swc-win32-ia32-msvc": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz",
-      "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.17.tgz",
+      "integrity": "sha512-BD/G++GKSLexQjdyoEUgyo5nClU7er5rK0sE+HlEqnldJSm96CIr/+YOTT063LVTT/dUOeQsNgp5DXr86/K7/A==",
       "cpu": [
         "ia32"
       ],
@@ -494,9 +797,9 @@
       }
     },
     "node_modules/@next/swc-win32-x64-msvc": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz",
-      "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.17.tgz",
+      "integrity": "sha512-vkQfN1+4V4KqDibkW2q0sJ6CxQuXq5l2ma3z0BRcfIqkAMZiiW67T9yCpwqJKP68QghBtPEFjPAlaqe38O6frw==",
       "cpu": [
         "x64"
       ],
@@ -826,18 +1129,22 @@
         "undici-types": "~6.13.0"
       }
     },
+    "node_modules/@types/parse-json": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+      "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+      "license": "MIT"
+    },
     "node_modules/@types/prop-types": {
       "version": "15.7.12",
       "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
       "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/@types/react": {
       "version": "18.3.3",
       "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
       "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "@types/prop-types": "*",
@@ -854,6 +1161,15 @@
         "@types/react": "*"
       }
     },
+    "node_modules/@types/react-transition-group": {
+      "version": "4.4.11",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
+      "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/react": "*"
+      }
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "7.18.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -1371,6 +1687,21 @@
         "deep-equal": "^2.0.5"
       }
     },
+    "node_modules/babel-plugin-macros": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+      "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "cosmiconfig": "^7.0.0",
+        "resolve": "^1.19.0"
+      },
+      "engines": {
+        "node": ">=10",
+        "npm": ">=6"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1445,7 +1776,6 @@
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
       "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -1573,6 +1903,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/convert-source-map": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+      "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+      "license": "MIT"
+    },
     "node_modules/cookie": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
@@ -1597,6 +1933,31 @@
         "url": "https://github.com/sponsors/mesqueeb"
       }
     },
+    "node_modules/cosmiconfig": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+      "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/parse-json": "^4.0.0",
+        "import-fresh": "^3.2.1",
+        "parse-json": "^5.0.0",
+        "path-type": "^4.0.0",
+        "yaml": "^1.10.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cosmiconfig/node_modules/yaml": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+      "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.3",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -1627,7 +1988,6 @@
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/damerau-levenshtein": {
@@ -1695,7 +2055,6 @@
       "version": "4.3.6",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
       "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ms": "2.1.2"
@@ -1823,6 +2182,16 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
     "node_modules/eastasianwidth": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1849,6 +2218,15 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/error-ex": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+      "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
     "node_modules/es-abstract": {
       "version": "1.23.3",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
@@ -2040,7 +2418,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
       "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=10"
@@ -2805,6 +3182,12 @@
         "url": "https://opencollective.com/final-form"
       }
     },
+    "node_modules/find-root": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+      "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+      "license": "MIT"
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3190,6 +3573,15 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/hoist-non-react-statics": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+      "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "react-is": "^16.7.0"
+      }
+    },
     "node_modules/ignore": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3204,7 +3596,6 @@
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
       "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "parent-module": "^1.0.0",
@@ -3295,6 +3686,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "license": "MIT"
+    },
     "node_modules/is-async-function": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
@@ -3764,6 +4161,18 @@
         "js-yaml": "bin/js-yaml.js"
       }
     },
+    "node_modules/jsesc": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+      "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/json-buffer": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -3771,6 +4180,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "license": "MIT"
+    },
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3931,6 +4346,12 @@
         "node": ">=10"
       }
     },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3941,9 +4362,9 @@
       }
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "license": "MIT",
       "dependencies": {
         "braces": "^3.0.3",
@@ -3991,7 +4412,6 @@
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
       "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/mz": {
@@ -4031,12 +4451,12 @@
       "license": "MIT"
     },
     "node_modules/next": {
-      "version": "14.2.5",
-      "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz",
-      "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
+      "version": "14.2.17",
+      "resolved": "https://registry.npmjs.org/next/-/next-14.2.17.tgz",
+      "integrity": "sha512-hNo/Zy701DDO3nzKkPmsLRlDfNCtb1OJxFUvjGEl04u7SFa3zwC6hqsOUzMajcaEOEV8ey1GjvByvrg0Qr5AiQ==",
       "license": "MIT",
       "dependencies": {
-        "@next/env": "14.2.5",
+        "@next/env": "14.2.17",
         "@swc/helpers": "0.5.5",
         "busboy": "1.6.0",
         "caniuse-lite": "^1.0.30001579",
@@ -4051,15 +4471,15 @@
         "node": ">=18.17.0"
       },
       "optionalDependencies": {
-        "@next/swc-darwin-arm64": "14.2.5",
-        "@next/swc-darwin-x64": "14.2.5",
-        "@next/swc-linux-arm64-gnu": "14.2.5",
-        "@next/swc-linux-arm64-musl": "14.2.5",
-        "@next/swc-linux-x64-gnu": "14.2.5",
-        "@next/swc-linux-x64-musl": "14.2.5",
-        "@next/swc-win32-arm64-msvc": "14.2.5",
-        "@next/swc-win32-ia32-msvc": "14.2.5",
-        "@next/swc-win32-x64-msvc": "14.2.5"
+        "@next/swc-darwin-arm64": "14.2.17",
+        "@next/swc-darwin-x64": "14.2.17",
+        "@next/swc-linux-arm64-gnu": "14.2.17",
+        "@next/swc-linux-arm64-musl": "14.2.17",
+        "@next/swc-linux-x64-gnu": "14.2.17",
+        "@next/swc-linux-x64-musl": "14.2.17",
+        "@next/swc-win32-arm64-msvc": "14.2.17",
+        "@next/swc-win32-ia32-msvc": "14.2.17",
+        "@next/swc-win32-x64-msvc": "14.2.17"
       },
       "peerDependencies": {
         "@opentelemetry/api": "^1.1.0",
@@ -4081,14 +4501,14 @@
       }
     },
     "node_modules/next-auth": {
-      "version": "4.24.7",
-      "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.7.tgz",
-      "integrity": "sha512-iChjE8ov/1K/z98gdKbn2Jw+2vLgJtVV39X+rCP5SGnVQuco7QOr19FRNGMIrD8d3LYhHWV9j9sKLzq1aDWWQQ==",
+      "version": "4.24.10",
+      "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.10.tgz",
+      "integrity": "sha512-8NGqiRO1GXBcVfV8tbbGcUgQkAGsX4GRzzXXea4lDikAsJtD5KiEY34bfhUOjHLvr6rT6afpcxw2H8EZqOV6aQ==",
       "license": "ISC",
       "dependencies": {
         "@babel/runtime": "^7.20.13",
         "@panva/hkdf": "^1.0.2",
-        "cookie": "^0.5.0",
+        "cookie": "^0.7.0",
         "jose": "^4.15.5",
         "oauth": "^0.9.15",
         "openid-client": "^5.4.0",
@@ -4097,21 +4517,25 @@
         "uuid": "^8.3.2"
       },
       "peerDependencies": {
-        "next": "^12.2.5 || ^13 || ^14",
+        "@auth/core": "0.34.2",
+        "next": "^12.2.5 || ^13 || ^14 || ^15",
         "nodemailer": "^6.6.5",
         "react": "^17.0.2 || ^18",
         "react-dom": "^17.0.2 || ^18"
       },
       "peerDependenciesMeta": {
+        "@auth/core": {
+          "optional": true
+        },
         "nodemailer": {
           "optional": true
         }
       }
     },
     "node_modules/next-auth/node_modules/cookie": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
-      "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
       "license": "MIT",
       "engines": {
         "node": ">= 0.6"
@@ -4419,7 +4843,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
       "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "callsites": "^3.0.0"
@@ -4428,6 +4851,24 @@
         "node": ">=6"
       }
     },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/path-exists": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4489,7 +4930,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -4850,7 +5290,6 @@
       "version": "15.8.1",
       "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
       "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "loose-envify": "^1.4.0",
@@ -4934,9 +5373,45 @@
       "version": "16.13.1",
       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
-      "dev": true,
       "license": "MIT"
     },
+    "node_modules/react-select": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.3.tgz",
+      "integrity": "sha512-lVswnIq8/iTj1db7XCG74M/3fbGB6ZaluCzvwPGT5ZOjCdL/k0CLWhEK0vCBLuU5bHTEf6Gj8jtSvi+3v+tO1w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.0",
+        "@emotion/cache": "^11.4.0",
+        "@emotion/react": "^11.8.1",
+        "@floating-ui/dom": "^1.0.1",
+        "@types/react-transition-group": "^4.4.0",
+        "memoize-one": "^6.0.0",
+        "prop-types": "^15.6.0",
+        "react-transition-group": "^4.3.0",
+        "use-isomorphic-layout-effect": "^1.1.2"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
     "node_modules/read-cache": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -5026,7 +5501,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=4"
@@ -5299,6 +5773,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/source-map": {
+      "version": "0.5.7",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+      "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/source-map-js": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@@ -5566,6 +6049,12 @@
         }
       }
     },
+    "node_modules/stylis": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+      "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+      "license": "MIT"
+    },
     "node_modules/sucrase": {
       "version": "3.35.0",
       "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@@ -5909,6 +6398,20 @@
         "punycode": "^2.1.0"
       }
     },
+    "node_modules/use-isomorphic-layout-effect": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz",
+      "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/react": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/ladderly-io/package.json b/ladderly-io/package.json
index 22db408a..69341014 100644
--- a/ladderly-io/package.json
+++ b/ladderly-io/package.json
@@ -29,6 +29,7 @@
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-final-form": "^6.5.9",
+    "react-select": "^5.8.3",
     "server-only": "^0.0.1",
     "superjson": "^2.2.1",
     "zod": "^3.23.3"

From 517518616aadf0c3adadc831d0e158ab1c5a69f4 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 20:07:59 -0500
Subject: [PATCH 10/31] feat: tell user their ID

---
 ladderly-io/src/app/settings/page.tsx | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/ladderly-io/src/app/settings/page.tsx b/ladderly-io/src/app/settings/page.tsx
index 021e4353..9be30fe8 100644
--- a/ladderly-io/src/app/settings/page.tsx
+++ b/ladderly-io/src/app/settings/page.tsx
@@ -5,6 +5,7 @@ import { LargeCard } from "~/app/core/components/LargeCard";
 import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
 import { SettingsFormWrapper } from "./components/SettingsFormWrapper";
 import { redirect } from "next/navigation";
+import { PaymentTierEnum } from "@prisma/client";
 
 export const metadata = {
   title: "Settings",
@@ -13,6 +14,7 @@ export const metadata = {
 export default async function SettingsPage() {
   try {
     const settings = await api.user.getSettings();
+    const isPremium = settings.subscription.tier === PaymentTierEnum.FREE;
 
     return (
       <LadderlyPageWrapper>
@@ -22,6 +24,10 @@ export default async function SettingsPage() {
               Edit User Settings
             </h1>
             <p className="mt-4">
+              Welcome, User ID {settings.id}!{" "}
+              {isPremium
+                ? "You are signed in to a free account."
+                : "You are signed in to a premium account."}{" "}
               Please email john@ladderly.io to update your subscription tier.
             </p>
 

From a56b27df266259715bc79baec3d8692938fb756f Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 20:36:35 -0500
Subject: [PATCH 11/31] feat: LadderlySession

---
 ladderly-io/src/app/home/HomePageContent.tsx | 34 ++++-------
 ladderly-io/src/app/page.tsx                 | 12 ++--
 ladderly-io/src/server/api/routers/user.ts   | 63 +++++++++++++++++---
 ladderly-io/src/server/auth.ts               | 62 ++++++++++++-------
 4 files changed, 117 insertions(+), 54 deletions(-)

diff --git a/ladderly-io/src/app/home/HomePageContent.tsx b/ladderly-io/src/app/home/HomePageContent.tsx
index bf076583..a12b698f 100644
--- a/ladderly-io/src/app/home/HomePageContent.tsx
+++ b/ladderly-io/src/app/home/HomePageContent.tsx
@@ -3,17 +3,17 @@
 import { PaymentTierEnum } from "@prisma/client";
 import Image from "next/image";
 import Link from "next/link";
-import { Suspense } from "react";
 
 import React from "react";
-import useSubscriptionLevel from "src/app/users/hooks/useSubscriptionLevel";
 import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
 import PricingGrid from "~/app/core/components/pricing-grid/PricingGrid";
-import styles from "~/styles/Home.module.css";
+import { LadderlySession } from "~/server/auth";
 import { AuthButtons } from "../_components/AuthButtons";
 import { LadderlyHelpsBlock } from "./LadderlyHelpsBlock";
 import { TestimonialBlock } from "./TestimonialBlock";
 
+import styles from "~/styles/Home.module.css";
+
 // note: do not extract `DARK_MODE_STANDARD_CLASSES` out of file.
 // it is duplicated intentionally between files to ensure tailwind classes are bundled
 const DARK_MODE_STANDARD_CLASSES =
@@ -25,11 +25,12 @@ const HomePageCardSubheading = ({
   children: React.ReactNode;
 }) => <h2 className="mb-2 text-xl font-bold">{children}</h2>;
 
-const AdvancedChecklistContentBlock = () => {
-  // const { tier } = useSubscriptionLevel();
-  // TODO: get subscription tier from user router
-  // const isPaid = tier != PaymentTierEnum.FREE;
-  const isPaid = true;
+const AdvancedChecklistContentBlock = ({
+  session,
+}: {
+  session: LadderlySession | null;
+}) => {
+  const isPaid = session?.user.subscription.tier !== PaymentTierEnum.FREE;
 
   return isPaid ? (
     <div
@@ -49,8 +50,8 @@ const AdvancedChecklistContentBlock = () => {
   ) : null;
 };
 
-const HomePageContent = ({ hello, session }: { hello: any; session: any }) => (
-  <LadderlyPageWrapper title="Home">
+const HomePageContent = ({ session }: { session: LadderlySession | null }) => (
+  <LadderlyPageWrapper>
     <main style={{ padding: "0rem 1rem" }}>
       <div className={styles.wrapper}>
         <div
@@ -139,19 +140,10 @@ const HomePageContent = ({ hello, session }: { hello: any; session: any }) => (
           </div>
 
           <section id="deleteme">
-            <div className="flex flex-col items-center gap-2">
-              <p className="text-2xl text-white">
-                {hello ? hello.greeting : "Loading tRPC query..."}
-              </p>
-
-              <AuthButtons initialSession={session} />
-            </div>
+            <AuthButtons initialSession={session} />
           </section>
 
-          <Suspense>
-            <AdvancedChecklistContentBlock />
-          </Suspense>
-
+          <AdvancedChecklistContentBlock session={session} />
           <PricingGrid />
         </div>
       </div>
diff --git a/ladderly-io/src/app/page.tsx b/ladderly-io/src/app/page.tsx
index 44280745..6539d493 100644
--- a/ladderly-io/src/app/page.tsx
+++ b/ladderly-io/src/app/page.tsx
@@ -1,18 +1,18 @@
 import { Suspense } from "react";
+import { getServerAuthSession, LadderlySession } from "~/server/auth";
 import HomePageContent from "./home/HomePageContent";
 import HomePageSkeleton from "./home/HomePageSkeleton";
 
-import { getServerAuthSession } from "~/server/auth";
-import { api } from "~/trpc/server";
+export const metadata = {
+  title: "Home",
+};
 
 export default async function HomePage() {
-  // TODO: replace hello with subscription level
-  const hello = await api.post.hello({ text: "from tRPC" });
-  const session = await getServerAuthSession();
+  const session: LadderlySession | null = await getServerAuthSession();
 
   return (
     <Suspense fallback={<HomePageSkeleton />}>
-      <HomePageContent hello={hello} session={session} />
+      <HomePageContent session={session} />
     </Suspense>
   );
 }
diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index fc661898..9f004363 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -13,12 +13,13 @@ const tiersOrder = {
   PREMIUM: 2,
 } as const;
 
-const UpdateSettingsSchema = z.object({
+// Define the settings input type
+export const UpdateSettingsSchema = z.object({
   email: z.string(),
-  emailBackup: z.string().optional(),
-  emailStripe: z.string().optional(),
-  nameFirst: z.string().optional(),
-  nameLast: z.string().optional(),
+  emailBackup: z.string().nullable(),
+  emailStripe: z.string().nullable(),
+  nameFirst: z.string().nullable(),
+  nameLast: z.string().nullable(),
   hasOpenToWork: z.boolean(),
   hasShoutOutsEnabled: z.boolean(),
   profileBlurb: z.string().nullable(),
@@ -35,6 +36,36 @@ const UpdateSettingsSchema = z.object({
   hasInPersonEventInterest: z.boolean(),
 });
 
+// Define the settings output type
+export const UserSettingsSchema = z.object({
+  id: z.number(),
+  email: z.string(),
+  emailBackup: z.string().nullable(),
+  emailStripe: z.string().nullable(),
+  nameFirst: z.string().nullable(),
+  nameLast: z.string().nullable(),
+  hasOpenToWork: z.boolean(),
+  hasShoutOutsEnabled: z.boolean(),
+  profileBlurb: z.string().nullable(),
+  profileContactEmail: z.string().nullable(),
+  profileGitHubUri: z.string().nullable(),
+  profileHomepageUri: z.string().nullable(),
+  profileLinkedInUri: z.string().nullable(),
+  residenceCountry: z.string(),
+  residenceUSState: z.string(),
+  hasPublicProfileEnabled: z.boolean(),
+  hasSmallGroupInterest: z.boolean(),
+  hasLiveStreamInterest: z.boolean(),
+  hasOnlineEventInterest: z.boolean(),
+  hasInPersonEventInterest: z.boolean(),
+  subscription: z.object({
+    tier: z.nativeEnum(PaymentTierEnum),
+    type: z.string(),
+  }),
+});
+
+export type UserSettings = z.infer<typeof UserSettingsSchema>;
+
 export const userRouter = createTRPCRouter({
   getCurrentUser: publicProcedure.query(async ({ ctx }) => {
     if (!ctx.session) {
@@ -235,7 +266,7 @@ export const userRouter = createTRPCRouter({
         });
       }
 
-      return {
+      const settings: UserSettings = {
         id: user.id,
         email: user.email,
         emailBackup: user.emailBackup,
@@ -261,6 +292,8 @@ export const userRouter = createTRPCRouter({
           type: subscription.type,
         },
       };
+
+      return UserSettingsSchema.parse(settings);
     });
 
     return result;
@@ -312,8 +345,24 @@ export const userRouter = createTRPCRouter({
           residenceCountry: input.residenceCountry?.trim() || '',
           residenceUSState: input.residenceUSState?.trim() || '',
         },
+        include: {
+          subscriptions: {
+            where: { type: 'ACCOUNT_PLAN' },
+            select: { tier: true, type: true },
+          },
+        },
       });
 
-      return user;
+      const subscription = user.subscriptions[0] || {
+        tier: PaymentTierEnum.FREE,
+        type: 'ACCOUNT_PLAN',
+      };
+
+      const settings: UserSettings = {
+        ...user,
+        subscription,
+      };
+
+      return UserSettingsSchema.parse(settings);
     }),
 });
diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index 5effd60f..3ae40b0c 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -7,36 +7,58 @@ import DiscordProvider from "next-auth/providers/discord";
 import GithubProvider from "next-auth/providers/github";
 import GoogleProvider from "next-auth/providers/google";
 import LinkedInProvider from "next-auth/providers/linkedin";
+import { PaymentTierEnum } from "@prisma/client";
 
 import { env } from "~/env";
 import { db } from "~/server/db";
 import { LadderlyMigrationAdapter } from "./LadderlyMigrationAdapter";
 
-declare module "next-auth" {
-  interface Session extends DefaultSession {
-    user: {
-      id: string;
-      // ...other properties
-      // role: UserRole;
-    } & DefaultSession["user"];
+export interface LadderlySession extends DefaultSession {
+  user: {
+    id: string;
+    subscription: {
+      tier: PaymentTierEnum;
+      type: string;
+    };
+    email?: string | null;
+    name?: string | null;
+    image?: string | null;
   }
+}
 
-  // interface User {
-  //   // ...other properties
-  //   // role: UserRole;
-  // }
+declare module "next-auth" {
+  interface Session extends LadderlySession {}
 }
 
 export const authOptions: NextAuthOptions = {
   callbacks: {
-    session: ({ session, user }) => ({
-      ...session,
-      user: {
-        ...session.user,
-        id: user.id,
-      },
-    }),
+    session: async ({ session, user }): Promise<LadderlySession> => {
+      const dbUser = await db.user.findUnique({
+        where: { id: parseInt(user.id) },
+        include: {
+          subscriptions: {
+            where: { type: 'ACCOUNT_PLAN' },
+            select: { tier: true, type: true },
+          },
+        },
+      });
+
+      const subscription = dbUser?.subscriptions[0] || {
+        tier: PaymentTierEnum.FREE,
+        type: 'ACCOUNT_PLAN',
+      };
+
+      return {
+        ...session,
+        user: {
+          ...session.user,
+          id: user.id,
+          subscription,
+        },
+      };
+    },
     signIn: async ({ user, account, profile, email, credentials }) => {
+      // If the user exists but doesn't have an account for this provider
       if (account?.provider && user.email) {
         const existingUser = await db.user.findUnique({
           where: { email: user.email },
@@ -44,7 +66,6 @@ export const authOptions: NextAuthOptions = {
         });
 
         if (existingUser) {
-          // If the user exists but doesn't have an account for this provider
           const existingAccount = existingUser.accounts.find(
             (acc) => acc.provider === account.provider
           );
@@ -107,4 +128,5 @@ export const authOptions: NextAuthOptions = {
   ].filter(Boolean) as NextAuthOptions["providers"],
 };
 
-export const getServerAuthSession = () => getServerSession(authOptions);
+export const getServerAuthSession = () => 
+  getServerSession(authOptions) as Promise<LadderlySession | null>;

From a577b046070c637b72f094450a97c689561ccfa5 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 21:23:09 -0500
Subject: [PATCH 12/31] feat: auth router

---
 .../src/app/(auth)/components/LoginForm.tsx   | 70 +++++++++++++++++++
 ladderly-io/src/app/(auth)/login/page.tsx     | 32 +++++++++
 ladderly-io/src/server/api/root.ts            |  2 +
 ladderly-io/src/server/api/routers/auth.ts    | 35 ++++++++++
 4 files changed, 139 insertions(+)
 create mode 100644 ladderly-io/src/app/(auth)/components/LoginForm.tsx
 create mode 100644 ladderly-io/src/app/(auth)/login/page.tsx
 create mode 100644 ladderly-io/src/server/api/routers/auth.ts

diff --git a/ladderly-io/src/app/(auth)/components/LoginForm.tsx b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
new file mode 100644
index 00000000..17ad3c71
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import { signIn } from "next-auth/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { Form } from "~/app/core/components/Form";
+import { LabeledTextField } from "~/app/core/components/LabeledTextField";
+import { Login as LoginSchema } from "~/app/(auth)/schemas";
+import { FORM_ERROR } from "final-form";
+
+export const LoginForm = () => {
+  const router = useRouter();
+
+  return (
+    <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
+      <h1 className="mb-4 text-2xl font-bold text-gray-800">Log In</h1>
+
+      <Form
+        className="space-y-4"
+        submitText="Log In"
+        schema={LoginSchema}
+        initialValues={{ email: "", password: "" }}
+        onSubmit={async (values) => {
+          try {
+            const result = await signIn("credentials", {
+              email: values.email,
+              password: values.password,
+              redirect: false,
+            });
+
+            if (result?.error) {
+              return { [FORM_ERROR]: "Invalid email or password" };
+            }
+
+            if (result?.ok) {
+              router.push("/");
+              router.refresh();
+            }
+          } catch (error: any) {
+            return {
+              [FORM_ERROR]: "An unexpected error occurred. Please try again.",
+            };
+          }
+        }}
+      >
+        <LabeledTextField name="email" label="Email" placeholder="Email" />
+        <LabeledTextField
+          name="password"
+          label="Password"
+          placeholder="Password"
+          type="password"
+        />
+        <div className="mt-4 text-left">
+          <Link className="underline" href="/forgot-password">
+            Forgot your password?
+          </Link>
+        </div>
+      </Form>
+
+      <div className="mt-4">
+        Need to create an account?{" "}
+        <Link className="underline" href="/signup">
+          Sign up here!
+        </Link>
+      </div>
+    </div>
+  );
+};
+
+export default LoginForm;
diff --git a/ladderly-io/src/app/(auth)/login/page.tsx b/ladderly-io/src/app/(auth)/login/page.tsx
new file mode 100644
index 00000000..6fcc7f1c
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/login/page.tsx
@@ -0,0 +1,32 @@
+// import { redirect } from "next/navigation";
+import Link from "next/link";
+import LoginForm from "src/app/(auth)/components/LoginForm";
+// import Layout from 'src/core/layouts/Layout'
+
+export const metadata = {
+  title: "Log In",
+};
+
+const LoginPage = () => {
+  // const router = useRouter();
+
+  return (
+    // <Layout title="Log In">
+    <div className="relative min-h-screen">
+      <nav className="flex border border-ladderly-light-purple-1 bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
+        <Link
+          href="/"
+          className="ml-auto text-gray-800 hover:text-ladderly-pink"
+        >
+          Back to Home
+        </Link>
+      </nav>
+      <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
+        <LoginForm />
+      </div>
+    </div>
+    // </Layout>
+  );
+};
+
+export default LoginPage;
diff --git a/ladderly-io/src/server/api/root.ts b/ladderly-io/src/server/api/root.ts
index 67b4a0dd..5f89842c 100644
--- a/ladderly-io/src/server/api/root.ts
+++ b/ladderly-io/src/server/api/root.ts
@@ -1,6 +1,7 @@
 import { postRouter } from "~/server/api/routers/post";
 import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
 import { userRouter } from "./routers/user";
+import { authRouter } from "./routers/auth";
 
 /**
  * This is the primary router for your server.
@@ -10,6 +11,7 @@ import { userRouter } from "./routers/user";
 export const appRouter = createTRPCRouter({
   post: postRouter,
   user: userRouter,
+  auth: authRouter,
 });
 
 // export type definition of API
diff --git a/ladderly-io/src/server/api/routers/auth.ts b/ladderly-io/src/server/api/routers/auth.ts
new file mode 100644
index 00000000..aede8f5d
--- /dev/null
+++ b/ladderly-io/src/server/api/routers/auth.ts
@@ -0,0 +1,35 @@
+import { z } from "zod";
+import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
+import { TRPCError } from '@trpc/server';
+
+export const LoginSchema = z.object({
+  email: z.string().email(),
+  password: z.string().min(8),
+});
+
+export const authRouter = createTRPCRouter({
+  validateCredentials: publicProcedure
+    .input(LoginSchema)
+    .mutation(async ({ ctx, input }) => {
+      const { email } = input;
+      
+      const user = await ctx.db.user.findFirst({ 
+        where: { email },
+        select: {
+          id: true,
+          email: true,
+          nameFirst: true,
+          nameLast: true,
+        }
+      });
+
+      if (!user) {
+        throw new TRPCError({
+          code: 'UNAUTHORIZED',
+          message: 'Invalid email or password',
+        });
+      }
+
+      return user;
+    }),
+}); 
\ No newline at end of file

From 474460e07e741a6a4a7fff57ae4d229ffe7359b3 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 21:36:07 -0500
Subject: [PATCH 13/31] feat: SocialSignIn

---
 .../src/app/(auth)/components/LoginForm.tsx   | 12 +++---
 ladderly-io/src/app/(auth)/schemas.ts         | 42 +++++++++++++++++++
 .../src/app/_components/AuthButtons.tsx       | 34 ---------------
 .../src/app/_components/SocialSignIn.tsx      | 34 +++++++++++++++
 ladderly-io/src/app/home/HomePageContent.tsx  |  5 ---
 5 files changed, 83 insertions(+), 44 deletions(-)
 create mode 100644 ladderly-io/src/app/(auth)/schemas.ts
 delete mode 100644 ladderly-io/src/app/_components/AuthButtons.tsx
 create mode 100644 ladderly-io/src/app/_components/SocialSignIn.tsx

diff --git a/ladderly-io/src/app/(auth)/components/LoginForm.tsx b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
index 17ad3c71..08da8d50 100644
--- a/ladderly-io/src/app/(auth)/components/LoginForm.tsx
+++ b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
@@ -1,20 +1,24 @@
 "use client";
 
+import { FORM_ERROR } from "final-form";
 import { signIn } from "next-auth/react";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
+import { Login as LoginSchema } from "~/app/(auth)/schemas";
+import { SocialSignIn } from "~/app/_components/SocialSignIn";
 import { Form } from "~/app/core/components/Form";
 import { LabeledTextField } from "~/app/core/components/LabeledTextField";
-import { Login as LoginSchema } from "~/app/(auth)/schemas";
-import { FORM_ERROR } from "final-form";
+import { LadderlySession } from "~/server/auth";
 
-export const LoginForm = () => {
+export const LoginForm = ({ session }: { session: LadderlySession | null }) => {
   const router = useRouter();
 
   return (
     <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
       <h1 className="mb-4 text-2xl font-bold text-gray-800">Log In</h1>
 
+      <SocialSignIn initialSession={session} />
+
       <Form
         className="space-y-4"
         submitText="Log In"
@@ -66,5 +70,3 @@ export const LoginForm = () => {
     </div>
   );
 };
-
-export default LoginForm;
diff --git a/ladderly-io/src/app/(auth)/schemas.ts b/ladderly-io/src/app/(auth)/schemas.ts
new file mode 100644
index 00000000..62e5e8ae
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/schemas.ts
@@ -0,0 +1,42 @@
+import { z } from 'zod'
+
+export const email = z
+  .string()
+  .email()
+  .transform((str) => str.toLowerCase().trim())
+
+export const password = z
+  .string()
+  .min(10)
+  .max(100)
+  .transform((str) => str.trim())
+
+export const Signup = z.object({
+  email,
+  password,
+})
+
+export const Login = z.object({
+  email,
+  password: z.string(),
+})
+
+export const ForgotPassword = z.object({
+  email,
+})
+
+export const ResetPassword = z
+  .object({
+    password: password,
+    passwordConfirmation: password,
+    token: z.string(),
+  })
+  .refine((data) => data.password === data.passwordConfirmation, {
+    message: "Passwords don't match",
+    path: ['passwordConfirmation'], // set the path of the error
+  })
+
+export const ChangePassword = z.object({
+  currentPassword: z.string(),
+  newPassword: password,
+})
diff --git a/ladderly-io/src/app/_components/AuthButtons.tsx b/ladderly-io/src/app/_components/AuthButtons.tsx
deleted file mode 100644
index 5ba5025b..00000000
--- a/ladderly-io/src/app/_components/AuthButtons.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-"use client";
-
-import { signIn, signOut } from "next-auth/react";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-
-export function AuthButtons({ initialSession }: { initialSession: any }) {
-  const [session, setSession] = useState(initialSession);
-  const router = useRouter();
-
-  const handleSignIn = async () => {
-    await signIn(undefined, { callbackUrl: "/" });
-  };
-
-  const handleSignOut = async () => {
-    const data = await signOut({ redirect: false, callbackUrl: "/" });
-    setSession(null);
-    router.push(data.url);
-  };
-
-  return (
-    <div className="flex flex-col items-center justify-center gap-4">
-      <p className="text-center text-2xl text-white">
-        {session && <span>Logged in as {session.user?.name}</span>}
-      </p>
-      <button
-        onClick={session ? handleSignOut : handleSignIn}
-        className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
-      >
-        {session ? "Sign out" : "Sign in"}
-      </button>
-    </div>
-  );
-}
diff --git a/ladderly-io/src/app/_components/SocialSignIn.tsx b/ladderly-io/src/app/_components/SocialSignIn.tsx
new file mode 100644
index 00000000..861980b4
--- /dev/null
+++ b/ladderly-io/src/app/_components/SocialSignIn.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { signIn, signOut } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { LadderlySession } from "~/server/auth";
+
+export function SocialSignIn({
+  initialSession,
+}: {
+  initialSession: LadderlySession | null;
+}) {
+  const [session, setSession] = useState(initialSession);
+  const router = useRouter();
+
+  const handleSignIn = async () => {
+    await signIn(undefined, { callbackUrl: "/" });
+  };
+
+  const handleSignOut = async () => {
+    const data = await signOut({ redirect: false, callbackUrl: "/" });
+    setSession(null);
+    router.push(data.url);
+  };
+
+  return (
+    <button
+      onClick={session ? handleSignOut : handleSignIn}
+      className="rounded-full bg-gray-200 px-10 p-2 my-2 font-semibold no-underline transition hover:bg-gray-800 hover:text-white"
+    >
+      {session ? "Sign out" : "Social Sign in"}
+    </button>
+  );
+}
diff --git a/ladderly-io/src/app/home/HomePageContent.tsx b/ladderly-io/src/app/home/HomePageContent.tsx
index a12b698f..b2ddf181 100644
--- a/ladderly-io/src/app/home/HomePageContent.tsx
+++ b/ladderly-io/src/app/home/HomePageContent.tsx
@@ -8,7 +8,6 @@ import React from "react";
 import { LadderlyPageWrapper } from "~/app/core/components/page-wrapper/LadderlyPageWrapper";
 import PricingGrid from "~/app/core/components/pricing-grid/PricingGrid";
 import { LadderlySession } from "~/server/auth";
-import { AuthButtons } from "../_components/AuthButtons";
 import { LadderlyHelpsBlock } from "./LadderlyHelpsBlock";
 import { TestimonialBlock } from "./TestimonialBlock";
 
@@ -139,10 +138,6 @@ const HomePageContent = ({ session }: { session: LadderlySession | null }) => (
             </section>
           </div>
 
-          <section id="deleteme">
-            <AuthButtons initialSession={session} />
-          </section>
-
           <AdvancedChecklistContentBlock session={session} />
           <PricingGrid />
         </div>

From 73a8f5da9f09e62f3d62081d01354fc2e04bc15a Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 21:44:46 -0500
Subject: [PATCH 14/31] feat: pass session from login page

---
 ladderly-io/src/app/(auth)/login/page.tsx | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/ladderly-io/src/app/(auth)/login/page.tsx b/ladderly-io/src/app/(auth)/login/page.tsx
index 6fcc7f1c..caa06740 100644
--- a/ladderly-io/src/app/(auth)/login/page.tsx
+++ b/ladderly-io/src/app/(auth)/login/page.tsx
@@ -1,17 +1,18 @@
-// import { redirect } from "next/navigation";
 import Link from "next/link";
-import LoginForm from "src/app/(auth)/components/LoginForm";
-// import Layout from 'src/core/layouts/Layout'
+import { LoginForm } from "~/app/(auth)/components/LoginForm";
+import { getServerAuthSession } from "~/server/auth";
+import { LadderlySession } from "~/server/auth";
 
 export const metadata = {
   title: "Log In",
 };
 
-const LoginPage = () => {
-  // const router = useRouter();
+const LoginPage = async () => {
+  const session: LadderlySession | null = await getServerAuthSession();
+
+  // TODO: tell user if they are already logged in
 
   return (
-    // <Layout title="Log In">
     <div className="relative min-h-screen">
       <nav className="flex border border-ladderly-light-purple-1 bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
         <Link
@@ -22,10 +23,9 @@ const LoginPage = () => {
         </Link>
       </nav>
       <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
-        <LoginForm />
+        <LoginForm session={session} />
       </div>
     </div>
-    // </Layout>
   );
 };
 

From ee36a3070e731850eed9eb989a304ca6629080db Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 10 Nov 2024 21:52:22 -0500
Subject: [PATCH 15/31] feat: easier login w goog

---
 .../src/app/(auth)/components/LoginForm.tsx   | 27 ++++++++++++---
 ladderly-io/src/app/_components/.gitkeep      |  0
 .../src/app/_components/SocialSignIn.tsx      | 34 -------------------
 3 files changed, 22 insertions(+), 39 deletions(-)
 delete mode 100644 ladderly-io/src/app/_components/.gitkeep
 delete mode 100644 ladderly-io/src/app/_components/SocialSignIn.tsx

diff --git a/ladderly-io/src/app/(auth)/components/LoginForm.tsx b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
index 08da8d50..db2ef2d4 100644
--- a/ladderly-io/src/app/(auth)/components/LoginForm.tsx
+++ b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
@@ -5,23 +5,40 @@ import { signIn } from "next-auth/react";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { Login as LoginSchema } from "~/app/(auth)/schemas";
-import { SocialSignIn } from "~/app/_components/SocialSignIn";
 import { Form } from "~/app/core/components/Form";
 import { LabeledTextField } from "~/app/core/components/LabeledTextField";
-import { LadderlySession } from "~/server/auth";
 
-export const LoginForm = ({ session }: { session: LadderlySession | null }) => {
+export const LoginForm = () => {
   const router = useRouter();
 
   return (
     <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
       <h1 className="mb-4 text-2xl font-bold text-gray-800">Log In</h1>
 
-      <SocialSignIn initialSession={session} />
+      <button
+        onClick={() => signIn("google", { callbackUrl: "/" })}
+        className="mb-6 flex w-full items-center justify-center gap-2 rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-700 shadow-sm hover:bg-gray-50"
+      >
+        <img
+          src="https://www.google.com/favicon.ico"
+          alt="Google"
+          className="h-5 w-5"
+        />
+        Sign in with Google
+      </button>
+
+      <div className="relative mb-6">
+        <div className="absolute inset-0 flex items-center">
+          <div className="w-full border-t border-gray-300"></div>
+        </div>
+        <div className="relative flex justify-center text-sm">
+          <span className="bg-white px-2 text-gray-500">Or continue with</span>
+        </div>
+      </div>
 
       <Form
         className="space-y-4"
-        submitText="Log In"
+        submitText="Log In with Email"
         schema={LoginSchema}
         initialValues={{ email: "", password: "" }}
         onSubmit={async (values) => {
diff --git a/ladderly-io/src/app/_components/.gitkeep b/ladderly-io/src/app/_components/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/ladderly-io/src/app/_components/SocialSignIn.tsx b/ladderly-io/src/app/_components/SocialSignIn.tsx
deleted file mode 100644
index 861980b4..00000000
--- a/ladderly-io/src/app/_components/SocialSignIn.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-"use client";
-
-import { signIn, signOut } from "next-auth/react";
-import { useRouter } from "next/navigation";
-import { useState } from "react";
-import { LadderlySession } from "~/server/auth";
-
-export function SocialSignIn({
-  initialSession,
-}: {
-  initialSession: LadderlySession | null;
-}) {
-  const [session, setSession] = useState(initialSession);
-  const router = useRouter();
-
-  const handleSignIn = async () => {
-    await signIn(undefined, { callbackUrl: "/" });
-  };
-
-  const handleSignOut = async () => {
-    const data = await signOut({ redirect: false, callbackUrl: "/" });
-    setSession(null);
-    router.push(data.url);
-  };
-
-  return (
-    <button
-      onClick={session ? handleSignOut : handleSignIn}
-      className="rounded-full bg-gray-200 px-10 p-2 my-2 font-semibold no-underline transition hover:bg-gray-800 hover:text-white"
-    >
-      {session ? "Sign out" : "Social Sign in"}
-    </button>
-  );
-}

From 0ca762b891b72cdfebc047e150521b2836ffb786 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Thu, 14 Nov 2024 22:53:57 -0500
Subject: [PATCH 16/31] feat: tell ppl if they're already logged in

---
 ladderly-io/src/app/(auth)/login/page.tsx |  4 +---
 ladderly-io/src/server/auth.ts            | 22 ++++++++++++++--------
 2 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/ladderly-io/src/app/(auth)/login/page.tsx b/ladderly-io/src/app/(auth)/login/page.tsx
index caa06740..b148b231 100644
--- a/ladderly-io/src/app/(auth)/login/page.tsx
+++ b/ladderly-io/src/app/(auth)/login/page.tsx
@@ -10,8 +10,6 @@ export const metadata = {
 const LoginPage = async () => {
   const session: LadderlySession | null = await getServerAuthSession();
 
-  // TODO: tell user if they are already logged in
-
   return (
     <div className="relative min-h-screen">
       <nav className="flex border border-ladderly-light-purple-1 bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
@@ -23,7 +21,7 @@ const LoginPage = async () => {
         </Link>
       </nav>
       <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
-        <LoginForm session={session} />
+        {session?.user ? <p>You are already logged in.</p> : <LoginForm />}
       </div>
     </div>
   );
diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index 3ae40b0c..e9bd4dbc 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -14,14 +14,14 @@ import { db } from "~/server/db";
 import { LadderlyMigrationAdapter } from "./LadderlyMigrationAdapter";
 
 export interface LadderlySession extends DefaultSession {
-  user: {
+  user?: {
     id: string;
     subscription: {
       tier: PaymentTierEnum;
       type: string;
     };
-    email?: string | null;
-    name?: string | null;
+    email: string | null;
+    name: string | null;
     image?: string | null;
   }
 }
@@ -48,13 +48,19 @@ export const authOptions: NextAuthOptions = {
         type: 'ACCOUNT_PLAN',
       };
 
+      const userData = dbUser
+        ? {
+            email: dbUser.email,
+            name: dbUser.name,
+            image: dbUser.image,
+            id: user.id,
+            subscription,
+          }
+        : undefined;
+
       return {
         ...session,
-        user: {
-          ...session.user,
-          id: user.id,
-          subscription,
-        },
+        user: userData,
       };
     },
     signIn: async ({ user, account, profile, email, credentials }) => {

From d6da6347707aab03d9a16e1d3fedd5a6eed42f22 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Thu, 14 Nov 2024 22:57:48 -0500
Subject: [PATCH 17/31] feat: forgot pass page visible

---
 .../src/app/(auth)/forgot-password/page.tsx   | 67 +++++++++++++++++++
 1 file changed, 67 insertions(+)
 create mode 100644 ladderly-io/src/app/(auth)/forgot-password/page.tsx

diff --git a/ladderly-io/src/app/(auth)/forgot-password/page.tsx b/ladderly-io/src/app/(auth)/forgot-password/page.tsx
new file mode 100644
index 00000000..4ea1c4f1
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/forgot-password/page.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { LabeledTextField } from "~/app/core/components/LabeledTextField";
+import { Form, FORM_ERROR } from "~/app/core/components/Form";
+import { ForgotPassword } from "src/app/(auth)/schemas";
+// import forgotPassword from "src/app/(auth)/mutations/forgotPassword";
+// import { useMutation } from "@blitzjs/rpc";
+import Link from "next/link";
+
+const ForgotPasswordPage = () => {
+  // const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword);
+  const isSuccess = false;
+
+  return (
+    <div className="relative min-h-screen">
+      <nav className="border-ladderly-light-purple flex border bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
+        <Link
+          href="/"
+          className="ml-auto text-gray-800 hover:text-ladderly-pink"
+        >
+          Back to Home
+        </Link>
+      </nav>
+      <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
+        <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
+          <h1 className="mb-4 text-2xl font-bold text-gray-800">
+            Forgot your password?
+          </h1>
+
+          {isSuccess ? (
+            <div>
+              <h2>Request Submitted</h2>
+              <p>
+                If your email is in our system, you will receive instructions to
+                reset your password shortly.
+              </p>
+            </div>
+          ) : (
+            <Form
+              submitText="Send Reset Password Instructions"
+              schema={ForgotPassword}
+              initialValues={{ email: "" }}
+              onSubmit={async (values) => {
+                try {
+                  // await forgotPasswordMutation(values);
+                } catch (error: any) {
+                  return {
+                    [FORM_ERROR]:
+                      "Sorry, we had an unexpected error. Please try again.",
+                  };
+                }
+              }}
+            >
+              <LabeledTextField
+                name="email"
+                label="Email"
+                placeholder="Email"
+              />
+            </Form>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ForgotPasswordPage;

From 9dc1b16c73f30795ee98fceb4f95aff2c2599cda Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Fri, 15 Nov 2024 00:04:26 -0500
Subject: [PATCH 18/31] feat: refresh page on logout

---
 .../src/app/core/components/page-wrapper/TopNavSubmenu.tsx | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/ladderly-io/src/app/core/components/page-wrapper/TopNavSubmenu.tsx b/ladderly-io/src/app/core/components/page-wrapper/TopNavSubmenu.tsx
index e5f16822..7c325eda 100644
--- a/ladderly-io/src/app/core/components/page-wrapper/TopNavSubmenu.tsx
+++ b/ladderly-io/src/app/core/components/page-wrapper/TopNavSubmenu.tsx
@@ -75,18 +75,15 @@ export const CommunityMenuItems = ({
 
 const LogoutButton = ({ className }: { className: string }) => {
   const { setMenu } = React.useContext(MenuContext);
-  const router = useRouter();
 
   return (
     <button
       className={className}
-      onClick={async () => {
+      onClick={() => {
         setMenu(null, "");
-        const signOutResponse = await signOut({
-          redirect: false,
+        signOut({
           callbackUrl: "/",
         });
-        router.push(signOutResponse.url);
       }}
     >
       Log Out

From 80590d64ccab66eb72f39aa0a00187c2ec4fbf1e Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Fri, 15 Nov 2024 00:08:31 -0500
Subject: [PATCH 19/31] chore: install argon

---
 ladderly-io/package-lock.json | 45 +++++++++++++++++++++++++++++++++++
 ladderly-io/package.json      |  1 +
 2 files changed, 46 insertions(+)

diff --git a/ladderly-io/package-lock.json b/ladderly-io/package-lock.json
index d8be6657..b2c194db 100644
--- a/ladderly-io/package-lock.json
+++ b/ladderly-io/package-lock.json
@@ -17,6 +17,7 @@
         "@trpc/client": "^11.0.0-rc.446",
         "@trpc/react-query": "^11.0.0-rc.446",
         "@trpc/server": "^11.0.0-rc.446",
+        "argon2": "^0.41.1",
         "geist": "^1.3.0",
         "next": "^14.2.4",
         "next-auth": "^4.24.7",
@@ -856,6 +857,15 @@
         "url": "https://github.com/sponsors/panva"
       }
     },
+    "node_modules/@phc/format": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
+      "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -1459,6 +1469,21 @@
       "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
       "license": "MIT"
     },
+    "node_modules/argon2": {
+      "version": "0.41.1",
+      "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.41.1.tgz",
+      "integrity": "sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "@phc/format": "^1.0.0",
+        "node-addon-api": "^8.1.0",
+        "node-gyp-build": "^4.8.1"
+      },
+      "engines": {
+        "node": ">=16.17.0"
+      }
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -4578,6 +4603,26 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/node-addon-api": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.2.tgz",
+      "integrity": "sha512-9emqXAKhVoNrQ792nLI/wpzPpJ/bj/YXxW0CvAau1+RdGBcCRF1Dmz7719zgVsQNrzHl9Tzn3ImZ4qWFarWL0A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/node-gyp-build": {
+      "version": "4.8.3",
+      "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz",
+      "integrity": "sha512-EMS95CMJzdoSKoIiXo8pxKoL8DYxwIZXYlLmgPb8KUv794abpnLK6ynsCAWNliOjREKruYKdzbh76HHYUHX7nw==",
+      "license": "MIT",
+      "bin": {
+        "node-gyp-build": "bin.js",
+        "node-gyp-build-optional": "optional.js",
+        "node-gyp-build-test": "build-test.js"
+      }
+    },
     "node_modules/normalize-path": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
diff --git a/ladderly-io/package.json b/ladderly-io/package.json
index 69341014..398a3d90 100644
--- a/ladderly-io/package.json
+++ b/ladderly-io/package.json
@@ -23,6 +23,7 @@
     "@trpc/client": "^11.0.0-rc.446",
     "@trpc/react-query": "^11.0.0-rc.446",
     "@trpc/server": "^11.0.0-rc.446",
+    "argon2": "^0.41.1",
     "geist": "^1.3.0",
     "next": "^14.2.4",
     "next-auth": "^4.24.7",

From 214d2c039a9b03c595c2a0d46d8159919c4ae549 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 17 Nov 2024 23:56:35 -0500
Subject: [PATCH 20/31] forgot password wip

---
 ladderly-io/package-lock.json                 | 109 ++++++++++++++++++
 ladderly-io/package.json                      |   1 +
 .../(auth)/components/ForgotPasswordForm.tsx  |  45 ++++++++
 .../src/app/(auth)/forgot-password/page.tsx   |  10 +-
 ladderly-io/src/server/api/routers/auth.ts    |  20 ++++
 ladderly-io/src/server/auth.ts                |  84 ++++++++++++++
 .../server/mailers/forgotPasswordMailer.ts    |  37 ++++++
 7 files changed, 300 insertions(+), 6 deletions(-)
 create mode 100644 ladderly-io/src/app/(auth)/components/ForgotPasswordForm.tsx
 create mode 100644 ladderly-io/src/server/mailers/forgotPasswordMailer.ts

diff --git a/ladderly-io/package-lock.json b/ladderly-io/package-lock.json
index b2c194db..c74f6c2f 100644
--- a/ladderly-io/package-lock.json
+++ b/ladderly-io/package-lock.json
@@ -21,6 +21,7 @@
         "geist": "^1.3.0",
         "next": "^14.2.4",
         "next-auth": "^4.24.7",
+        "postmark": "^4.0.5",
         "react": "^18.3.1",
         "react-dom": "^18.3.1",
         "react-final-form": "^6.5.9",
@@ -1676,6 +1677,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
     "node_modules/available-typed-arrays": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1702,6 +1709,17 @@
         "node": ">=4"
       }
     },
+    "node_modules/axios": {
+      "version": "1.7.7",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+      "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
     "node_modules/axobject-query": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@@ -1912,6 +1930,18 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "license": "MIT"
     },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
     "node_modules/commander": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -2169,6 +2199,15 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
     "node_modules/didyoumean": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -3252,6 +3291,26 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/for-each": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -3278,6 +3337,20 @@
         "url": "https://github.com/sponsors/isaacs"
       }
     },
+    "node_modules/form-data": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -4399,6 +4472,27 @@
         "node": ">=8.6"
       }
     },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/minimatch": {
       "version": "9.0.5",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -5181,6 +5275,15 @@
       "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
       "license": "MIT"
     },
+    "node_modules/postmark": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.5.tgz",
+      "integrity": "sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ==",
+      "license": "MIT",
+      "dependencies": {
+        "axios": "^1.7.4"
+      }
+    },
     "node_modules/preact": {
       "version": "10.11.3",
       "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
@@ -5342,6 +5445,12 @@
         "react-is": "^16.13.1"
       }
     },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
diff --git a/ladderly-io/package.json b/ladderly-io/package.json
index 398a3d90..cc6cdf83 100644
--- a/ladderly-io/package.json
+++ b/ladderly-io/package.json
@@ -27,6 +27,7 @@
     "geist": "^1.3.0",
     "next": "^14.2.4",
     "next-auth": "^4.24.7",
+    "postmark": "^4.0.5",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-final-form": "^6.5.9",
diff --git a/ladderly-io/src/app/(auth)/components/ForgotPasswordForm.tsx b/ladderly-io/src/app/(auth)/components/ForgotPasswordForm.tsx
new file mode 100644
index 00000000..dfa3b465
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/components/ForgotPasswordForm.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import { Form, FORM_ERROR } from "~/app/core/components/Form";
+import LabeledTextField from "~/app/core/components/LabeledTextField";
+import { ForgotPassword } from "../schemas";
+import { api } from "~/trpc/react";
+
+export function ForgotPasswordForm() {
+  const forgotPasswordMutation = api.auth.forgotPassword.useMutation();
+
+  return (
+    <>
+      <h1>Forgot your password?</h1>
+      <>
+        {forgotPasswordMutation.isSuccess ? (
+          <div>
+            <h2>Request Submitted</h2>
+            <p>
+              If your email is in our system, you will receive instructions to
+              reset your password shortly.
+            </p>
+          </div>
+        ) : (
+          <Form
+            submitText="Send Reset Password Instructions"
+            schema={ForgotPassword}
+            initialValues={{ email: "" }}
+            onSubmit={async (values: any) => {
+              try {
+                await forgotPasswordMutation.mutateAsync(values);
+              } catch (error: any) {
+                return {
+                  [FORM_ERROR]:
+                    "Sorry, we had an unexpected error. Please try again.",
+                };
+              }
+            }}
+          >
+            <LabeledTextField name="email" label="Email" placeholder="Email" />
+          </Form>
+        )}
+      </>
+    </>
+  );
+}
diff --git a/ladderly-io/src/app/(auth)/forgot-password/page.tsx b/ladderly-io/src/app/(auth)/forgot-password/page.tsx
index 4ea1c4f1..9cc43e2a 100644
--- a/ladderly-io/src/app/(auth)/forgot-password/page.tsx
+++ b/ladderly-io/src/app/(auth)/forgot-password/page.tsx
@@ -3,13 +3,11 @@
 import { LabeledTextField } from "~/app/core/components/LabeledTextField";
 import { Form, FORM_ERROR } from "~/app/core/components/Form";
 import { ForgotPassword } from "src/app/(auth)/schemas";
-// import forgotPassword from "src/app/(auth)/mutations/forgotPassword";
-// import { useMutation } from "@blitzjs/rpc";
+import { api } from "~/trpc/react";
 import Link from "next/link";
 
 const ForgotPasswordPage = () => {
-  // const [forgotPasswordMutation, { isSuccess }] = useMutation(forgotPassword);
-  const isSuccess = false;
+  const forgotPasswordMutation = api.auth.forgotPassword.useMutation();
 
   return (
     <div className="relative min-h-screen">
@@ -27,7 +25,7 @@ const ForgotPasswordPage = () => {
             Forgot your password?
           </h1>
 
-          {isSuccess ? (
+          {forgotPasswordMutation.isSuccess ? (
             <div>
               <h2>Request Submitted</h2>
               <p>
@@ -42,7 +40,7 @@ const ForgotPasswordPage = () => {
               initialValues={{ email: "" }}
               onSubmit={async (values) => {
                 try {
-                  // await forgotPasswordMutation(values);
+                  await forgotPasswordMutation.mutateAsync(values);
                 } catch (error: any) {
                   return {
                     [FORM_ERROR]:
diff --git a/ladderly-io/src/server/api/routers/auth.ts b/ladderly-io/src/server/api/routers/auth.ts
index aede8f5d..062ef07e 100644
--- a/ladderly-io/src/server/api/routers/auth.ts
+++ b/ladderly-io/src/server/api/routers/auth.ts
@@ -1,6 +1,7 @@
 import { z } from "zod";
 import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
 import { TRPCError } from '@trpc/server';
+import { sendForgotPasswordEmail } from '~/server/mailers/forgotPasswordMailer';
 
 export const LoginSchema = z.object({
   email: z.string().email(),
@@ -32,4 +33,23 @@ export const authRouter = createTRPCRouter({
 
       return user;
     }),
+
+  forgotPassword: publicProcedure
+    .input(z.object({ email: z.string().email() }))
+    .mutation(async ({ ctx, input }) => {
+      const { email } = input;
+      
+      const user = await ctx.db.user.findUnique({ where: { email } });
+
+      if (!user) {
+        throw new TRPCError({
+          code: 'NOT_FOUND',
+          message: 'If your email is in our system, you will receive instructions to reset your password shortly.',
+        });
+      }
+
+      await sendForgotPasswordEmail({ to: user.email, token: "generated-token" });
+
+      return { success: true };
+    }),
 }); 
\ No newline at end of file
diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index e9bd4dbc..94b5111a 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -1,3 +1,11 @@
+/* TODO:
+ * let's abandon compatability with blitz.js passwords and simply force users to reset passwords as part of this migration.
+ * I'll mark user passwords as empty strings
+ * If the user password in the DB is empty we will trigger the forgot password flow and inform the user
+ * If the user password in the DB is populated, suggest a current best practice for node.js v20+
+ * Using the argon2 package seems fine to me.
+ */
+
 import {
   getServerSession,
   type DefaultSession,
@@ -7,11 +15,14 @@ import DiscordProvider from "next-auth/providers/discord";
 import GithubProvider from "next-auth/providers/github";
 import GoogleProvider from "next-auth/providers/google";
 import LinkedInProvider from "next-auth/providers/linkedin";
+import CredentialsProvider from "next-auth/providers/credentials";
 import { PaymentTierEnum } from "@prisma/client";
+import * as argon2 from "argon2";
 
 import { env } from "~/env";
 import { db } from "~/server/db";
 import { LadderlyMigrationAdapter } from "./LadderlyMigrationAdapter";
+import { TRPCError } from "@trpc/server";
 
 export interface LadderlySession extends DefaultSession {
   user?: {
@@ -30,6 +41,21 @@ declare module "next-auth" {
   interface Session extends LadderlySession {}
 }
 
+// Helper function to hash password
+async function hashPassword(password: string): Promise<string> {
+  return await argon2.hash(password);
+}
+
+// Helper function to verify password
+async function verifyPassword(hashedPassword: string, plaintext: string): Promise<boolean> {
+  try {
+    return await argon2.verify(hashedPassword, plaintext);
+  } catch (error) {
+    console.error('Password verification failed:', error);
+    return false;
+  }
+}
+
 export const authOptions: NextAuthOptions = {
   callbacks: {
     session: async ({ session, user }): Promise<LadderlySession> => {
@@ -131,6 +157,64 @@ export const authOptions: NextAuthOptions = {
           clientSecret: env.LINKEDIN_CLIENT_SECRET,
         })
       : null,
+    CredentialsProvider({
+      name: "Credentials",
+      credentials: {
+        email: { label: "Email", type: "email", placeholder: "your-email@example.com" },
+        password: { label: "Password", type: "password" },
+      },
+      async authorize(credentials, req) {
+        if (!credentials?.email || !credentials?.password) {
+          throw new TRPCError({
+            code: 'UNAUTHORIZED',
+            message: 'Invalid credentials',
+          });
+        }
+
+        const user = await db.user.findUnique({
+          where: { email: credentials.email },
+        });
+
+        if (!user) {
+          throw new TRPCError({
+            code: 'UNAUTHORIZED',
+            message: 'Invalid email or password',
+          });
+        }
+
+        if (!user.hashedPassword) {
+          // Trigger password reset flow
+          throw new TRPCError({
+            code: 'UNAUTHORIZED',
+            message: 'Password reset required. Please check your email to reset your password.',
+          });
+        }
+
+        try {
+          const isValid = await verifyPassword(user.hashedPassword, credentials.password);
+          
+          if (!isValid) {
+            throw new TRPCError({
+              code: 'UNAUTHORIZED',
+              message: 'Invalid email or password',
+            });
+          }
+
+          return {
+            id: user.id.toString(),
+            email: user.email,
+            name: `${user.nameFirst} ${user.nameLast}`.trim() || null,
+            image: user.image || null,
+          };
+        } catch (error) {
+          console.error('Password verification failed:', error);
+          throw new TRPCError({
+            code: 'INTERNAL_SERVER_ERROR',
+            message: 'An error occurred during authentication',
+          });
+        }
+      },
+    }),
   ].filter(Boolean) as NextAuthOptions["providers"],
 };
 
diff --git a/ladderly-io/src/server/mailers/forgotPasswordMailer.ts b/ladderly-io/src/server/mailers/forgotPasswordMailer.ts
new file mode 100644
index 00000000..3364fcb3
--- /dev/null
+++ b/ladderly-io/src/server/mailers/forgotPasswordMailer.ts
@@ -0,0 +1,37 @@
+import { ServerClient } from 'postmark';
+
+const postmarkClient = new ServerClient(
+  process.env.POSTMARK_API_KEY || ''
+);
+
+type ResetPasswordMailer = {
+  to: string;
+  token: string;
+};
+
+export async function sendForgotPasswordEmail({ to, token }: ResetPasswordMailer) {
+  const origin = process.env.APP_ORIGIN || 'http://localhost:3000'; // Default to localhost if APP_ORIGIN is not set
+  const resetUrl = `${origin}/reset-password?token=${token}`;
+
+  const html = `
+    <h1>Reset Your Password</h1>
+    <a href="${resetUrl}">
+      Click here to set a new password
+    </a>
+  `;
+
+  const msg = {
+    From: 'support@ladderly.io',
+    To: to,
+    Subject: 'Your Password Reset Instructions',
+    HtmlBody: html,
+  };
+
+  try {
+    await postmarkClient.sendEmail(msg);
+    console.log(`Password reset email sent to ${to}`);
+  } catch (error) {
+    console.error('Failed to send password reset email:', error);
+    throw new Error('Failed to send password reset email');
+  }
+} 
\ No newline at end of file

From 23a28867081b38bad9666679bd34c94c088b4a80 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Mon, 18 Nov 2024 00:19:08 -0500
Subject: [PATCH 21/31] feat: reset pass

---
 .../src/app/(auth)/components/LoginForm.tsx   | 40 +++++-------
 .../ResetPasswordClientPageClient.tsx         | 65 +++++++++++++++++++
 .../src/app/(auth)/reset-password/page.tsx    | 28 ++++++++
 ladderly-io/src/server/api/routers/auth.ts    | 63 +++++++++++++++++-
 4 files changed, 173 insertions(+), 23 deletions(-)
 create mode 100644 ladderly-io/src/app/(auth)/reset-password/ResetPasswordClientPageClient.tsx
 create mode 100644 ladderly-io/src/app/(auth)/reset-password/page.tsx

diff --git a/ladderly-io/src/app/(auth)/components/LoginForm.tsx b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
index db2ef2d4..0681b39c 100644
--- a/ladderly-io/src/app/(auth)/components/LoginForm.tsx
+++ b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
@@ -11,6 +11,23 @@ import { LabeledTextField } from "~/app/core/components/LabeledTextField";
 export const LoginForm = () => {
   const router = useRouter();
 
+  const handleSubmit = async (values: { email: string; password: string }) => {
+    const result = await signIn("credentials", {
+      redirect: false,
+      email: values.email,
+      password: values.password,
+    });
+
+    if (result?.error) {
+      return { [FORM_ERROR]: result.error };
+    }
+
+    if (result?.ok) {
+      router.push("/");
+      router.refresh();
+    }
+  };
+
   return (
     <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
       <h1 className="mb-4 text-2xl font-bold text-gray-800">Log In</h1>
@@ -41,28 +58,7 @@ export const LoginForm = () => {
         submitText="Log In with Email"
         schema={LoginSchema}
         initialValues={{ email: "", password: "" }}
-        onSubmit={async (values) => {
-          try {
-            const result = await signIn("credentials", {
-              email: values.email,
-              password: values.password,
-              redirect: false,
-            });
-
-            if (result?.error) {
-              return { [FORM_ERROR]: "Invalid email or password" };
-            }
-
-            if (result?.ok) {
-              router.push("/");
-              router.refresh();
-            }
-          } catch (error: any) {
-            return {
-              [FORM_ERROR]: "An unexpected error occurred. Please try again.",
-            };
-          }
-        }}
+        onSubmit={handleSubmit}
       >
         <LabeledTextField name="email" label="Email" placeholder="Email" />
         <LabeledTextField
diff --git a/ladderly-io/src/app/(auth)/reset-password/ResetPasswordClientPageClient.tsx b/ladderly-io/src/app/(auth)/reset-password/ResetPasswordClientPageClient.tsx
new file mode 100644
index 00000000..e84eaa0f
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/reset-password/ResetPasswordClientPageClient.tsx
@@ -0,0 +1,65 @@
+"use client";
+
+import { LabeledTextField } from "~/app/core/components/LabeledTextField";
+import { Form, FORM_ERROR } from "~/app/core/components/Form";
+import { ResetPassword } from "src/app/(auth)/schemas";
+import { api } from "~/trpc/react";
+import { useSearchParams } from "next/navigation";
+import Link from "next/link";
+
+const ResetPasswordClientPageClient = () => {
+  const searchParams = useSearchParams();
+  const token = searchParams?.get("token")?.toString();
+  const resetPasswordMutation = api.auth.resetPassword.useMutation();
+
+  return (
+    <>
+      {resetPasswordMutation.isSuccess ? (
+        <div>
+          <h2>Password Reset Successfully</h2>
+          <p>
+            Go to the <Link href="/">homepage</Link>
+          </p>
+        </div>
+      ) : (
+        <Form
+          submitText="Reset Password"
+          schema={ResetPassword}
+          initialValues={{
+            password: "",
+            passwordConfirmation: "",
+            token,
+          }}
+          onSubmit={async (values) => {
+            try {
+              if (!token) throw new Error("Token is required.");
+              await resetPasswordMutation.mutateAsync({
+                ...values,
+                token,
+              });
+            } catch (error: any) {
+              return {
+                [FORM_ERROR]:
+                  error.message ||
+                  "Sorry, we had an unexpected error. Please try again.",
+              };
+            }
+          }}
+        >
+          <LabeledTextField
+            name="password"
+            label="New Password"
+            type="password"
+          />
+          <LabeledTextField
+            name="passwordConfirmation"
+            label="Confirm New Password"
+            type="password"
+          />
+        </Form>
+      )}
+    </>
+  );
+};
+
+export default ResetPasswordClientPageClient;
diff --git a/ladderly-io/src/app/(auth)/reset-password/page.tsx b/ladderly-io/src/app/(auth)/reset-password/page.tsx
new file mode 100644
index 00000000..8a4ce2bb
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/reset-password/page.tsx
@@ -0,0 +1,28 @@
+import { Metadata } from "next";
+import ResetPasswordClientPageClient from "./ResetPasswordClientPageClient";
+
+export const metadata: Metadata = {
+  title: "Reset Password",
+};
+
+const ResetPasswordPage = () => {
+  return (
+    <div className="relative min-h-screen">
+      <nav className="border-ladderly-light-purple flex border bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
+        <a href="/" className="ml-auto text-gray-800 hover:text-ladderly-pink">
+          Back to Home
+        </a>
+      </nav>
+      <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
+        <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
+          <h1 className="mb-4 text-2xl font-bold text-gray-800">
+            Set a New Password
+          </h1>
+          <ResetPasswordClientPageClient />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ResetPasswordPage;
diff --git a/ladderly-io/src/server/api/routers/auth.ts b/ladderly-io/src/server/api/routers/auth.ts
index 062ef07e..1a41611e 100644
--- a/ladderly-io/src/server/api/routers/auth.ts
+++ b/ladderly-io/src/server/api/routers/auth.ts
@@ -2,6 +2,8 @@ import { z } from "zod";
 import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
 import { TRPCError } from '@trpc/server';
 import { sendForgotPasswordEmail } from '~/server/mailers/forgotPasswordMailer';
+import crypto from 'crypto';
+import * as argon2 from 'argon2';
 
 export const LoginSchema = z.object({
   email: z.string().email(),
@@ -48,7 +50,66 @@ export const authRouter = createTRPCRouter({
         });
       }
 
-      await sendForgotPasswordEmail({ to: user.email, token: "generated-token" });
+      const token = crypto.randomBytes(32).toString('hex');
+      const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
+      const thirtyMinutes = 30 * 60 * 1000;
+
+      await ctx.db.token.create({
+        data: {
+          userId: user.id,
+          hashedToken,
+          type: 'RESET_PASSWORD',
+          expiresAt: new Date(Date.now() + thirtyMinutes),
+          sentTo: user.email,
+        },
+      });
+
+      await sendForgotPasswordEmail({ to: user.email, token });
+
+      return { success: true };
+    }),
+
+  resetPassword: publicProcedure
+    .input(z.object({
+      token: z.string(),
+      password: z.string().min(8),
+      passwordConfirmation: z.string().min(8),
+    }))
+    .mutation(async ({ ctx, input }) => {
+      const { token, password, passwordConfirmation } = input;
+
+      if (password !== passwordConfirmation) {
+        throw new TRPCError({
+          code: 'BAD_REQUEST',
+          message: 'Passwords do not match',
+        });
+      }
+
+      const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
+
+      const tokenRecord = await ctx.db.token.findFirst({
+        where: {
+          hashedToken,
+          type: 'RESET_PASSWORD',
+          expiresAt: { gt: new Date() },
+        },
+      });
+
+      if (!tokenRecord) {
+        throw new TRPCError({
+          code: 'BAD_REQUEST',
+          message: 'Invalid or expired token',
+        });
+      }
+
+      const hashedPassword = await argon2.hash(password);
+
+      await ctx.db.user.update({
+        where: { id: tokenRecord.userId },
+        data: { hashedPassword },
+      });
+
+      await ctx.db.token.delete({ where: { id: tokenRecord.id } });
 
       return { success: true };
     }),

From afb07041118c975133956227b65b7e068975e113 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sat, 30 Nov 2024 13:45:38 -0500
Subject: [PATCH 22/31] feat: better signin logging

---
 ladderly-io/src/server/api/routers/user.ts |  5 +++--
 ladderly-io/src/server/auth.ts             | 20 ++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index 9f004363..590d2b92 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -68,13 +68,14 @@ export type UserSettings = z.infer<typeof UserSettingsSchema>;
 
 export const userRouter = createTRPCRouter({
   getCurrentUser: publicProcedure.query(async ({ ctx }) => {
-    if (!ctx.session) {
+    const strId = ctx?.session?.user?.id;
+    if (!strId) {
       return null;
     }
 
     const user = await ctx.db.user.findUnique({
       where: {
-        id: parseInt(ctx.session.user.id),
+        id: parseInt(strId),
       },
       include: {
         subscriptions: true
diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index 94b5111a..a14972b0 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -58,7 +58,12 @@ async function verifyPassword(hashedPassword: string, plaintext: string): Promis
 
 export const authOptions: NextAuthOptions = {
   callbacks: {
+    // TODO: this seems to work with google login, but not with credentials login
     session: async ({ session, user }): Promise<LadderlySession> => {
+      console.log("Session Callback Triggered"); // TODO: delete this log
+      console.log("Session:", session); // TODO: delete this log
+      console.log("User:", user); // TODO: delete this log
+
       const dbUser = await db.user.findUnique({
         where: { id: parseInt(user.id) },
         include: {
@@ -69,6 +74,8 @@ export const authOptions: NextAuthOptions = {
         },
       });
 
+      console.log("Database User:", dbUser); // TODO: delete this log
+
       const subscription = dbUser?.subscriptions[0] || {
         tier: PaymentTierEnum.FREE,
         type: 'ACCOUNT_PLAN',
@@ -84,26 +91,31 @@ export const authOptions: NextAuthOptions = {
           }
         : undefined;
 
+      console.log("User Data for Session:", userData); // TODO: delete this log
+
       return {
         ...session,
         user: userData,
       };
     },
     signIn: async ({ user, account, profile, email, credentials }) => {
-      // If the user exists but doesn't have an account for this provider
       if (account?.provider && user.email) {
         const existingUser = await db.user.findUnique({
           where: { email: user.email },
           include: { accounts: true },
         });
 
+        console.log("Existing User:", existingUser); // TODO: delete this log
+
         if (existingUser) {
           const existingAccount = existingUser.accounts.find(
             (acc) => acc.provider === account.provider
           );
 
+          console.log("Existing Account:", existingAccount); // TODO: delete this log
+
           if (!existingAccount) {
-            await db.account.create({
+            const newAccount = await db.account.create({
               data: {
                 userId: existingUser.id,
                 type: account.type,
@@ -117,9 +129,13 @@ export const authOptions: NextAuthOptions = {
                 session_state: account.session_state,
               },
             });
+            console.log(`New Account Created with User ID: ${existingUser.id} and Account ID: ${newAccount.id}`);
           }
 
           return true;
+        } else {
+          console.log(`User not found by email with User ID: ${user.id}`);
+          return false;
         }
       }
 

From a7903b506af174b73aea28fe15659b43188461eb Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sat, 30 Nov 2024 13:46:38 -0500
Subject: [PATCH 23/31] feat: broken signup migration

---
 .../src/app/(auth)/components/SignupForm.tsx  | 64 +++++++++++++++++++
 ladderly-io/src/app/(auth)/signup/page.tsx    | 32 ++++++++++
 2 files changed, 96 insertions(+)
 create mode 100644 ladderly-io/src/app/(auth)/components/SignupForm.tsx
 create mode 100644 ladderly-io/src/app/(auth)/signup/page.tsx

diff --git a/ladderly-io/src/app/(auth)/components/SignupForm.tsx b/ladderly-io/src/app/(auth)/components/SignupForm.tsx
new file mode 100644
index 00000000..a236eb67
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/components/SignupForm.tsx
@@ -0,0 +1,64 @@
+/* TODO: migrate from blitzjs to tRPC */
+
+"use client";
+import { useMutation } from "@blitzjs/rpc";
+import Link from "next/link";
+import signup from "src/app/(auth)/mutations/signup";
+import { Signup } from "src/app/(auth)/schemas";
+import { Form, FORM_ERROR } from "src/core/components/Form";
+import { LabeledTextField } from "src/core/components/LabeledTextField";
+
+type SignupFormProps = {
+  onSuccess?: () => void;
+};
+
+export const SignupForm = (props: SignupFormProps) => {
+  const [signupMutation] = useMutation(signup);
+  return (
+    <div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
+      <h1 className="mb-4 text-2xl font-bold text-gray-800">
+        Create an Account
+      </h1>
+
+      <Form
+        submitText="Create Account"
+        schema={Signup}
+        initialValues={{ email: "", password: "" }}
+        className="space-y-4"
+        onSubmit={async (values) => {
+          try {
+            await signupMutation(values);
+            props.onSuccess?.();
+          } catch (error: any) {
+            if (
+              error.code === "P2002" &&
+              error.meta?.target?.includes("email")
+            ) {
+              // This error comes from Prisma
+              return { email: "This email is already being used" };
+            } else {
+              return { [FORM_ERROR]: error.toString() };
+            }
+          }
+        }}
+      >
+        <LabeledTextField name="email" label="Email" placeholder="Email" />
+        <LabeledTextField
+          name="password"
+          label="Password"
+          placeholder="Password"
+          type="password"
+        />
+      </Form>
+
+      <div className="mt-4">
+        Already signed up?{" "}
+        <Link className="underline" href="/login">
+          Log in here!
+        </Link>
+      </div>
+    </div>
+  );
+};
+
+export default SignupForm;
diff --git a/ladderly-io/src/app/(auth)/signup/page.tsx b/ladderly-io/src/app/(auth)/signup/page.tsx
new file mode 100644
index 00000000..56d2f919
--- /dev/null
+++ b/ladderly-io/src/app/(auth)/signup/page.tsx
@@ -0,0 +1,32 @@
+/* TODO: migrate from blitzjs to tRPC */
+
+"use client";
+import { BlitzPage } from "@blitzjs/next";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { SignupForm } from "src/app/(auth)/components/SignupForm";
+import Layout from "src/core/layouts/Layout";
+
+const CreateAccountPage: BlitzPage = () => {
+  const router = useRouter();
+
+  return (
+    <Layout title="Create Account">
+      <div className="relative min-h-screen">
+        <nav className="border-ladderly-light-purple flex border bg-ladderly-off-white px-4 py-1 text-ladderly-violet-700">
+          <Link
+            href="/"
+            className="ml-auto text-gray-800 hover:text-ladderly-pink"
+          >
+            Back to Home
+          </Link>
+        </nav>
+        <div className="flex min-h-[calc(100vh-4rem)] items-center justify-center">
+          <SignupForm onSuccess={() => router.push("/")} />
+        </div>
+      </div>
+    </Layout>
+  );
+};
+
+export default CreateAccountPage;

From a4a0fd2e2c1d21e3843dff3a58d4abcf8dd1506a Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 14:29:16 -0500
Subject: [PATCH 24/31] feat: explicit jwt strategy

---
 ladderly-io/src/server/auth.ts | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index a14972b0..2de65664 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -41,11 +41,6 @@ declare module "next-auth" {
   interface Session extends LadderlySession {}
 }
 
-// Helper function to hash password
-async function hashPassword(password: string): Promise<string> {
-  return await argon2.hash(password);
-}
-
 // Helper function to verify password
 async function verifyPassword(hashedPassword: string, plaintext: string): Promise<boolean> {
   try {
@@ -57,6 +52,9 @@ async function verifyPassword(hashedPassword: string, plaintext: string): Promis
 }
 
 export const authOptions: NextAuthOptions = {
+  session: {
+    strategy: "jwt",
+  },
   callbacks: {
     // TODO: this seems to work with google login, but not with credentials login
     session: async ({ session, user }): Promise<LadderlySession> => {
@@ -98,6 +96,10 @@ export const authOptions: NextAuthOptions = {
         user: userData,
       };
     },
+    jwt: async ({ token, user, account, profile }) => {
+      // TODO: implement or remove jwt callback
+      return token;
+    },
     signIn: async ({ user, account, profile, email, credentials }) => {
       if (account?.provider && user.email) {
         const existingUser = await db.user.findUnique({
@@ -132,13 +134,13 @@ export const authOptions: NextAuthOptions = {
             console.log(`New Account Created with User ID: ${existingUser.id} and Account ID: ${newAccount.id}`);
           }
 
-          return true;
         } else {
           console.log(`User not found by email with User ID: ${user.id}`);
           return false;
         }
       }
 
+      console.log("signIn callback completed successfully"); // TODO: delete this log
       return true;
     },
   },

From 2c59f0f89a96df878d417c3caa74d9d57790be62 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 14:40:26 -0500
Subject: [PATCH 25/31] fix: TRPCClientError: Invalid response or stream
 interrupted

---
 ladderly-io/src/server/api/routers/user.ts | 5 ++++-
 ladderly-io/src/server/constants.ts        | 5 +++++
 2 files changed, 9 insertions(+), 1 deletion(-)
 create mode 100644 ladderly-io/src/server/constants.ts

diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index 590d2b92..78d24664 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -1,3 +1,5 @@
+// src/server/api/routers/user.ts
+
 import {
   createTRPCRouter,
   protectedProcedure,
@@ -6,6 +8,7 @@ import {
 import { PaymentTierEnum } from "@prisma/client";
 import { z } from "zod";
 import { TRPCError } from '@trpc/server';
+import { NULL_RESULT_TRPC_INT } from "~/server/constants";
 
 const tiersOrder = {
   FREE: 0,
@@ -70,7 +73,7 @@ export const userRouter = createTRPCRouter({
   getCurrentUser: publicProcedure.query(async ({ ctx }) => {
     const strId = ctx?.session?.user?.id;
     if (!strId) {
-      return null;
+      return NULL_RESULT_TRPC_INT;
     }
 
     const user = await ctx.db.user.findUnique({
diff --git a/ladderly-io/src/server/constants.ts b/ladderly-io/src/server/constants.ts
new file mode 100644
index 00000000..316f4bba
--- /dev/null
+++ b/ladderly-io/src/server/constants.ts
@@ -0,0 +1,5 @@
+// src/server/constants.ts
+
+// return 0 instead of null to avoid `TRPCClientError: Invalid response or stream interrupted`
+// ref: https://github.com/trpc/trpc/discussions/5919#discussioncomment-10172462
+export const NULL_RESULT_TRPC_INT = 0;

From 2efbedfdb2ff57cfeb6a234746d8fdbb024ad6c7 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 15:07:34 -0500
Subject: [PATCH 26/31] feat: user lookup by email

---
 ladderly-io/src/server/auth.ts | 22 +++++-----------------
 1 file changed, 5 insertions(+), 17 deletions(-)

diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index 2de65664..4f1ded31 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -56,14 +56,9 @@ export const authOptions: NextAuthOptions = {
     strategy: "jwt",
   },
   callbacks: {
-    // TODO: this seems to work with google login, but not with credentials login
-    session: async ({ session, user }): Promise<LadderlySession> => {
-      console.log("Session Callback Triggered"); // TODO: delete this log
-      console.log("Session:", session); // TODO: delete this log
-      console.log("User:", user); // TODO: delete this log
-
+    session: async ({ session }: { session: DefaultSession }): Promise<LadderlySession> => {
       const dbUser = await db.user.findUnique({
-        where: { id: parseInt(user.id) },
+        where: { email: session.user?.email ?? '' },
         include: {
           subscriptions: {
             where: { type: 'ACCOUNT_PLAN' },
@@ -72,8 +67,6 @@ export const authOptions: NextAuthOptions = {
         },
       });
 
-      console.log("Database User:", dbUser); // TODO: delete this log
-
       const subscription = dbUser?.subscriptions[0] || {
         tier: PaymentTierEnum.FREE,
         type: 'ACCOUNT_PLAN',
@@ -84,13 +77,11 @@ export const authOptions: NextAuthOptions = {
             email: dbUser.email,
             name: dbUser.name,
             image: dbUser.image,
-            id: user.id,
+            id: dbUser.id.toString(),
             subscription,
           }
         : undefined;
 
-      console.log("User Data for Session:", userData); // TODO: delete this log
-
       return {
         ...session,
         user: userData,
@@ -101,21 +92,19 @@ export const authOptions: NextAuthOptions = {
       return token;
     },
     signIn: async ({ user, account, profile, email, credentials }) => {
+      // signIn is called by both social login and credentials login
+
       if (account?.provider && user.email) {
         const existingUser = await db.user.findUnique({
           where: { email: user.email },
           include: { accounts: true },
         });
 
-        console.log("Existing User:", existingUser); // TODO: delete this log
-
         if (existingUser) {
           const existingAccount = existingUser.accounts.find(
             (acc) => acc.provider === account.provider
           );
 
-          console.log("Existing Account:", existingAccount); // TODO: delete this log
-
           if (!existingAccount) {
             const newAccount = await db.account.create({
               data: {
@@ -140,7 +129,6 @@ export const authOptions: NextAuthOptions = {
         }
       }
 
-      console.log("signIn callback completed successfully"); // TODO: delete this log
       return true;
     },
   },

From 816b6ec6615c958b9d63e7c197cf4513009be108 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 15:39:01 -0500
Subject: [PATCH 27/31] feat: jwt()

---
 ladderly-io/src/app/home/HomePageContent.tsx |  2 +-
 ladderly-io/src/server/auth.ts               | 73 +++++++++++---------
 2 files changed, 43 insertions(+), 32 deletions(-)

diff --git a/ladderly-io/src/app/home/HomePageContent.tsx b/ladderly-io/src/app/home/HomePageContent.tsx
index b2ddf181..49871357 100644
--- a/ladderly-io/src/app/home/HomePageContent.tsx
+++ b/ladderly-io/src/app/home/HomePageContent.tsx
@@ -29,7 +29,7 @@ const AdvancedChecklistContentBlock = ({
 }: {
   session: LadderlySession | null;
 }) => {
-  const isPaid = session?.user.subscription.tier !== PaymentTierEnum.FREE;
+  const isPaid = session?.user?.subscription.tier !== PaymentTierEnum.FREE;
 
   return isPaid ? (
     <div
diff --git a/ladderly-io/src/server/auth.ts b/ladderly-io/src/server/auth.ts
index 4f1ded31..3635f647 100644
--- a/ladderly-io/src/server/auth.ts
+++ b/ladderly-io/src/server/auth.ts
@@ -8,6 +8,7 @@
 
 import {
   getServerSession,
+  Session,
   type DefaultSession,
   type NextAuthOptions,
 } from "next-auth";
@@ -23,6 +24,7 @@ import { env } from "~/env";
 import { db } from "~/server/db";
 import { LadderlyMigrationAdapter } from "./LadderlyMigrationAdapter";
 import { TRPCError } from "@trpc/server";
+import { JWT } from "next-auth/jwt";
 
 export interface LadderlySession extends DefaultSession {
   user?: {
@@ -56,40 +58,49 @@ export const authOptions: NextAuthOptions = {
     strategy: "jwt",
   },
   callbacks: {
-    session: async ({ session }: { session: DefaultSession }): Promise<LadderlySession> => {
-      const dbUser = await db.user.findUnique({
-        where: { email: session.user?.email ?? '' },
-        include: {
-          subscriptions: {
-            where: { type: 'ACCOUNT_PLAN' },
-            select: { tier: true, type: true },
+    jwt: async ({ token, user, account }) => {
+      // Initial sign in
+      if (account && user) {
+        token.id = user.id;
+        token.email = user.email;
+        token.name = user.name;
+        token.picture = user.image;
+        // Add any additional user data you want to include in the token
+        const dbUser = await db.user.findUnique({
+          where: { id: parseInt(user.id) },
+          include: {
+            subscriptions: {
+              where: { type: 'ACCOUNT_PLAN' },
+              select: { tier: true, type: true },
+            },
           },
-        },
-      });
-
-      const subscription = dbUser?.subscriptions[0] || {
-        tier: PaymentTierEnum.FREE,
-        type: 'ACCOUNT_PLAN',
-      };
-
-      const userData = dbUser
-        ? {
-            email: dbUser.email,
-            name: dbUser.name,
-            image: dbUser.image,
-            id: dbUser.id.toString(),
-            subscription,
-          }
-        : undefined;
-
-      return {
+        });
+        token.subscription = dbUser?.subscriptions[0] || {
+          tier: PaymentTierEnum.FREE,
+          type: 'ACCOUNT_PLAN',
+        };
+      }
+      return token;
+    },
+    session: async ({ session, token }: { session: Session, token: JWT }): Promise<LadderlySession> => {
+      // jwt() is executed first then session()
+      const user = session.user as LadderlySession['user'];
+      const userId = user?.id?.toString() || token.id?.toString() || null;
+      const newSession: LadderlySession = {
         ...session,
-        user: userData,
+        user: userId ? {
+          id: userId,
+          email: session.user?.email || token.email?.toString() || null,
+          name: token.name?.toString() || null,
+          image: token.picture?.toString() || null,
+          subscription: token.subscription as {
+            tier: PaymentTierEnum;
+            type: string;
+          },
+        } : undefined,
       };
-    },
-    jwt: async ({ token, user, account, profile }) => {
-      // TODO: implement or remove jwt callback
-      return token;
+
+      return newSession;
     },
     signIn: async ({ user, account, profile, email, credentials }) => {
       // signIn is called by both social login and credentials login

From 1f93c1e2816284f4c4ade91d73bbcad5a151b287 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 15:55:10 -0500
Subject: [PATCH 28/31] feat: getCurrentUser by email

---
 ladderly-io/src/server/api/routers/user.ts | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/ladderly-io/src/server/api/routers/user.ts b/ladderly-io/src/server/api/routers/user.ts
index 78d24664..61c9f14f 100644
--- a/ladderly-io/src/server/api/routers/user.ts
+++ b/ladderly-io/src/server/api/routers/user.ts
@@ -71,14 +71,15 @@ export type UserSettings = z.infer<typeof UserSettingsSchema>;
 
 export const userRouter = createTRPCRouter({
   getCurrentUser: publicProcedure.query(async ({ ctx }) => {
-    const strId = ctx?.session?.user?.id;
-    if (!strId) {
+    const email = ctx?.session?.user?.email;
+
+    if (!email) {
       return NULL_RESULT_TRPC_INT;
     }
 
     const user = await ctx.db.user.findUnique({
       where: {
-        id: parseInt(strId),
+        email,
       },
       include: {
         subscriptions: true

From 40cf590ca93cc105346ab00425218d827ee17084 Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 16:03:09 -0500
Subject: [PATCH 29/31] feat: better adapter types

---
 .../src/server/LadderlyMigrationAdapter.ts    | 20 +++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/ladderly-io/src/server/LadderlyMigrationAdapter.ts b/ladderly-io/src/server/LadderlyMigrationAdapter.ts
index c1cabf2e..22e0f345 100644
--- a/ladderly-io/src/server/LadderlyMigrationAdapter.ts
+++ b/ladderly-io/src/server/LadderlyMigrationAdapter.ts
@@ -4,12 +4,14 @@ import {
   AdapterAccount,
   AdapterSession,
   AdapterUser,
-} from "next-auth/adapters";
-import { ProviderType } from "next-auth/providers/index";
+  VerificationToken,
+} from "@auth/core/adapters";
+
+import { AdapterAccountType } from "@auth/core/adapters";
 
 export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
   return {
-    createUser: async (data) => {
+    createUser: async (data: AdapterUser) => {
       const prismaUserData: Prisma.UserCreateInput = {
         name: data.name ?? undefined,
         email: data.email,
@@ -60,7 +62,7 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
       });
       return adaptUser(user);
     },
-    linkAccount: async (account) => {
+    linkAccount: async (account: AdapterAccount) => {
       const prismaAccountData: Prisma.AccountCreateInput = {
         provider: account.provider,
         type: account.type,
@@ -70,7 +72,7 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
         token_type: account.token_type,
         scope: account.scope,
         id_token: account.id_token,
-        session_state: account.session_state,
+        session_state: account.session_state?.toString() ?? undefined,
         refresh_token: account.refresh_token,
         user: {
           connect: { id: parseInt(account.userId) },
@@ -90,6 +92,7 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
       return account ? adaptAccount(account) : undefined;
     },
     createSession: async (data) => {
+      console.log("ENTERED CREATE SESSION WITH DATA:", data);
       const session = await prisma.session.create({
         data: {
           ...data,
@@ -105,6 +108,7 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
         where: { sessionToken },
         include: { user: true },
       });
+
       return result?.user
         ? { session: adaptSession(result), user: adaptUser(result.user) }
         : null;
@@ -127,7 +131,7 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
     deleteSession: async (sessionToken) => {
       await prisma.session.delete({ where: { sessionToken } });
     },
-    createVerificationToken: (data) =>
+    createVerificationToken: (data: VerificationToken) =>
       prisma.verificationToken.create({ data }),
     useVerificationToken: (identifier_token) =>
       prisma.verificationToken.delete({ where: { identifier_token } }),
@@ -155,13 +159,13 @@ function adaptUser(user: User): AdapterUser {
 function adaptAccount(account: Account): AdapterAccount {
   return {
     userId: account.userId.toString(),
-    type: account.type as ProviderType,
+    type: account.type as AdapterAccountType,
     provider: account.provider,
     providerAccountId: account.providerAccountId,
     refresh_token: account.refresh_token ?? undefined,
     access_token: account.access_token ?? undefined,
     expires_at: account.expires_at ?? undefined,
-    token_type: account.token_type ?? undefined,
+    token_type: account.token_type as Lowercase<string> ?? undefined,
     scope: account.scope ?? undefined,
     id_token: account.id_token ?? undefined,
     session_state: account.session_state ?? undefined,

From 3f3b682f681fbe5409c61867869b7e5b697cf9ba Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Sun, 1 Dec 2024 16:03:27 -0500
Subject: [PATCH 30/31] feat: simpler adapter

---
 .../src/server/LadderlyMigrationAdapter.ts    | 22 -------------------
 1 file changed, 22 deletions(-)

diff --git a/ladderly-io/src/server/LadderlyMigrationAdapter.ts b/ladderly-io/src/server/LadderlyMigrationAdapter.ts
index 22e0f345..a44c663c 100644
--- a/ladderly-io/src/server/LadderlyMigrationAdapter.ts
+++ b/ladderly-io/src/server/LadderlyMigrationAdapter.ts
@@ -91,28 +91,6 @@ export function LadderlyMigrationAdapter(prisma: PrismaClient): Adapter {
       });
       return account ? adaptAccount(account) : undefined;
     },
-    createSession: async (data) => {
-      console.log("ENTERED CREATE SESSION WITH DATA:", data);
-      const session = await prisma.session.create({
-        data: {
-          ...data,
-          handle: data.sessionToken,
-          expiresAt: data.expires,
-          userId: parseInt(data.userId),
-        },
-      });
-      return adaptSession(session);
-    },
-    getSessionAndUser: async (sessionToken) => {
-      const result = await prisma.session.findUnique({
-        where: { sessionToken },
-        include: { user: true },
-      });
-
-      return result?.user
-        ? { session: adaptSession(result), user: adaptUser(result.user) }
-        : null;
-    },
     updateSession: async (data) => {
       const { sessionToken, ...updateData } = data;
 

From bdc28ef31817f59682894e1c463c6bd8c19724db Mon Sep 17 00:00:00 2001
From: Vandivier <john@ladderly.io>
Date: Thu, 5 Dec 2024 00:31:07 -0500
Subject: [PATCH 31/31] feat: refresh session after login

---
 .../src/app/(auth)/components/LoginForm.tsx   |  2 +-
 .../components/page-wrapper/TopNavRight.tsx   | 20 ++++++++++++++++++-
 2 files changed, 20 insertions(+), 2 deletions(-)

diff --git a/ladderly-io/src/app/(auth)/components/LoginForm.tsx b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
index 0681b39c..a5edf60d 100644
--- a/ladderly-io/src/app/(auth)/components/LoginForm.tsx
+++ b/ladderly-io/src/app/(auth)/components/LoginForm.tsx
@@ -23,7 +23,7 @@ export const LoginForm = () => {
     }
 
     if (result?.ok) {
-      router.push("/");
+      router.push("/?refresh_current_user=true");
       router.refresh();
     }
   };
diff --git a/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx b/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
index 73560756..f128b34d 100644
--- a/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
+++ b/ladderly-io/src/app/core/components/page-wrapper/TopNavRight.tsx
@@ -1,5 +1,7 @@
+import { useRouter } from "next/navigation";
+import { useSearchParams } from "next/navigation";
 import Link from "next/link";
-import React from "react";
+import React, { useEffect } from "react";
 import { api } from "~/trpc/react";
 import { IconVerticalChevron } from "../icons/VerticalChevron";
 import { MenuContext } from "./MenuProvider";
@@ -13,10 +15,26 @@ import {
 const TOP_NAV_RIGHT_SECTION_CLASSES = "ml-auto flex items-center space-x-6";
 
 export const TopNavRight = () => {
+  const router = useRouter();
+  const searchParams = useSearchParams();
   const currentUserQuery = api.user.getCurrentUser.useQuery();
   const currentUser = currentUserQuery.data;
   const { setMenu, openMenuName } = React.useContext(MenuContext);
 
+  useEffect(() => {
+    const refreshCurrentUser = searchParams.get("refresh_current_user");
+
+    if (refreshCurrentUser === "true") {
+      // Remove the query parameter and refetch data
+      const newQuery = new URLSearchParams(searchParams);
+      newQuery.delete("refresh_current_user");
+      router.replace(`?${newQuery.toString()}`);
+
+      // Refetch the current user
+      currentUserQuery.refetch();
+    }
+  }, [searchParams, currentUserQuery, router]);
+
   const handleCommunityClick = (e: React.MouseEvent<HTMLButtonElement>) => {
     e.preventDefault();
     if (openMenuName === "community") {