Files
IB/src/app/(main)/settings/user_name/page.tsx
2025-10-27 11:56:36 +05:30

425 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useEffect, useState } from "react";
import {
TextInput,
Button,
Title,
Paper,
Group,
Text,
List,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { IconUser, IconRefresh } from "@tabler/icons-react";
import { generateCaptcha } from "@/app/captcha";
import { sendOtp, verifyOtp } from "@/app/_util/otp";
export default function SetPreferredNameSimple() {
const [preferredName, setPreferredName] = useState("");
const [confirmName, setConfirmName] = useState("");
const [preferredNameError, setPreferredNameError] = useState<string | null>(null);
const [confirmNameError, setConfirmNameError] = useState<string | null>(null);
const [captcha, setCaptcha] = useState("");
const [captchaInput, setCaptchaInput] = useState("");
const [otp, setOtp] = useState("");
const [otpValidated, setOtpValidated] = useState(false);
const [countdown, setCountdown] = useState(180);
const [timerActive, setTimerActive] = useState(false);
const [step, setStep] = useState<"form" | "otp" | "final">("form");
const [existingName, setExistingName] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const icon = <IconUser size={18} stroke={1.5} />;
// Fetch name + generate captcha on mount
useEffect(() => {
checkPreferredName();
regenerateCaptcha();
}, []);
// OTP timer
useEffect(() => {
let interval: number | undefined;
if (timerActive && countdown > 0) {
interval = window.setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
}
if (countdown === 0) {
if (interval) clearInterval(interval);
setTimerActive(false);
}
return () => {
if (interval) clearInterval(interval);
};
}, [timerActive, countdown]);
// API: Fetch preferred name
async function checkPreferredName() {
try {
const token = localStorage.getItem("access_token");
const response = await fetch("/api/auth/user_name", {
method: "GET",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Failed to fetch preferred name");
setExistingName(data.user_name || null);
} catch (err) {
console.error(err);
setExistingName(null);
} finally {
setLoading(false);
}
}
// Captcha
const regenerateCaptcha = async () => {
const newCaptcha = await generateCaptcha();
setCaptcha(newCaptcha);
setCaptchaInput("");
};
function onPreferredNameChange(value: string) {
const sanitized = value.replace(/[^A-Za-z0-9@_]/g, "").slice(0, 11);
setPreferredName(sanitized);
if (sanitized.length > 0 && (sanitized.length < 5 || sanitized.length > 11)) {
setPreferredNameError("Preferred Name must be 511 characters.");
} else if (!/[A-Za-z]/.test(sanitized)) {
setPreferredNameError("Preferred Name must contain at least one letter.");
} else if (!/[@_]/.test(sanitized)) {
setPreferredNameError("Preferred Name must contain at least one special character (@ or _).");
} else {
setPreferredNameError(null);
}
}
function onConfirmNameChange(value: string) {
const sanitized = value.replace(/[^A-Za-z0-9@_]/g, "").slice(0, 11);
setConfirmName(sanitized);
if (sanitized && sanitized !== preferredName) {
setConfirmNameError("Confirm name does not match Preferred Name.");
} else {
setConfirmNameError(null);
}
}
// OTP
async function handleSendOtp() {
try {
await sendOtp({ type: "USERNAME_UPDATED" });
notifications.show({
title: "OTP Sent",
message: "An OTP has been sent to your registered mobile.",
color: "blue",
});
setStep("otp");
setCountdown(180);
setTimerActive(true);
} catch (err: any) {
notifications.show({
title: "Error",
message: err.message || "Failed to send OTP.",
color: "red",
});
}
}
async function handleVerifyOtp() {
try {
await verifyOtp(otp);
notifications.show({
title: "OTP Verified",
message: "OTP has been successfully verified.",
color: "green",
});
setOtpValidated(true);
setStep("final");
} catch {
notifications.show({
title: "Invalid OTP",
message: "The OTP entered is incorrect.",
color: "red",
});
}
}
// Update Preferred Name API
async function handleUpdatePreferredName() {
const token = localStorage.getItem("access_token");
if (!token) {
notifications.show({
title: "Session Expired",
message: "Please log in again.",
color: "red",
});
return;
}
try {
const response = await fetch("/api/auth/user_name", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Login-Type": "IB",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ user_name: preferredName }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.message || "Failed to update preferred name");
notifications.show({
title: "Success",
message: "Preferred name updated successfully!",
color: "green",
});
resetForm();
await checkPreferredName(); // refresh
} catch (err: any) {
notifications.show({
title: "Error",
message: err.message || "Server error, please try again later.",
color: "red",
});
}
}
const resetForm = () => {
setPreferredName("");
setConfirmName("");
setCaptchaInput("");
setOtp("");
setOtpValidated(false);
setStep("form");
regenerateCaptcha();
};
// Main Submit
const handleSubmit = async () => {
if (step === "form") {
if (!preferredName || !confirmName || !captchaInput) {
notifications.show({
title: "Missing Fields",
message: "Please fill all mandatory fields.",
color: "red",
});
return;
}
if(preferredName !== confirmName){
notifications.show({
title: "Mismatch Input",
message: "Preferred name and Confirm preferred name not same.",
color: "red",
});
return;
}
if (preferredNameError || confirmNameError) {
notifications.show({
title: "Invalid Input",
message: "Please correct highlighted fields before continuing.",
color: "red",
});
return;
}
if (captchaInput !== captcha) {
notifications.show({
title: "Invalid Captcha",
message: "Please enter correct captcha.",
color: "red",
});
regenerateCaptcha();
return;
}
await handleSendOtp();
return;
}
if (step === "otp") {
await handleVerifyOtp();
return;
}
if (step === "final") {
await handleUpdatePreferredName();
return;
}
};
if (loading) return <Text>Loading...</Text>;
return (
<Paper shadow="sm" radius="md" p="md" withBorder>
<Title order={3} mb="sm">
Set Preferred Name
</Title>
<div style={{ overflowY: "auto", maxHeight: "280px", paddingRight: 8 }}>
{existingName && (
<Text mb="sm">
<Text span fw={600}>Current Preferred Name: </Text>
<Text span>{existingName}</Text>
</Text>
)}
{!existingName && (
<Text mb="sm">
<Text span fw={500} c='red' >You have not set the user ID yet. Please set it first.</Text>
</Text>
)}
<Group grow>
<TextInput
label="Preferred Name"
placeholder="Enter preferred name (511 chars, only letters, numbers, @, _)"
value={preferredName}
onChange={(e) => onPreferredNameChange(e.currentTarget.value)}
withAsterisk
mb="xs"
maxLength={11}
readOnly={step !== "form"}
error={preferredNameError || undefined}
/>
<TextInput
label="Confirm Preferred Name"
placeholder="Re-enter preferred name"
value={confirmName}
onChange={(e) => onConfirmNameChange(e.currentTarget.value)}
withAsterisk
mb="xs"
rightSection={icon}
readOnly={step !== "form"}
onCopy={(e) => e.preventDefault()}
onPaste={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
error={confirmNameError || undefined}
/>
</Group>
{/* CAPTCHA */}
<div style={{ marginTop: 5 }}>
<label style={{ display: "block", marginBottom: 4, fontSize: "14px" }}>
Enter CAPTCHA <span style={{ color: "red" }}>*</span>
</label>
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
marginBottom: 5,
}}
>
<div
style={{
fontSize: "18px",
letterSpacing: "3px",
background: "#f3f4f6",
padding: "6px 12px",
borderRadius: "6px",
border: "1px solid #d1d5db",
userSelect: "none",
textDecoration: "line-through",
fontFamily: "cursive",
}}
>
{captcha}
</div>
<Button
size="xs"
variant="outline"
onClick={regenerateCaptcha}
style={{ height: 30, padding: "0 10px", lineHeight: "1" }}
disabled={step !== "form"}
>
Refresh
</Button>
<TextInput
placeholder="Enter above text"
value={captchaInput}
onChange={(e) => setCaptchaInput(e.currentTarget.value)}
withAsterisk
style={{ flexGrow: 1 }}
readOnly={step !== "form"}
/>
</div>
</div>
{/* OTP */}
{step !== "form" && (
<Group grow mt="sm">
<TextInput
label="OTP"
placeholder="Enter OTP"
value={otp}
onChange={(e) => setOtp(e.currentTarget.value)}
maxLength={6}
withAsterisk
disabled={otpValidated}
/>
{!otpValidated && (
timerActive ? (
<Text size="xs" c="dimmed" style={{ minWidth: "120px" }}>
Resend OTP in{" "}
{String(Math.floor(countdown / 60)).padStart(2, "0")}:
{String(countdown % 60).padStart(2, "0")}
</Text>
) : (
<IconRefresh
size={22}
style={{
cursor: "pointer",
color: "blue",
marginBottom: "6px",
}}
onClick={handleSendOtp}
/>
)
)}
</Group>
)}
</div>
{/* BUTTONS */}
<Group mt="md" gap="sm">
<Button onClick={handleSubmit}>
{step === "form" && "Submit"}
{step === "otp" && "Validate OTP"}
{step === "final" && "Set Preferred Name"}
</Button>
<Button variant="outline" color="gray" onClick={resetForm}>
Reset
</Button>
</Group>
<List size="sm" mt="xs" withPadding>
<List.Item>
Your Preferred Name must be 511 characters long and contain only
letters, numbers, and @ or _ symbols.
</List.Item>
<List.Item>
You can change your username a maximum of 5 times.
</List.Item>
<List.Item>
Preferred Name must contain at least one alphabet and one special character (@ or _).
</List.Item>
</List>
</Paper>
);
}