feat(auth): add verification feature in settings

This commit is contained in:
shafin-r
2025-09-09 00:54:06 +06:00
parent 4042e28bf7
commit c3ead879ad
7 changed files with 205 additions and 1 deletions

View File

@ -4,6 +4,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { useAuthStore } from "@/stores/authStore"; import { useAuthStore } from "@/stores/authStore";
import { import {
BadgeCheck,
Bookmark, Bookmark,
ChartColumn, ChartColumn,
ChevronRight, ChevronRight,
@ -59,6 +60,19 @@ const SettingsPage = () => {
</div> </div>
</section> </section>
<section className="flex flex-col gap-8"> <section className="flex flex-col gap-8">
<button
onClick={() => router.push("/settings/verify")}
className="flex justify-between"
>
<div className="flex items-center gap-4">
<BadgeCheck size={30} color="#113768" />
<h3 className="text-md font-medium text-[#113768]">
Verify your account
</h3>
</div>
<ChevronRight size={30} color="#113768" />
</button>
<div className="h-[0.5px] border-[0.1px] w-full border-[#113768]/20"></div>
<button <button
onClick={() => router.push("/profile")} onClick={() => router.push("/profile")}
className="flex justify-between" className="flex justify-between"

View File

@ -0,0 +1,96 @@
"use client";
import BackgroundWrapper from "@/components/BackgroundWrapper";
import Header from "@/components/Header";
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/components/ui/input-otp";
import { API_URL } from "@/lib/auth";
import { useAuthStore } from "@/stores/authStore";
import Image from "next/image";
import React, { useState } from "react";
const VerificationScreen = () => {
const [otp, setOtp] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { fetchUser } = useAuthStore();
const handleVerify = async () => {
if (otp.length < 6) return;
setLoading(true);
setError(null);
try {
const response = await fetch(`${API_URL}/auth/verify?code=${otp}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || "Verification failed");
}
// 🔥 Call zustand action to update auth state
await fetchUser();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<BackgroundWrapper>
<Header displayTabTitle="Verification" />
<div className="flex flex-col items-center justify-center pt-10 px-6 gap-4">
<Image
src={"/images/icons/otp.svg"}
height={200}
width={300}
alt="otp-banner"
/>
<h1 className="font-medium text-xl text-center ">
Enter the code here that you received in your email.
</h1>
<InputOTP
maxLength={6}
value={otp}
onChange={setOtp}
onComplete={handleVerify} // auto-submit when complete
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
{error && <p className="text-red-500 mt-3">{error}</p>}
<button
onClick={handleVerify}
disabled={otp.length < 6 || loading}
className="mt-6 px-6 py-2 bg-blue-600 text-white rounded-lg disabled:bg-gray-400 transition"
>
{loading ? "Verifying..." : "Verify"}
</button>
</div>
</BackgroundWrapper>
);
};
export default VerificationScreen;

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import Header from "@/components/Header"; import Header from "@/components/Header";
import QuestionItem from "@/components/QuestionItem"; import QuestionItem from "@/components/QuestionItem";
import BackgroundWrapper from "@/components/BackgroundWrapper"; import BackgroundWrapper from "@/components/BackgroundWrapper";
@ -10,6 +10,7 @@ import { useTimerStore } from "@/stores/timerStore";
export default function ExamPage() { export default function ExamPage() {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const test_id = searchParams.get("test_id") || ""; const test_id = searchParams.get("test_id") || "";
const type = searchParams.get("type") || ""; const type = searchParams.get("type") || "";
@ -136,3 +137,6 @@ export default function ExamPage() {
</div> </div>
); );
} }
function cancelExam() {
throw new Error("Function not implemented.");
}

View File

@ -0,0 +1,77 @@
"use client";
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-14 w-14 items-center justify-center border-y border-r text-lg font-bold shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };

11
package-lock.json generated
View File

@ -16,6 +16,7 @@
"capacitor-secure-storage-plugin": "^0.11.0", "capacitor-secure-storage-plugin": "^0.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.523.0", "lucide-react": "^0.523.0",
"next": "15.3.2", "next": "15.3.2",
"react": "^19.0.0", "react": "^19.0.0",
@ -4652,6 +4653,16 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/internal-slot": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",

View File

@ -17,6 +17,7 @@
"capacitor-secure-storage-plugin": "^0.11.0", "capacitor-secure-storage-plugin": "^0.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"input-otp": "^1.4.2",
"lucide-react": "^0.523.0", "lucide-react": "^0.523.0",
"next": "15.3.2", "next": "15.3.2",
"react": "^19.0.0", "react": "^19.0.0",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB