From 70134b49672506a824336571b897e69480163a17 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Fri, 17 Apr 2026 09:56:06 -0500 Subject: [PATCH] Polish login and signup UX: fix stale copy, add password toggles, strength meter, invite context, terms footer Made-with: Cursor --- src/app/(auth)/login/page.tsx | 16 ++++-- src/app/(auth)/signup/page.tsx | 101 +++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 17 deletions(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index c532932..7bc104d 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -2,7 +2,7 @@ import { Suspense, useState } from "react"; import { signIn } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; +import { useSearchParams } from "next/navigation"; import Link from "next/link"; import { ScanLine, Mail, Lock, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -19,7 +19,6 @@ export default function LoginPage() { } function LoginForm() { - const router = useRouter(); const searchParams = useSearchParams(); const callbackUrl = searchParams.get("callbackUrl") || "/"; const error = searchParams.get("error"); @@ -47,8 +46,7 @@ function LoginForm() { setFormError("Invalid email or password"); setLoading(false); } else { - router.push(callbackUrl); - router.refresh(); + window.location.href = callbackUrl; } } @@ -60,7 +58,7 @@ function LoginForm() {

Welcome back

- Sign in to your Echo account + Digitize your connect cards

@@ -139,9 +137,15 @@ function LoginForm() {

Don't have an account?{" "} - Sign up with an invite + Sign up

+

+ By signing in, you agree to our{" "} + Terms + {" "}and{" "} + Privacy Policy. +

); } diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 3dc1445..93f8f14 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,10 +1,10 @@ "use client"; -import { Suspense, useState } from "react"; +import { Suspense, useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; import { signIn } from "next-auth/react"; import Link from "next/link"; -import { ScanLine, Mail, Lock, User, Loader2 } from "lucide-react"; +import { ScanLine, Mail, Lock, User, Loader2, Eye, EyeOff } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -17,9 +17,23 @@ export default function SignupPage() { ); } +function getPasswordStrength(pw: string): { level: number; label: string } { + if (!pw || pw.length < 8) return { level: 0, label: "Weak" }; + const hasUpper = /[A-Z]/.test(pw); + const hasLower = /[a-z]/.test(pw); + const hasNumber = /\d/.test(pw); + if (pw.length >= 12 && hasUpper && hasLower && hasNumber) { + return { level: 2, label: "Strong" }; + } + return { level: 1, label: "Fair" }; +} + +const strengthColors = ["bg-red-500", "bg-amber-500", "bg-emerald-500"]; + function SignupForm() { const searchParams = useSearchParams(); const token = searchParams.get("token") || ""; + const callbackUrl = searchParams.get("callbackUrl") || "/"; const [formData, setFormData] = useState({ displayName: "", @@ -29,6 +43,26 @@ function SignupForm() { }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [inviteInfo, setInviteInfo] = useState<{ + orgName: string; + email: string; + role: string; + } | null>(null); + + useEffect(() => { + if (!token) return; + fetch(`/api/invitations/verify?token=${token}`) + .then((r) => r.json()) + .then((data) => { + if (data.valid) { + setInviteInfo({ orgName: data.orgName, email: data.email, role: data.role }); + setFormData((prev) => ({ ...prev, email: data.email })); + } + }) + .catch(() => {}); + }, [token]); function update(field: string, value: string) { setFormData((prev) => ({ ...prev, [field]: value })); @@ -72,7 +106,7 @@ function SignupForm() { await signIn("credentials", { email: formData.email, password: formData.password, - callbackUrl: "/", + callbackUrl, }); } catch { setError("Something went wrong. Please try again."); @@ -80,6 +114,9 @@ function SignupForm() { } } + const strength = getPasswordStrength(formData.password); + const showStrength = formData.password.length > 0; + return (
@@ -88,9 +125,11 @@ function SignupForm() {

Create account

- {token - ? "Complete your account setup" - : "Get started with Echo"} + {inviteInfo + ? <>Join {inviteInfo.orgName} + : token + ? "Complete your account setup" + : "Get started with Echo"}

@@ -129,7 +168,8 @@ function SignupForm() { onChange={(e) => update("email", e.target.value)} required autoComplete="email" - className="pl-10" + readOnly={!!inviteInfo} + className={`pl-10 ${inviteInfo ? "bg-muted/50 cursor-not-allowed" : ""}`} /> @@ -140,16 +180,40 @@ function SignupForm() { update("password", e.target.value)} required minLength={8} autoComplete="new-password" - className="pl-10" + className="pl-10 pr-10" /> + + {showStrength && ( +
+
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ {strength.label} +
+ )}
@@ -158,15 +222,24 @@ function SignupForm() { update("confirmPassword", e.target.value)} required minLength={8} autoComplete="new-password" - className="pl-10" + className="pl-10 pr-10" /> +
@@ -182,6 +255,12 @@ function SignupForm() { Sign in

+

+ By creating an account, you agree to our{" "} + Terms + {" "}and{" "} + Privacy Policy. +

); }