Skip to content

Commit cb7eec0

Browse files
committed
feat: add Passwords component with animation and validation functionality
1 parent 322a350 commit cb7eec0

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

src/components/animations/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import Map from "@/components/animations/map";
3232
import ModeToggle from "@/components/animations/mode-toggle";
3333
import MovieGallery from "@/components/animations/movie-gallery";
3434
import MusicSheet from "@/components/animations/music-shit";
35+
import Passwords from "@/components/animations/password";
3536
import PeerListBar from "@/components/animations/peerlist-bar";
3637
import Plan from "@/components/animations/plan";
3738
import ProfileEdit from "@/components/animations/profile-edit";
@@ -95,6 +96,7 @@ export {
9596
MusicSheet,
9697
NewHero,
9798
NewMember,
99+
Passwords,
98100
PeerListBar,
99101
Plan,
100102
ProfileEdit,
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"use client";
2+
3+
import { cn } from "@/lib/utils";
4+
import { motion } from "motion/react";
5+
import { useEffect, useState } from "react";
6+
import { Input } from "../ui/input";
7+
import { Button } from "../ui/button";
8+
9+
const Passwords = () => {
10+
const [password, setPassword] = useState("");
11+
const [confirmPassword, setConfirmPassword] = useState("");
12+
const [shake, setShake] = useState(false);
13+
const [showConfirm, setShowConfirm] = useState(false);
14+
15+
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
16+
setPassword(e.target.value);
17+
};
18+
19+
const handleConfirmPasswordChange = (
20+
e: React.ChangeEvent<HTMLInputElement>
21+
) => {
22+
if (
23+
confirmPassword.length >= password.length &&
24+
e.target.value.length > confirmPassword.length
25+
) {
26+
setShake(true);
27+
} else {
28+
setConfirmPassword(e.target.value);
29+
}
30+
};
31+
32+
useEffect(() => {
33+
if (shake) {
34+
const timer = setTimeout(() => setShake(false), 500);
35+
return () => clearTimeout(timer);
36+
}
37+
}, [shake]);
38+
39+
const getLetterStatus = (index: number) => {
40+
if (!confirmPassword[index]) return "";
41+
return password[index] === confirmPassword[index]
42+
? "bg-green-500"
43+
: "bg-red-500";
44+
};
45+
46+
const passwordsMatch = password === confirmPassword && password.length > 0;
47+
48+
const matchAnimation = {
49+
transition: { duration: 0.3 },
50+
};
51+
52+
return (
53+
<main className="full">
54+
<div className="z-10 flex w-full flex-col full">
55+
<div className="mx-auto flex h-full w-full max-w-lg flex-col items-center justify-center gap-8 p-16">
56+
<h1 className="text-2xl font-bold text-start border-b w-full h-12">
57+
Change password
58+
</h1>
59+
<div className="flex w-full flex-col items-start justify-center gap-4">
60+
<div className="flex gap-2 flex-col w-full">
61+
<p className="text-muted-foreground">Enter new password</p>
62+
<div className="w-full relative">
63+
{!showConfirm ? (
64+
<Input
65+
className="placeholder:tracking-widest tracking-[.59rem] focus:border-foreground-muted"
66+
type="password"
67+
placeholder="Password"
68+
value={password}
69+
onChange={handlePasswordChange}
70+
/>
71+
) : (
72+
<motion.div
73+
className={cn(
74+
"h-9 w-full rounded-md border px-2 py-2 bg-background"
75+
)}
76+
animate={{
77+
...matchAnimation,
78+
}}
79+
>
80+
<div className="relative h-full w-fit overflow-hidden rounded-lg z-0">
81+
<div className="z-10 flex h-full items-center justify-center px-0 py-1">
82+
{password.split("").map((_, index) => (
83+
<div
84+
key={index}
85+
className="flex h-full w-4 shrink-0 items-center justify-center"
86+
>
87+
<span
88+
className={cn(
89+
"size-[3.5px] bg-primary",
90+
getLetterStatus(index)
91+
)}
92+
/>
93+
</div>
94+
))}
95+
</div>
96+
</div>
97+
</motion.div>
98+
)}
99+
</div>
100+
</div>
101+
102+
<div className="w-full relative flex gap-2 flex-col">
103+
<p className="text-muted-foreground">Confirm password</p>
104+
<Input
105+
className="tracking-[.71rem] outline-none placeholder:tracking-widest focus:border-foreground-muted"
106+
type="password"
107+
placeholder="Confirm Password"
108+
value={confirmPassword}
109+
onChange={handleConfirmPasswordChange}
110+
onFocus={() => setShowConfirm(true)}
111+
onBlur={() => {
112+
if (!passwordsMatch) setShowConfirm(false);
113+
}}
114+
/>
115+
</div>
116+
</div>
117+
<Button className="w-full" disabled={!passwordsMatch}>
118+
Change password
119+
</Button>
120+
</div>
121+
</div>
122+
</main>
123+
);
124+
};
125+
126+
export default Passwords;

src/constants/components.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ type Component = {
99
};
1010

1111
export const COMPONENTS: Component[] = [
12+
{
13+
name: "Passwords",
14+
component: Animated.Passwords,
15+
href: "",
16+
notReady: true,
17+
},
1218
{
1319
name: "iMessage",
1420
component: Animated.Imessage,

0 commit comments

Comments
 (0)