Skip to content

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 wrapper
  • KeysecondCard - Action card component for each chatbot type
  • Next/Image - Optimized image display

Layout Structure

3 Chatbot Type Cards:

  1. Text Chatbot

  2. Image: chatbotImg (chatbott.png)

  3. Icon: avtarimg in orange circle
  4. Component: <KeysecondCard type="text" />

  5. Voice Chatbot

  6. Image: voiceImg (voice.png)

  7. Icon: avtarimg in orange circle
  8. Component: <KeysecondCard type="voice" />

  9. 3D Chatbot

  10. Image: avatarImg (chat-new.png)
  11. Icon: avtarimg in orange circle
  12. 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:

  1. chatbots (ChatbotData[]) - Active chatbots list
  2. trashedChatbots (TrashedChatbotData[]) - Deleted chatbots (7-day window)
  3. sharedChatbots (SharedChatbot[]) - Chatbots shared with user
  4. 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:

  1. POST /v2/create-chatbot - Create new chatbot project
  2. POST /v2/select-chatbot - Select chatbot type (3D/text/voice)
  3. GET /v2/get-chatbots?user_id={id} - Get user's project list
  4. GET /v2/get-chatbot-selection?user_id={id}&project_id={id} - Get chatbot details
  5. DELETE /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 only
  • chatbot_editor - Edit permissions
  • chatbot_analytics - Analytics access
  • Remove own access option
  • URL includes role parameter

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_id
  • chatbot_type
  • user_id
  • selectedPurpose
  • avatarType
  • selection_avatar
  • selection_voice
  • user_role (for shared chatbots)
  • isSharedAccess (for shared chatbots)
  • continueChatbot (true/false)

Keys Removed on Create:

  • chatbotTitle
  • chatbot_name
  • qa_completed
  • chatbot_purpose2

Components Used

  • ShareModal - Chatbot sharing interface
  • Image (Next.js) - Optimized images
  • ToastContainer - Notifications
  • Various avatar images (Eva, Shayla, Myra, Chris, Jack, Anu, Emma)
  • Inline SVG icons (trash, restore, delete, share, info, etc.)

Security Features

  1. JWT Authentication - All API calls require auth token
  2. User-ID Header - Validates user ownership
  3. Project-ID Header - Additional validation
  4. Role-based access - Shared chatbot permissions
  5. Trash recovery window - 7-day soft delete
  6. Confirmation modals - Prevent accidental deletions

Performance Optimizations

  1. Parallel API Calls:
const [desktopResponse, mobileResponse, promptResponse] = await Promise.all([...]);
  1. Sorted Display: Pre-sorted by date (newest first)

  2. Conditional Rendering: Only load sections with data

  3. 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:

NEXT_PUBLIC_API_BASE_URL=https://api.machineavatars.com

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)

  1. email (string) - User's email address
  2. otp (string) - 6-digit OTP code
  3. newPassword (string) - New password to set
  4. step (number) - Current step (1, 2, or 3)
  5. error (string) - Error message display
  6. message (string) - Success message display
  7. timer (number) - Countdown for OTP resend (60 seconds)
  8. 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:

if (!email) {
  setError("Please enter your email.");
  return;
}

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:

if (!otp) {
  setError("Please enter the OTP.");
  return;
}

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:

if (!newPassword) {
  setError("Please enter a new password.");
  return;
}

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

<h2>
  {step === 1
    ? "Forgot Password"
    : step === 2
    ? "Verify OTP"
    : "Reset Password"}
</h2>

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):

FormData {
  email: string
}

Step 2 (Verify OTP):

FormData {
  email: string,
  otp: string
}

Step 3 (Reset Password):

FormData {
  email: string,
  otp: string,
  new_password: string
}

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

  1. OTP verification - Email ownership confirmation
  2. Rate limiting - 60-second cooldown between OTPs
  3. Password masking - Hidden by default with toggle
  4. OTP in memory - Required for final password reset (validates entire flow)
  5. Auto-redirect - Forces login after password reset

Dependencies

  • next/link - Navigation
  • react (useState, useEffect)
  • No external icon library (uses inline SVG)

Environment Variables

NEXT_PUBLIC_API_BASE_URL=https://api.machineavatars.com

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)

  1. email (string) - User email address
  2. name (string) - User's full name
  3. password (string) - User's chosen password
  4. confirmPassword (string) - Password confirmation
  5. otp (string) - 6-digit OTP code
  6. isOtpSent (boolean) - Tracks if OTP has been sent (changes UI)
  7. isResendAvailable (boolean) - Controls resend button availability
  8. countdown (number) - Countdown timer (60 seconds)
  9. showPassword (boolean) - Toggle password visibility
  10. showConfirmPassword (boolean) - Toggle confirm password visibility
  11. showOtp (boolean) - Toggle OTP visibility
  12. 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 = true again

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:

if (otp.length !== 6) {
  toast.error("OTP must be 6 digits!");
  return;
}

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):

<button onClick={() => setIsOtpSent(false)}>
  Entered wrong email? Go Back
</button>

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

NEXT_PUBLIC_API_BASE_URL=https://api.machineavatars.com

Dependencies

  • next/link - Navigation
  • next/navigation (useRouter) - Programmatic routing
  • react (useState, useEffect)
  • react-icons/fa (FaEye, FaEyeSlash) - Icon library
  • react-toastify - Toast notifications

Security Features

  1. Email validation - Regex pattern
  2. Password strength - Minimum 6 characters
  3. Password confirmation - Must match
  4. OTP verification - Email confirmation required
  5. Rate limiting - 60-second cooldown between OTPs
  6. Numeric OTP - Prevents string injection
  7. 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)

  1. User fills signup form
  2. Clicks "Sign up" → OTP sent to email
  3. Enters OTP from email
  4. Clicks "Verify OTP" → Account created
  5. Redirected to /pricing to choose plan

Flow B: Signup After Plan Selection

  1. User selects plan on /pricing (not logged in)
  2. Plan saved in sessionStorage.selectedPlanForPayment
  3. Redirected to /signup
  4. Completes signup + OTP verification
  5. Redirected to /login
  6. After login → Automatically redirected to /payment with 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."