From 93049e864ae6d281b60c23dee868bca3f585dd4a Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Thu, 10 Jul 2025 08:35:32 +0000
Subject: feat: Add support for email verification

---
 apps/web/app/verify-email/page.tsx | 152 +++++++++++++++++++++++++++++++++++++
 1 file changed, 152 insertions(+)
 create mode 100644 apps/web/app/verify-email/page.tsx

(limited to 'apps/web/app/verify-email/page.tsx')

diff --git a/apps/web/app/verify-email/page.tsx b/apps/web/app/verify-email/page.tsx
new file mode 100644
index 00000000..e8792465
--- /dev/null
+++ b/apps/web/app/verify-email/page.tsx
@@ -0,0 +1,152 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import { api } from "@/lib/trpc";
+import { CheckCircle, Loader2, XCircle } from "lucide-react";
+
+export default function VerifyEmailPage() {
+  const searchParams = useSearchParams();
+  const router = useRouter();
+  const [status, setStatus] = useState<"loading" | "success" | "error">(
+    "loading",
+  );
+  const [message, setMessage] = useState("");
+
+  const token = searchParams.get("token");
+  const email = searchParams.get("email");
+
+  const verifyEmailMutation = api.users.verifyEmail.useMutation({
+    onSuccess: () => {
+      setStatus("success");
+      setMessage(
+        "Your email has been successfully verified! You can now sign in.",
+      );
+    },
+    onError: (error) => {
+      setStatus("error");
+      setMessage(
+        error.message ||
+          "Failed to verify email. The link may be invalid or expired.",
+      );
+    },
+  });
+
+  const resendEmailMutation = api.users.resendVerificationEmail.useMutation({
+    onSuccess: () => {
+      setMessage(
+        "A new verification email has been sent to your email address.",
+      );
+    },
+    onError: (error) => {
+      setMessage(error.message || "Failed to resend verification email.");
+    },
+  });
+
+  useEffect(() => {
+    if (token && email) {
+      verifyEmailMutation.mutate({ token, email });
+    } else {
+      setStatus("error");
+      setMessage("Invalid verification link. Missing token or email.");
+    }
+  }, [token, email]);
+
+  const handleResendEmail = () => {
+    if (email) {
+      resendEmailMutation.mutate({ email });
+    }
+  };
+
+  const handleSignIn = () => {
+    router.push("/signin");
+  };
+
+  return (
+    <div className="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
+      <Card className="w-full max-w-md">
+        <CardHeader className="text-center">
+          <CardTitle className="text-2xl font-bold">
+            Email Verification
+          </CardTitle>
+          <CardDescription>
+            {status === "loading" && "Verifying your email address..."}
+            {status === "success" && "Email verified successfully!"}
+            {status === "error" && "Verification failed"}
+          </CardDescription>
+        </CardHeader>
+        <CardContent className="space-y-4">
+          {status === "loading" && (
+            <div className="flex items-center justify-center">
+              <Loader2 className="h-8 w-8 animate-spin text-blue-600" />
+            </div>
+          )}
+
+          {status === "success" && (
+            <>
+              <div className="flex items-center justify-center">
+                <CheckCircle className="h-12 w-12 text-green-600" />
+              </div>
+              <Alert>
+                <AlertDescription className="text-center">
+                  {message}
+                </AlertDescription>
+              </Alert>
+              <Button onClick={handleSignIn} className="w-full">
+                Sign In
+              </Button>
+            </>
+          )}
+
+          {status === "error" && (
+            <>
+              <div className="flex items-center justify-center">
+                <XCircle className="h-12 w-12 text-red-600" />
+              </div>
+              <Alert variant="destructive">
+                <AlertDescription className="text-center">
+                  {message}
+                </AlertDescription>
+              </Alert>
+              {email && (
+                <div className="space-y-2">
+                  <Button
+                    onClick={handleResendEmail}
+                    variant="outline"
+                    className="w-full"
+                    disabled={resendEmailMutation.isPending}
+                  >
+                    {resendEmailMutation.isPending ? (
+                      <>
+                        <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                        Sending...
+                      </>
+                    ) : (
+                      "Resend Verification Email"
+                    )}
+                  </Button>
+                  <Button
+                    onClick={handleSignIn}
+                    variant="ghost"
+                    className="w-full"
+                  >
+                    Back to Sign In
+                  </Button>
+                </div>
+              )}
+            </>
+          )}
+        </CardContent>
+      </Card>
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

