Dashboard & Management Pages¶
4. Dashboard Page¶
File: src/app/dashboard/page.tsx (102 lines)
Route: /dashboard
Type: Protected
Purpose: Chatbot type selection landing page
Overview¶
Simple landing page showing 3 chatbot type options with visual cards.
Components Used¶
ProtectedLayout- Authentication wrapperKeysecondCard- Action card component for each chatbot typeNext/Image- Optimized image display
Layout Structure¶
3 Chatbot Type Cards:
-
Text Chatbot
-
Image:
chatbotImg(chatbott.png) - Icon:
avtarimgin orange circle -
Component:
<KeysecondCard type="text" /> -
Voice Chatbot
-
Image:
voiceImg(voice.png) - Icon:
avtarimgin orange circle -
Component:
<KeysecondCard type="voice" /> -
3D Chatbot
- Image:
avatarImg(chat-new.png) - Icon:
avtarimgin orange circle - Component:
<KeysecondCard type="3d" />
Responsive Design¶
- Desktop: 3-column grid with gap-16
- Mobile: Single column stack (h-[195vh])
- Max Width: screen-xl (1280px)
Styling¶
- Background:
bg-[#0A0A0A](dark theme) - Cards: Orange (#FF6622) branding
- images: 300x300px with priority loading
5. Chatbots List Page¶
File: src/app/chatbots/page.tsx (1,827 lines)
Route: /chatbots
Type: Protected
Purpose: Comprehensive chatbot management dashboard
⚠️ Most Complex Page in Application
State Variables (25+)¶
Chatbot Data:
- chatbots (
ChatbotData[]) - Active chatbots list - trashedChatbots (
TrashedChatbotData[]) - Deleted chatbots (7-day window) - sharedChatbots (
SharedChatbot[]) - Chatbots shared with user - incompleteChatbots (
IncompleteChatbot[]) - Unfinished setup processes
Loading States: 5. loading (boolean) - Initial page load 6. deleteLoading (string | null) - Delete in progress (project_id) 7. createLoading (boolean) - Create chatbot in progress 8. continueLoading (string | null) - Continue setup in progress 9. closeLoading (string | null) - Close incomplete in progress 10. trashLoading (boolean) - Fetching trash 11. restoreLoading (string | null) - Restore from trash 12. permanentDeleteLoading (string | null) - Permanent deletion 13. loadingShared (boolean) - Loading shared chatbots 14. removingSharedAccess ({[key: string]: boolean}) - Remove access loading
UI State: 15. error (string) - Error messages 16. showDeleteModal (boolean) - Delete confirmation modal 17. chatbotToDelete (string | null) - Chatbot being deleted 18. showCloseModal (boolean) - Close incomplete modal 19. chatbotToClose (string | null) - Incomplete chatbot to close 20. showTrashModal (boolean) - Trash view modal 21. showShareModal (boolean) - Share modal 22. chatbotToShare (ChatbotData | null) - Chatbot to share 23. showRemoveAccessModal (boolean) - Remove access confirmation 24. chatbotToRemoveAccess (SharedChatbot | null) - Shared chatbot to remove 25. userFeatures (UserFeatures | null) - User subscription features
Type Definitions (10 interfaces)¶
interface ChatbotData {
_id: string;
user_id: string;
project_id: string;
chatbot_type: string; // "3D-chatbot" | "text-chatbot" | "voice-chatbot"
chatbot_purpose: string;
avatar: string;
voice: string;
chatbot_registered_at: string;
avatar_type?: string;
hidden_name?: string;
title_name?: string;
domain?: string;
model?: string;
active?: boolean;
}
interface SharedAccess {
project_id: string;
shared_with_email: string;
share_access: string; // Role: "chatbot_viewer" | "chatbot_editor" | "chatbot_analytics"
shared_at: string;
}
interface SharedChatbot extends ChatbotData {
share_access: string;
shared_at: string;
shared_with_email: string;
}
interface TrashedChatbotData extends ChatbotData {
deleted_at: string;
}
interface IncompleteChatbot {
user_id: string;
project_id: string;
state: string; // "start" | "sitemap_urls_fetched" | "chatbot_purpose_selected" | "avatar_selected" | "voice_selected" | "END"
}
interface UserFeatures {
user_id: string;
subscription_id: string;
no_of_chatbots: string;
no_of_chat_sessions: string;
features: Feature[];
// ... more fields
}
API Endpoints (15+)¶
Chatbot CRUD:
POST /v2/create-chatbot- Create new chatbot projectPOST /v2/select-chatbot- Select chatbot type (3D/text/voice)GET /v2/get-chatbots?user_id={id}- Get user's project listGET /v2/get-chatbot-selection?user_id={id}&project_id={id}- Get chatbot detailsDELETE /v2/delete-chatbot-selection?user_id={id}&project_id={id}- Delete chatbot (to trash)
Incomplete Chatbots: 6. GET /v2/check-selection-state?user_id={id}&project_id={id} - Check setup progress 7. POST /v2/delete-chatbot-selection-state - Close incomplete chatbot
Trash Management: 8. GET /v2/get-trashed-chatbot-sevendays?user_id={id} - Get trashed chatbots (7-day window) 9. GET /v2/get-trashed-chatbots?user_id={id}&project_id={id} - Restore chatbot 10. DELETE /v2/delete-trashed-chatbots?user_id={id}&project_id={id} - Permanent delete
Shared Access: 11. GET /v2/get-shared-access?user_id={id} - Get chatbots shared with user 12. DELETE /v2/remove-shared-access?user_id={id}&project_id={id}&shared_with_email={email} - Remove shared access
User Features: 13. GET /v2/get-user-features/{userId} - Get subscription features
Share Chatbot: 14. Uses ShareModal component (dedicated share flow)
Key Features¶
1. Create New Chatbot Flow
const handleCreateChatbot = async () => {
// Step 1: Create project
const response1 = await fetch("/v2/create-chatbot", {
method: "POST",
body: formData(user_id),
});
const { project_id } = await response1.json();
// Step 2: Auto-select 3D chatbot type
const response2 = await fetch("/v2/select-chatbot", {
method: "POST",
body: formData(user_id, project_id, "3D-chatbot", session_id),
});
// Step 3: Redirect to data source page
router.push("/3d-chatbot-data-source");
};
2. Incomplete Chatbot Tracking
- Detects chatbots with state != "start" && != "END"
- Shows separate section with amber warning styling
- Actions:
- Continue Setup (redirect to appropriate step)
- Delete (close incomplete setup)
State Labels:
const getStateLabel = (state: string) => {
switch (state) {
case "start":
return "Chatbot Type Selected";
case "sitemap_urls_fetched":
return "Data Fetched";
case "chatbot_purpose_selected":
return "Chatbot Purpose Selected";
case "avatar_selected":
return "Chatbot Avatar Selected";
case "voice_selected":
return "Chatbot Voice Selected";
case "END":
return "Chatbot Created";
default:
return "In Progress";
}
};
3. Trash System (7-Day Window)
- Deleted chatbots recoverable for 7 days
- Actions:
- Restore (return to active chatbots)
- Delete Forever (permanent deletion)
- Sorted by deletion date (newest first)
4. Shared Chatbots
- View chatbots shared by others
- Role-Based Access:
chatbot_viewer- View onlychatbot_editor- Edit permissionschatbot_analytics- Analytics access- Remove own access option
- URL includes
roleparameter
Share URL Format:
const params = new URLSearchParams({
avatarName: chatbot.avatar,
avatarType: chatbot.avatar_type,
project_id: chatbot.project_id,
user_id: chatbot.user_id,
selected_project_id: chatbot.project_id,
role: chatbot.share_access,
isSharedView: "true",
});
5. Admin Token Support
- URL params:
?token=xxx&user_id=yyy - Auto-sets session storage for superadmin access
Chatbot Sorting¶
Active Chatbots: By registration date (newest first)
Trashed Chatbots: By deletion date (newest first)
Shared Chatbots: By share date (newest first)
Avatar Image Mapping¶
const avatarImages: Record<string, any> = {
Eva: Eva,
Shayla: Shayla,
Myra: Myra,
Chris: Chris,
Jack: Jack,
Anu: Anu,
Emma: Emma,
};
Fallback Images:
- Text chatbot:
textimg.png - Voice chatbot:
voiceimg.png
View Chatbot Flow¶
const handleViewChatbot = (chatbot: ChatbotData) => {
// Store session data
sessionStorage.setItem("selected_project_id", chatbot.project_id);
sessionStorage.setItem("chatbot_type", chatbot.chatbot_type);
sessionStorage.setItem("user_id", chatbot.user_id);
sessionStorage.setItem("selectedPurpose", chatbot.chatbot_purpose);
sessionStorage.setItem("avatarType", chatbot.avatar_type);
sessionStorage.setItem("selection_avatar", chatbot.avatar);
sessionStorage.setItem("selection_voice", chatbot.voice);
// Route based on type
switch (chatbot.chatbot_type) {
case "3D-chatbot":
router.push("/3d-chatbot");
break;
case "text-chatbot":
router.push("/text-chatbot");
break;
case "voice-chatbot":
router.push("/voice-chatbot");
break;
default:
router.push("/3d-chatbot");
}
};
UI Sections¶
1. Empty State (no chatbots)
"No AI Agents yet"
"Start by generating your first AI Agent Assistant!"
[Create New AI Agent] button
2. Main View (has chatbots)
- Header: "Your AI Assistants" + Trash icon
- Incomplete Chatbots (if any) - Amber warning section
- Create Button (if no incomplete chatbots)
- Active Chatbots Grid - ½/3 columns responsive
- Shared Chatbots Section (if any)
3. Modals:
- Delete Confirmation Modal
- Close Incomplete Modal
- Trash Modal (full-screen chatbot list)
- Share Modal (via ShareModal component)
- Remove Access Confirmation
Chatbot Card Layout¶
Active Chatbot Card:
┌─────────────────────────────┐
│ [Avatar Image - 192px] │
│ [Active/Inactive Badge] │
│ [Share Button] (commented) │
├─────────────────────────────┤
│ Type: 3D-chatbot │
│ Purpose: Customer Support │
│ Voice: Emma │
│ Domain: example.com │
│ Model: gpt-4 │
│ Created: Jan 15, 2025 │
├─────────────────────────────┤
│ [View] [Share] [Delete] │
└─────────────────────────────┘
Inactive Chatbot:
- Opacity 60%
- Red border
- "Inactive" badge
- Cursor not-allowed
Error Handling¶
Nested JSON Error Extraction:
// Handles backend errors like:
// { "detail": "{\"detail\": \"Nested error\"}" }
try {
const errorData = JSON.parse(errorText);
if (errorData.detail && typeof errorData.detail === "string") {
const nestedError = JSON.parse(errorData.detail);
errorMessage = nestedError.detail || errorData.detail;
}
} catch {
errorMessage = errorText || fallback;
}
Feature Flags Integration¶
const isFeatureEnabled = (featureName: string) => {
if (!userFeatures?.features) return false;
const feature = userFeatures.features.find((f) => f.feature === featureName);
return feature?.feature_value === "enabled";
};
Usage: Control UI elements based on subscription tier
Session Storage Management¶
Keys Set:
selected_project_idchatbot_typeuser_idselectedPurposeavatarTypeselection_avatarselection_voiceuser_role(for shared chatbots)isSharedAccess(for shared chatbots)continueChatbot(true/false)
Keys Removed on Create:
chatbotTitlechatbot_nameqa_completedchatbot_purpose2
Components Used¶
ShareModal- Chatbot sharing interfaceImage(Next.js) - Optimized imagesToastContainer- Notifications- Various avatar images (Eva, Shayla, Myra, Chris, Jack, Anu, Emma)
- Inline SVG icons (trash, restore, delete, share, info, etc.)
Security Features¶
- ✅ JWT Authentication - All API calls require auth token
- ✅ User-ID Header - Validates user ownership
- ✅ Project-ID Header - Additional validation
- ✅ Role-based access - Shared chatbot permissions
- ✅ Trash recovery window - 7-day soft delete
- ✅ Confirmation modals - Prevent accidental deletions
Performance Optimizations¶
- Parallel API Calls:
-
Sorted Display: Pre-sorted by date (newest first)
-
Conditional Rendering: Only load sections with data
-
Loading States: Individual loading per action (no full page reload)
Responsive Design¶
Grid Breakpoints:
- Mobile: 1 column
- Tablet (md): 2 columns
- Desktop (lg): 3 columns
Trash Modal: ½/3 columns based on screen size
Dependencies:
next/navigation(useRouter)next/image(Image)react(useState, useEffect)react-toastify(toast notifications)
Environment Variables:
3. Forgot Password Page¶
File: src/app/forgot-password/page.tsx (349 lines)
Route: /forgot-password
Type: Public
Purpose: Password reset via email OTP verification
State Variables (7)¶
- email (
string) - User's email address - otp (
string) - 6-digit OTP code - newPassword (
string) - New password to set - step (
number) - Current step (1, 2, or 3) - error (
string) - Error message display - message (
string) - Success message display - timer (
number) - Countdown for OTP resend (60 seconds) - showPassword (
boolean) - Toggle new password visibility
Three-Step Password Reset Flow¶
Step 1: Enter Email → Send OTP
Step 2: Verify OTP → Confirm validity
Step 3: Reset Password → Update password
Error Message Extraction Helper¶
Purpose: Parse nested JSON error responses from backend
Implementation:
const extractErrorMessage = (errorData: any, fallbackMessage: string) => {
try {
// Check if detail exists and is a string
if (errorData.detail && typeof errorData.detail === "string") {
// Try to parse the detail as JSON
const parsedDetail = JSON.parse(errorData.detail);
return parsedDetail.detail || parsedDetail.message || fallbackMessage;
}
// If detail is already an object
if (errorData.detail && typeof errorData.detail === "object") {
return (
errorData.detail.detail || errorData.detail.message || fallbackMessage
);
}
// Fallback to direct message or detail
return errorData.message || errorData.detail || fallbackMessage;
} catch (e) {
// If parsing fails, return the original detail or fallback
return errorData.detail || errorData.message || fallbackMessage;
}
};
Usage: Handles multiple backend error response formats
- String:
{ "detail": "Error message" } - JSON String:
{ "detail": "{\"detail\": \"Nested error\"}" } - Object:
{ "detail": { "message": "Error" } }
Step 1: Request OTP¶
API Endpoint: POST /v2/forgot-password
Validation:
Request:
const formData = new FormData();
formData.append("email", email);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/forgot-password`,
{
method: "POST",
body: formData,
}
);
Success Flow:
if (response.ok) {
setMessage("OTP has been sent to your email.");
setStep(2); // Move to OTP verification step
setTimer(60); // Start/resume timer
}
Error Handling:
else {
const errorData = await response.json();
const errorMessage = extractErrorMessage(errorData, "Failed to send OTP.");
setError(errorMessage);
}
Step 2: Verify OTP¶
API Endpoint: POST /v2/verify-otp
Note: Same endpoint used for signup OTP verification
Validation:
Request:
const formData = new FormData();
formData.append("email", email);
formData.append("otp", otp);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/verify-otp`,
{
method: "POST",
body: formData,
}
);
Success Flow:
if (response.ok) {
setMessage("OTP verified. You can now reset your password.");
setStep(3); // Move to reset password step
}
Step 3: Reset Password¶
API Endpoint: POST /v2/reset-password
Validation:
Request:
const formData = new FormData();
formData.append("email", email);
formData.append("otp", otp);
formData.append("new_password", newPassword);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/reset-password`,
{
method: "POST",
body: formData,
}
);
Success Flow:
if (response.ok) {
setMessage("Password reset successful. Redirecting to login...");
setTimeout(() => {
window.location.href = "/login"; // Redirect to login page
}, 2000);
}
Note: Uses window.location.href instead of router.push() for hard redirect
Resend OTP¶
Handler:
const handleResendOtp = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setError("");
const formData = new FormData();
formData.append("email", email);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/forgot-password`,
{
method: "POST",
body: formData,
}
);
if (response.ok) {
setMessage("OTP has been resent to your email.");
setTimer(60); // Reset timer when OTP is resent
} else {
const errorData = await response.json();
const errorMessage = extractErrorMessage(
errorData,
"Failed to resend OTP."
);
setError(errorMessage);
}
};
Note: Reuses /v2/forgot-password endpoint (not a dedicated resend endpoint)
OTP Timer Implementation¶
Purpose: 60-second cooldown to prevent OTP spam
Logic:
useEffect(() => {
let interval: NodeJS.Timeout;
if (step === 2 && timer > 0) {
interval = setInterval(() => {
setTimer((prevTimer) => {
if (prevTimer <= 1) {
clearInterval(interval);
return 0;
}
return prevTimer - 1;
});
}, 1000);
}
return () => clearInterval(interval);
}, [step, timer]);
Features:
- Only runs when on step 2 (OTP verification)
- Decrements every second
- Auto-stops at 0
- Cleanup on unmount
UI States by Step¶
Step 1: Enter Email
{
step === 1 && (
<form onSubmit={handleForgotPassword}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Send OTP</button>
</form>
);
}
Step 2: Verify OTP
{
step === 2 && (
<>
<form onSubmit={handleVerifyOtp}>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
/>
<button type="submit">Verify OTP</button>
</form>
<button
onClick={handleResendOtp}
disabled={timer > 0}
className={timer > 0 ? "opacity-50 cursor-not-allowed" : ""}
>
{timer > 0 ? `Resend OTP in ${timer}s` : "Resend OTP"}
</button>
</>
);
}
Step 3: Reset Password
{
step === 3 && (
<form onSubmit={handleResetPassword}>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<button onClick={() => setShowPassword(!showPassword)}>
{showPassword ? <EyeSlashIcon /> : <EyeIcon />}
</button>
</div>
<button type="submit">Reset Password</button>
</form>
);
}
Dynamic Page Title¶
Changes based on current step
UI Features¶
1. Password Visibility Toggle (Step 3 only):
- Eye icon button
- SVG icons for show/hide states
- Positioned absolutely in input field
2. Timer-Based Resend Button:
<button
disabled={timer > 0}
className={timer > 0 ? "opacity-50 cursor-not-allowed" : ""}
>
{timer > 0 ? `Resend OTP in ${timer}s` : "Resend OTP"}
</button>
- Disabled during countdown
- Visual feedback (opacity change)
- Dynamic text
3. Back to Login Link:
<Link href="/login">
<span className="text-orange-500 hover:text-orange-700 cursor-pointer">
Back to Login
</span>
</Link>
Always visible on all steps
4. Error/Success Messages:
{
error && <p className="text-red-500 text-center">{error}</p>;
}
{
message && <p className="text-green-500 text-center">{message}</p>;
}
Centralized display for both error and success states
API Endpoints Summary¶
| Step | Endpoint | Method | Purpose |
|---|---|---|---|
| 1 | /v2/forgot-password |
POST | Send OTP to email |
| 2 | /v2/verify-otp |
POST | Verify OTP code |
| 3 | /v2/reset-password |
POST | Update password |
| Resend | /v2/forgot-password |
POST | Resend OTP |
Request Payloads¶
Step 1 (Send OTP):
Step 2 (Verify OTP):
Step 3 (Reset Password):
User Flow Diagram¶
1. User enters email
↓
POST /v2/forgot-password
↓
2. Email sent with OTP
User enters OTP
↓
POST /v2/verify-otp
↓
3. OTP verified
User enters new password
↓
POST /v2/reset-password
↓
4. Password updated
Redirect to /login (after 2s)
Error Messages¶
| Condition | Error Message |
|---|---|
| Empty email | "Please enter your email." |
| OTP send failed | "Failed to send OTP." (+ backend detail) |
| Empty OTP | "Please enter the OTP." |
| OTP verification failed | "OTP verification failed." (+ backend detail) |
| Empty new password | "Please enter a new password." |
| Password reset failed | "Password reset failed." (+ backend detail) |
| OTP resend failed | "Failed to resend OTP." (+ backend detail) |
| Network error | "An error occurred. Please try again." |
Success Messages¶
| Step | Success Message |
|---|---|
| OTP sent | "OTP has been sent to your email." |
| OTP verified | "OTP verified. You can now reset your password." |
| Password reset | "Password reset successful. Redirecting to login..." |
| OTP resent | "OTP has been resent to your email." |
Security Features¶
- ✅ OTP verification - Email ownership confirmation
- ✅ Rate limiting - 60-second cooldown between OTPs
- ✅ Password masking - Hidden by default with toggle
- ✅ OTP in memory - Required for final password reset (validates entire flow)
- ✅ Auto-redirect - Forces login after password reset
Dependencies¶
next/link- Navigationreact(useState, useEffect)- No external icon library (uses inline SVG)
Environment Variables¶
Differences from Signup OTP Flow¶
| Feature | Signup | Forgot Password |
|---|---|---|
| Steps | 2 (signup → verify) | 3 (email → OTP → password) |
| OTP visibility toggle | ✅ Yes | ❌ No (plain text input) |
| Password confirmation | ✅ Yes | ❌ No |
| Terms checkbox | ✅ Yes | ❌ No |
| Back button | ✅ Yes (OTP screen) | ❌ No |
| Final redirect | /login or /pricing |
/login (hard redirect) |
| Icon library | react-icons/fa | Inline SVG |
State Persistence¶
Important: Email and OTP are kept in state throughout all 3 steps
- Email - Set in step 1, used in steps 2 & 3
- OTP - Set in step 2, used in step 3
- This validates the complete flow (user must complete all steps sequentially)
2. Signup Page¶
File: src/app/signup/page.tsx (534 lines)
Route: /signup
Type: Public
Purpose: User registration with email OTP verification
State Variables (11)¶
- email (
string) - User email address - name (
string) - User's full name - password (
string) - User's chosen password - confirmPassword (
string) - Password confirmation - otp (
string) - 6-digit OTP code - isOtpSent (
boolean) - Tracks if OTP has been sent (changes UI) - isResendAvailable (
boolean) - Controls resend button availability - countdown (
number) - Countdown timer (60 seconds) - showPassword (
boolean) - Toggle password visibility - showConfirmPassword (
boolean) - Toggle confirm password visibility - showOtp (
boolean) - Toggle OTP visibility - termsAccepted (
boolean) - Terms & Privacy checkbox state
Two-Step Registration Flow¶
Step 1: Sign Up Form → Sends OTP
Step 2: OTP Verification → Creates Account
Form Validation¶
Email Validation:
if (!email) {
toast.error("Please enter your email.");
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
toast.error("Please enter a valid email address.");
return;
}
Password Validation:
if (!password) {
toast.error("Please enter a password.");
return;
}
if (password.length < 6) {
toast.error("Password must be at least 6 characters long.");
return;
}
if (password !== confirmPassword) {
toast.error("Passwords do not match!");
return;
}
Terms Acceptance:
if (!termsAccepted) {
toast.error(
"Please accept the Terms of Service and Privacy Policy to continue."
);
return;
}
OTP Countdown Timer¶
Purpose: Prevent OTP spam by limiting resend to once per 60 seconds
Implementation:
useEffect(() => {
let interval: NodeJS.Timeout | undefined;
// Only run the timer if OTP has been sent AND resend is not available
if (isOtpSent && !isResendAvailable && countdown > 0) {
interval = setInterval(() => {
setCountdown((prevCountdown) => {
if (prevCountdown <= 1) {
setIsResendAvailable(true); // Enable resend when countdown finishes
if (interval) clearInterval(interval);
return 0;
}
return prevCountdown - 1;
});
}, 1000);
} else if (countdown === 0) {
setIsResendAvailable(true);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isOtpSent, isResendAvailable, countdown]);
States:
- Initial:
isResendAvailable = true(first send allowed) - After send:
isResendAvailable = false, countdown starts (60s) - When countdown = 0:
isResendAvailable = trueagain
Step 1: Sign Up (Send OTP)¶
API Endpoint: POST /v2/signup
Request:
const formData = new FormData();
formData.append("email", email);
formData.append("password", password);
formData.append("name", name);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/signup`,
{
method: "POST",
body: formData,
}
);
Success Flow:
if (response.ok) {
toast.success("OTP Sent! Please check your email and verify.");
setIsOtpSent(true); // Switch to OTP verification form
// Timer starts automatically via useEffect
}
Error Handling:
else {
const errorMessage = responseData?.message || "This email might already be registered or another error occurred.";
toast.error(errorMessage);
setIsResendAvailable(true); // Allow trying again
}
Countdown Management:
// Disable resend button immediately and start countdown for first send
setIsResendAvailable(false);
setCountdown(60);
Step 2: OTP Verification¶
API Endpoint: POST /v2/verify-otp
Validation:
Request:
const formData = new FormData();
formData.append("email", email);
formData.append("otp", otp);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/verify-otp`,
{
method: "POST",
body: formData,
}
);
Success Flow - With Payment Plan:
if (response.ok) {
const storedPlan = sessionStorage.getItem("selectedPlanForPayment");
if (storedPlan) {
toast.success(
"Account successfully created! Please login to continue with your subscription."
);
setTimeout(() => {
router.push("/login"); // User will be redirected to payment after login
}, 2000);
} else {
// No plan selected - redirect to pricing page
toast.success(
"Account successfully created! Please select a subscription plan to continue."
);
setTimeout(() => {
router.push("/pricing");
}, 2000);
}
}
Payment Flow Integration:
- If user clicked "Subscribe" before signing up, plan is saved in
sessionStorage.selectedPlanForPayment - After OTP verification, user is sent to login
- After login, payment page opens automatically (handled by Login component)
OTP Input Handling¶
Numeric-Only Input:
const handleOtpChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// Only allow numbers and max 6 digits
if (/^\d*$/.test(value) && value.length <= 6) {
setOtp(value);
}
};
Features:
- Numeric keyboard on mobile (
inputMode="numeric") - Pattern validation (
pattern="\d{6}") - Max length 6
- Centered text with letter spacing for readability
- Password-style masking with toggle
Resend OTP¶
Handler:
const handleResendOtp = async () => {
if (!isResendAvailable) return; // Prevent if timer is active
if (!email) {
toast.error("Email not found. Cannot resend OTP.");
return;
}
// Disable button and start countdown
setIsResendAvailable(false);
setCountdown(60);
const formData = new FormData();
formData.append("email", email);
formData.append("password", password);
formData.append("name", name);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/v2/signup`,
{
method: "POST",
body: formData,
}
);
if (response.ok) {
toast.success("OTP Resent! Please check your email.");
} else {
const errorMessage =
responseData?.message || "Failed to resend OTP. Please try again later.";
toast.error(errorMessage);
setIsResendAvailable(true); // Allow trying again
setCountdown(0); // Stop countdown
}
};
Note: Uses same /v2/signup endpoint (not a dedicated /resend-otp endpoint)
UI Features¶
1. Password Visibility Toggles:
<button onClick={() => setShowPassword(!showPassword)}>
{showPassword ? <FaEyeSlash size={18} /> : <FaEye size={18} />}
</button>
- Password field
- Confirm password field -OTP field (unique feature!)
2. Terms & Privacy Checkbox:
<input
type="checkbox"
checked={termsAccepted}
onChange={(e) => setTermsAccepted(e.target.checked)}
/>
<label>
I agree to the{" "}
<Link href="/terms">Terms of Service</Link>{" "}
and{" "}
<Link href="/privacy">Privacy Policy</Link>
</label>
3. Dynamic Submit Button:
<button
className={termsAccepted ? "bg-orange-600" : "bg-gray-500"}
type="submit"
>
Sign up
</button>
- Disabled if terms not accepted
- Visual feedback (gray vs orange)
4. Resend Button with Countdown:
<button
disabled={!isResendAvailable}
className={
!isResendAvailable
? "bg-gray-700 text-gray-400 cursor-not-allowed"
: "bg-gray-600 hover:bg-gray-500"
}
>
{isResendAvailable ? `Resend OTP` : `Resend in ${countdown}s`}
</button>
5. Back Button (OTP Form):
Returns to signup form to change email
Form States¶
Initial State: Signup Form
- Name, email, password, confirm password fields
- Terms checkbox
- "Sign up" button
- "Already have an account? Sign in" link
After OTP Sent: OTP Verification Form
- OTP input (6-digit, masked, numeric)
- "Verify OTP" button
- "Resend OTP" button (with countdown)
- "Entered wrong email? Go Back" button
Environment Variables¶
Dependencies¶
next/link- Navigationnext/navigation(useRouter) - Programmatic routingreact(useState, useEffect)react-icons/fa(FaEye, FaEyeSlash) - Icon libraryreact-toastify- Toast notifications
Security Features¶
- ✅ Email validation - Regex pattern
- ✅ Password strength - Minimum 6 characters
- ✅ Password confirmation - Must match
- ✅ OTP verification - Email confirmation required
- ✅ Rate limiting - 60-second cooldown between OTPs
- ✅ Numeric OTP - Prevents string injection
- ✅ Terms acceptance - Legal compliance
Error Messages¶
| Condition | Error Message |
|---|---|
| Empty email | "Please enter your email." |
| Invalid email | "Please enter a valid email address." |
| Empty password | "Please enter a password." |
| Short password | "Password must be at least 6 characters long." |
| Passwords don't match | "Passwords do not match!" |
| Terms not accepted | "Please accept the Terms of Service and Privacy Policy to continue." |
| OTP not 6 digits | "OTP must be 6 digits!" |
| Duplicate email | "This email might already be registered..." (from backend) |
| OTP verification failed | "OTP verification failed! Please check the code and try again." |
User Flows¶
Flow A: Direct Signup (No Plan)
- User fills signup form
- Clicks "Sign up" → OTP sent to email
- Enters OTP from email
- Clicks "Verify OTP" → Account created
- Redirected to
/pricingto choose plan
Flow B: Signup After Plan Selection
- User selects plan on
/pricing(not logged in) - Plan saved in
sessionStorage.selectedPlanForPayment - Redirected to
/signup - Completes signup + OTP verification
- Redirected to
/login - After login → Automatically redirected to
/paymentwith selected plan
Session Storage Keys¶
| Key | Value | Set When |
|---|---|---|
selectedPlanForPayment |
JSON stringified plan object | User clicks "Subscribe" on pricing page |
"Every line, every flow, every edge case documented."