Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions components/auth/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useToggle } from "@/hooks/useToggle";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import { PasswordStrength } from "@/components/ui/password-strength";
import { FormSkeleton } from "@/components/ui/form-skeleton";

export default function SignUp() {
const [isSubmitting, setIsSubmitting] = useState(false);
Expand All @@ -40,6 +42,7 @@ export default function SignUp() {
},
});
const router = useRouter();
const [isValidating, setIsValidating] = useState(false);

const handleSignUp = async (data: SignUpFormData) => {
if (isSubmitting) return;
Expand Down Expand Up @@ -97,6 +100,32 @@ export default function SignUp() {
};
const [isPasswordVisible, togglePasswordVisibility] = useToggle(false);

const validateField = async (field: keyof SignUpFormData) => {
setIsValidating(true);
try {
await form.trigger(field);
} finally {
setIsValidating(false);
}
};

if (isSubmitting) {
return (
<section className="relative">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<div className="pb-12 pt-32 md:pb-20 md:pt-40">
<div className="md:pb-15 mx-auto max-w-3xl pb-10 text-center text-xl sm:text-2xl md:text-3xl lg:text-4xl">
<h1 className="h1">Creating your account...</h1>
</div>
<div className="mx-auto max-w-sm">
<FormSkeleton fieldCount={5} />
</div>
</div>
</div>
</section>
);
}

return (
<section className="relative">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
Expand Down Expand Up @@ -169,6 +198,7 @@ export default function SignUp() {
id="full_name"
placeholder="First and last name"
{...field}
onBlur={() => validateField("full_name")}
/>
</FormControl>
<FormMessage />
Expand All @@ -187,6 +217,7 @@ export default function SignUp() {
type="email"
placeholder="[email protected]"
{...field}
onBlur={() => validateField("email")}
/>
</FormControl>
<FormMessage />
Expand All @@ -206,8 +237,10 @@ export default function SignUp() {
type={isPasswordVisible ? "text" : "password"}
placeholder="Password (at least 8 characters)"
{...field}
onBlur={() => validateField("password")}
/>
</FormControl>
<PasswordStrength password={field.value} className="mt-2" />
<FormMessage />
</FormItem>
)}
Expand Down
20 changes: 20 additions & 0 deletions components/ui/form-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { cn } from "@/lib/utils";

interface FormSkeletonProps {
className?: string;
fieldCount?: number;
}

export function FormSkeleton({ className, fieldCount = 4 }: FormSkeletonProps) {
return (
<div className={cn("space-y-4", className)}>
{Array.from({ length: fieldCount }).map((_, index) => (
<div key={index} className="space-y-2">
<div className="h-4 w-24 bg-gray-200 rounded animate-pulse" />
<div className="h-10 w-full bg-gray-200 rounded animate-pulse" />
</div>
))}
<div className="h-10 w-full bg-gray-200 rounded animate-pulse mt-6" />
</div>
);
}
105 changes: 105 additions & 0 deletions components/ui/password-strength.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

interface PasswordStrengthProps {
password: string;
className?: string;
}

export function PasswordStrength({ password, className }: PasswordStrengthProps) {
const [strength, setStrength] = useState(0);
const [message, setMessage] = useState("");

useEffect(() => {
if (!password) {
setStrength(0);
setMessage("");
return;
}

let score = 0;
let feedback = [];

// Length check
if (password.length >= 8) {
score += 1;
} else {
feedback.push("At least 8 characters");
}

// Uppercase check
if (/[A-Z]/.test(password)) {
score += 1;
} else {
feedback.push("One uppercase letter");
}

// Lowercase check
if (/[a-z]/.test(password)) {
score += 1;
} else {
feedback.push("One lowercase letter");
}

// Number check
if (/[0-9]/.test(password)) {
score += 1;
} else {
feedback.push("One number");
}

// Special character check
if (/[^A-Za-z0-9]/.test(password)) {
score += 1;
} else {
feedback.push("One special character");
}

setStrength(score);

if (score === 0) {
setMessage("");
} else if (score <= 2) {
setMessage("Weak - " + feedback.join(", "));
} else if (score <= 3) {
setMessage("Medium - " + feedback.join(", "));
} else if (score <= 4) {
setMessage("Strong - " + feedback.join(", "));
} else {
setMessage("Very Strong");
}
}, [password]);

const getStrengthColor = () => {
if (strength <= 2) return "bg-red-500";
if (strength <= 3) return "bg-yellow-500";
if (strength <= 4) return "bg-blue-500";
return "bg-green-500";
};

return (
<div className={cn("space-y-2", className)}>
<div className="h-1 w-full bg-gray-200 rounded-full overflow-hidden">
<div
className={cn(
"h-full transition-all duration-300",
getStrengthColor()
)}
style={{ width: `${(strength / 5) * 100}%` }}
/>
</div>
{message && (
<p
className={cn("text-sm", {
"text-red-500": strength <= 2,
"text-yellow-500": strength === 3,
"text-blue-500": strength === 4,
"text-green-500": strength === 5,
})}
>
{message}
</p>
)}
</div>
);
}