Files
SerpentRace/Documentations/FRONTEND_IMPLEMENTATION_GUIDE.md
T

66 KiB

SerpentRace Backend API Documentation for Frontend Developers

Table of Contents

  1. Test User Credentials
  2. Data Structures & Entities
  3. Authentication Endpoints
  4. User Management
  5. Deck Management
  6. Organization Management
  7. Chat System
  8. Contact Management
  9. Admin Endpoints
  10. Import/Export Functionality
  11. Error Handling
  12. WebSocket Events

Test User Credentials

For development and testing, use these pre-configured user accounts:

Regular User (Verified)

  • Username: john_doe
  • Password: password123
  • Email: john.doe@email.com
  • Type: Regular user (state: 1 - VERIFIED_REGULAR)
  • Organization: None

Premium User (Organization Member)

  • Username: jane_premium
  • Password: password123
  • Email: jane.smith@email.com
  • Type: Premium user (state: 2 - VERIFIED_PREMIUM)
  • Organization: Tech Solutions Inc

Teacher (Premium Organization Member)

  • Username: teacher_bob
  • Password: password123
  • Email: bob.teacher@eduinst.edu
  • Type: Premium user (state: 2 - VERIFIED_PREMIUM)
  • Organization: Educational Institute

Admin User

  • Username: admin_user
  • Password: password123
  • Email: admin@serpentrace.com
  • Type: Admin (state: 5 - ADMIN)
  • Organization: None

Unverified User

  • Username: new_user
  • Password: password123
  • Email: newuser@email.com
  • Type: Unverified (state: 0 - REGISTERED_NOT_VERIFIED)
  • Organization: None

Data Structures & Entities

User Entity

interface User {
  id: string;                    // UUID
  orgid: string | null;          // Organization ID (if member)
  username: string;              // Unique username
  email: string;                 // Unique email
  fname: string;                 // First name
  lname: string;                 // Last name
  type: string;                  // 'personal' | 'premium' | 'admin'
  phone: string | null;          // Phone number
  state: UserState;              // User status (see enum below)
  regdate: Date;                 // Registration date
  updatedate: Date;              // Last update
  Orglogindate: Date | null;     // Last organization login
}

enum UserState {
  REGISTERED_NOT_VERIFIED = 0,  // Email not verified
  VERIFIED_REGULAR = 1,          // Regular verified user
  VERIFIED_PREMIUM = 2,          // Premium verified user  
  SOFT_DELETE = 3,               // Soft deleted
  DEACTIVATED = 4,               // Account deactivated
  ADMIN = 5                      // Admin user
}

Deck Entity

interface Deck {
  id: string;                    // UUID
  name: string;                  // Deck name
  type: DeckType;                // Deck type (see enum)
  userid: string;                // Owner's user ID
  creationdate: Date;            // Creation timestamp
  cards: Card[];                 // Array of cards
  playedNumber: number;          // Times played
  ctype: DeckVisibility;         // Visibility type
  updatedate: Date;              // Last update
  state: DeckState;              // Deck status
  organization: Organization | null; // Organization reference
}

enum DeckType {
  LUCK = 0,                      // Luck-based cards
  JOKER = 1,                     // Joker/wild cards
  QUESTION = 2                   // Question-based cards
}

enum DeckVisibility {
  PUBLIC = 0,                    // Public to all
  PRIVATE = 1,                   // Private to owner
  ORGANIZATION = 2               // Shared within organization
}

enum DeckState {
  ACTIVE = 0,                    // Active deck
  SOFT_DELETE = 1                // Soft deleted
}

interface Card {
  id: string;                    // Card ID
  type: CardType;                // Type of card
  text: string;                  // Question/prompt text
  answer?: string | boolean | null; // Answer (varies by type)
  options?: string[];            // Multiple choice options
}

enum CardType {
  QUIZ = 0,                      // Multiple choice question
  SENTENCE_PAIRING = 1,          // Sentence completion
  OWN_ANSWER = 2,                // Open-ended question
  TRUE_FALSE = 3,                // True/false question
  CLOSER = 4                     // closer to answer
}

Organization Entity

interface Organization {
  id: string;                    // UUID
  name: string;                  // Organization name
  contactfname: string;          // Contact first name
  contactlname: string;          // Contact last name
  contactphone: string;          // Contact phone
  contactemail: string;          // Contact email
  state: OrganizationState;      // Organization status
  regdate: Date;                 // Registration date
  updatedate: Date;              // Last update
  url: string | null;            // Organization website
  userinorg: number;             // User count in org
  maxOrganizationalDecks: number | null; // Max org decks allowed
}

enum OrganizationState {
  REGISTERED = 0,                // Just registered
  ACTIVE = 1,                    // Active organization
  SOFT_DELETE = 2                // Soft deleted
}

Chat Entity

interface Chat {
  id: string;                    // UUID
  type: ChatType;                // Chat type
  name: string | null;           // Group/game name
  gameId: string | null;         // Associated game ID
  createdBy: string | null;      // Creator's user ID
  users: string[];               // Participant user IDs
  messages: Message[];           // Chat messages
  lastActivity: Date | null;     // Last message time
  createDate: Date;              // Chat creation date
  updateDate: Date;              // Last update
  state: ChatState;              // Chat status
  archiveDate: Date | null;      // Archive date
}

interface Message {
  id: string;                    // Message ID
  date: Date;                    // Message timestamp
  userid: string;                // Sender's user ID
  text: string;                  // Message content
}

enum ChatType {
  DIRECT = 'direct',             // Direct message
  GROUP = 'group',               // Group chat
  GAME = 'game'                  // Game-specific chat
}

enum ChatState {
  ACTIVE = 0,                    // Active chat
  ARCHIVE = 1,                   // Archived chat
  SOFT_DELETE = 2                // Soft deleted
}

Contact Entity

interface Contact {
  id: string;                    // UUID
  name: string;                  // Contact name
  email: string;                 // Contact email
  userid: string | null;         // Associated user ID
  type: ContactType;             // Contact type
  txt: string;                   // Message text
  state: ContactState;           // Contact status
  createDate: Date;              // Creation date
  updateDate: Date;              // Last update
  adminResponse: string | null;  // Admin response
  responseDate: Date | null;     // Response date
  respondedBy: string | null;    // Responding admin ID
}

enum ContactType {
  BUG = 0,                       // Bug report
  PROBLEM = 1,                   // Problem report
  QUESTION = 2,                  // General question
  SALES = 3,                     // Sales inquiry
  OTHER = 4                      // Other type
}

enum ContactState {
  ACTIVE = 0,                    // Active/unresolved
  RESOLVED = 1,                  // Resolved
  SOFT_DELETE = 2                // Soft deleted
}

Authentication Endpoints

User Login

Endpoint: POST /api/users/login

Description: Authenticate user with username or Email and password

Request Data:

{
  username: string;    // 3-50 characters
  password: string;    // 6-100 characters
}

Response Data (Success):

{
  token: string;                 // JWT authentication token its safed in cookie its just a copy
  user: {
    id: string;
    username: string;
    email: string;
    fname: string;
    lname: string;
    type: string;
    state: number;
    orgid: string | null;
  };
  requiresOrgReauth?: boolean;   // If org re-auth needed
  orgLoginUrl?: string;          // Organization auth URL
  organizationName?: string;     // Organization name
}

Error Responses:

  • 401: Invalid credentials, unverified email, or account restrictions
  • 400: Validation error (missing fields, invalid length)
  • 500: Internal server error

Account State Restrictions: Users cannot login if their account state is:

  • 0 (REGISTERED_NOT_VERIFIED): Email verification required
  • 3 (SOFT_DELETE): Account has been deleted
  • 4 (DEACTIVATED): Account has been deactivated

Successful Login States:

  • 1 (VERIFIED_REGULAR): Regular user can login
  • 2 (VERIFIED_PREMIUM): Premium user can login
  • 5 (ADMIN): Admin user can login

Example Usage:

const loginUser = async (username: string, password: string) => {
  const response = await fetch('/api/users/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username, password })
  });
  
  if (response.ok) {
    const data = await response.json();
    localStorage.setItem('auth_token', data.token);
    return data;
  }
  throw new Error('Login failed');
};

User Registration

Endpoint: POST /api/users/create

Description: Create new user account

Request Data:

{
  username: string;    // 3-50 characters, unique
  email: string;       // Valid email format, unique
  password: string;    // 6-100 characters
  fname: string;       // First name
  lname: string;       // Last name
  type?: string;       // 'personal' | 'premium' (default: 'personal')
  phone?: string;      // Optional phone number
}

Response Data (Success):

{
  id: string;
  username: string;
  email: string;
  fname: string;
  lname: string;
  type: string;
  state: 0;           // REGISTERED_NOT_VERIFIED
  regdate: Date;
}

Error Responses:

  • 409: Username or email already exists
  • 400: Validation error
  • 500: Internal server error

User Management

Get User Profile

Endpoint: GET /api/users/profile

Authentication: Required (Bearer token)

Description: Get current user's profile information

Request Data: None (user ID from JWT token)

Response Data:

{
  id: string;
  orgid: string | null;
  username: string;
  email: string;
  fname: string;
  lname: string;
  code: string | null;      // Verification token
  type: string;
  phone: string | null;
  state: number;
}

Update User Profile

Endpoint: PATCH /api/users/profile

Authentication: Required (Bearer token)

Description: Update current user's profile

Request Data:

{
  fname?: string;
  lname?: string;
  email?: string;
  phone?: string;
  // Other updatable fields
}

Response Data: Updated user object (same as GET profile)

Error Responses:

  • 409: Email already exists
  • 400: Validation error
  • 404: User not found

Deck Management

Endpoint: GET /api/decks/page/{from}/{to}

Authentication: Required (Bearer token)

Description: Get user's accessible decks with pagination

URL Parameters:

  • from: Start index (0-based)
  • to: End index (inclusive)

Response Data:

{
  decks: ShortDeckDto[];
  totalCount: number;
}

interface ShortDeckDto {
  id: string;
  name: string;
  type: number;              // DeckType enum
  playedNumber: number;
  ctype: number;             // DeckVisibility enum
}

Deck Access Rules:

  • Regular users: Own private decks + all public decks
  • Premium users: Above + organization decks from their org
  • Admins: All decks

Get All Decks - DEPRECATED

Endpoint: GET /api/decks

Authentication: Required (Bearer token)

Description: Get all accessible decks (deprecated, use paginated version)

Response: Array of ShortDeckDto

Create Deck

Endpoint: POST /api/decks

Authentication: Required (Bearer token)

Description: Create a new deck

Request Data:

{
  name: string;                 // Deck name
  type: DeckType;               // 0=LUCK, 1=JOKER, 2=QUESTION
  cards: Card[];                // Array of cards
  ctype?: DeckVisibility;       // Default: PUBLIC (0)
  userid: string;               // Auto-filled from token
}

Response Data: ShortDeckDto

Deck Limits:

  • Regular users: 8 decks max
  • Premium users: 12 decks max
  • Only premium users can create organizational decks
  • Organization deck limits set by admin

Get Deck by ID

Endpoint: GET /api/decks/{id}

Authentication: Required (Bearer token)

Description: Get detailed deck information

Response Data:

{
  id: string;
  name: string;
  type: number;
  userid: string;
  creationdate: Date;
  cards: Card[];
  playedNumber: number;
  ctype: number;
}

Update Deck

Endpoint: PUT /api/decks/{id}

Authentication: Required (Bearer token)

Description: Update existing deck (owner only)

Request Data: Partial deck data to update

Response Data: Updated ShortDeckDto

Delete Deck

Endpoint: DELETE /api/decks/{id}

Authentication: Required (Bearer token)

Description: Delete deck (owner only)

Response: 204 No Content

Search Decks

Endpoint: GET /api/decks/search

Authentication: Required (Bearer token)

Description: Search decks by name or content

Query Parameters:

  • q: Search query (required)
  • limit: Results limit (1-100, default: 20)
  • offset: Results offset (default: 0)

Response Data: Array of matching decks


Organization Management

Get Organizations (Paginated)

Endpoint: GET /api/organizations/page/{from}/{to}

Authentication: Required (Bearer token)

Description: Get organizations with pagination

Response Data:

{
  organizations: ShortOrganizationDto[];
  totalCount: number;
}

interface ShortOrganizationDto {
  id: string;
  name: string;
  state: number;
  userinorg: number;
  maxOrganizationalDecks?: number | null;
}

Get All Organizations - DEPRECATED

Endpoint: GET /api/organizations

Authentication: Required (Bearer token)

Create Organization

Endpoint: POST /api/organizations

Authentication: Required (Bearer token)

Request Data:

{
  name: string;
  contactfname: string;
  contactlname: string;
  contactphone: string;
  contactemail: string;
  description?: string;
  maxOrganizationalDecks?: number;
}

Get Organization by ID

Endpoint: GET /api/organizations/{id}

Authentication: Required (Bearer token)

Response Data:

{
  id: string;
  name: string;
  contactfname: string;
  contactlname: string;
  contactphone: string;
  contactemail: string;
  state: number;
  regdate: Date;
  updatedate: Date;
  url: string | null;
  userinorg: number;
  maxOrganizationalDecks: number | null;
  users: string[];               // User IDs
}

Update Organization

Endpoint: PUT /api/organizations/{id}

Authentication: Required (Bearer token)

Delete Organization

Endpoint: DELETE /api/organizations/{id}

Authentication: Required (Bearer token)


Chat System

Get User Chats

Endpoint: GET /api/chats/user-chats

Authentication: Required (Bearer token)

Description: Get all chats for current user

Query Parameters:

  • includeArchived: boolean (default: false)

Response Data:

ShortChatDto[]

interface ShortChatDto {
  id: string;
  userCount: number;
  state: number;
}

Get Chat History

Endpoint: GET /api/chats/history/{chatId}

Authentication: Required (Bearer token)

Description: Get detailed chat with message history

Response Data:

{
  id: string;
  users: string[];              // Participant user IDs
  messages: Message[];
  updateDate: Date;
  state: number;
}

Create Chat

Endpoint: POST /api/chats

Authentication: Required (Bearer token)

Request Data:

{
  users: string[];              // Participant user IDs
  type?: string;                // 'direct' | 'group' | 'game'
  name?: string;                // Group/game name
  gameId?: string;              // Associated game ID
}

Send Message

Endpoint: POST /api/chats/{chatId}/message

Authentication: Required (Bearer token)

Request Data:

{
  text: string;                 // Message content
}

Response Data: Message object with generated ID and timestamp


Contact Management

Create Contact

Endpoint: POST /api/contact

Authentication: Optional (can be anonymous)

Description: Submit contact/support request

Request Data:

{
  name: string;                 // Contact name
  email: string;                // Contact email
  type: ContactType;            // 0=BUG, 1=PROBLEM, 2=QUESTION, 3=SALES, 4=OTHER
  txt: string;                  // Message content
}

Response Data:

{
  id: string;
  name: string;
  email: string;
  userid: string | null;        // If authenticated
  type: number;
  txt: string;
  state: 0;                     // ACTIVE
  createDate: Date;
  updateDate: Date;
}

Contact Types:

  • 0 - Bug Report
  • 1 - Problem Report
  • 2 - General Question
  • 3 - Sales Inquiry
  • 4 - Other

Admin Endpoints

All admin endpoints require authentication with admin role (UserState.ADMIN = 5).

Get Users (Paginated)

Endpoint: GET /api/admin/users/page/{from}/{to}

Authentication: Required (Admin only)

Query Parameters:

  • includeDeleted: boolean (default: false)

Response Data:

{
  users: DetailUserDto[];
  pagination: {
    from: number;
    to: number;
    returned: number;
    totalCount: number;
    includeDeleted: boolean;
  };
}

Update User State

Endpoint: PATCH /api/admin/users/{userId}/state

Authentication: Required (Admin only)

Request Data:

{
  state: UserState;             // New user state
}

Get All Contacts

Endpoint: GET /api/admin/contacts

Authentication: Required (Admin only)

Response Data: Array of all contact submissions

Respond to Contact

Endpoint: POST /api/admin/contacts/{contactId}/response

Authentication: Required (Admin only)

Request Data:

{
  response: string;             // Admin response text
}

Import/Export Functionality

Export Deck

Endpoint: GET /api/deck-import-export/export/{deckId}

Authentication: Required (Bearer token)

Description: Export user's own deck as encrypted .spr file

Response: Binary file download

Import Deck

Endpoint: POST /api/deck-import-export/import

Authentication: Required (Bearer token)

Description: Import deck from .spr file

Request: Multipart form data with file

Response Data: Created deck object


Error Handling

Standard Error Response Format

{
  error: string;                // Error message
  details?: string;             // Additional details (validation errors)
  code?: string;                // Error code
}

HTTP Status Codes

  • 200 - Success
  • 201 - Created
  • 204 - No Content
  • 400 - Bad Request (validation error)
  • 401 - Unauthorized (authentication required)
  • 403 - Forbidden (insufficient permissions)
  • 404 - Not Found
  • 409 - Conflict (duplicate data)
  • 500 - Internal Server Error

Common Error Scenarios

Authentication Errors:

// Missing token
{ error: "Authentication required" }

// Invalid token
{ error: "Invalid or expired token" }

// Insufficient permissions
{ error: "Admin access required" }

Validation Errors:

// Missing required fields
{ error: "Missing required fields: username, password" }

// Invalid field length
{ error: "Username must be between 3 and 50 characters" }

// Invalid email format
{ error: "Invalid email format" }

Business Logic Errors:

// Deck limit exceeded
{ error: "Deck limit exceeded. Maximum 8 decks allowed for your account type." }

// Organization deck restrictions
{ error: "Only premium users can create organizational decks." }

// Ownership restrictions
{ error: "You can only modify your own decks." }

WebSocket Real-Time Communication

Connection & Authentication

Connect to WebSocket server with JWT authentication:

import io from 'socket.io-client';

// Connect with JWT token
const socket = io('/', {
  auth: { 
    token: localStorage.getItem('auth_token') || 'your-jwt-token' 
  }
});

// Alternative: Pass token via cookie header
const socket = io('/', {
  extraHeaders: {
    cookie: `auth_token=${localStorage.getItem('auth_token')}`
  }
});

Connection Events:

// Connection successful
socket.on('connect', () => {
  console.log('Connected to WebSocket server');
});

// Authentication failed
socket.on('connect_error', (error) => {
  console.error('WebSocket connection failed:', error.message);
  // Redirect to login or refresh token
});

// Disconnected
socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

Chat Management Events

Initial Chat List:

// Receive user's active chats on connection
socket.on('chats:list', (chats: Array<{
  id: string;
  type: 'direct' | 'group' | 'game';
  name?: string;
  gameId?: string;
  users: string[];
  lastActivity?: Date;
  unreadCount: number;
  isArchived: boolean;
}>) => {
  // Update chat list in UI
  setChatList(chats);
});

Join/Leave Chat:

// Join a chat room
socket.emit('chat:join', { chatId: 'chat-uuid' });

// Confirmation of joining
socket.on('chat:joined', (data: {
  chatId: string;
  messages: Message[]; // Last 10 messages
}) => {
  // Load chat messages into UI
});

// Leave a chat room
socket.emit('chat:leave', { chatId: 'chat-uuid' });

// Confirmation of leaving
socket.on('chat:left', (data: { chatId: string }) => {
  // Update UI to show chat as inactive
});

Send/Receive Messages:

// Send message to chat
socket.emit('message:send', {
  chatId: 'chat-uuid',
  message: 'Hello everyone!'
});

// Receive new message in chat
socket.on('message:received', (data: {
  chatId: string;
  message: {
    id: string;
    date: Date;
    userid: string;
    text: string;
  };
}) => {
  // Add message to chat UI
  addMessageToChat(data.chatId, data.message);
});

Rate Limiting:

  • Maximum 100 messages per user per minute
  • Exceeded limit returns error: { message: "Rate limit exceeded. Maximum 100 messages per minute allowed." }

Chat Creation Events

Create Group Chat:

// Create group (premium users only)
socket.emit('group:create', {
  name: 'Study Group',
  userIds: ['user-1', 'user-2', 'user-3']
});

// Group created successfully
socket.on('group:created', (data: {
  chat: {
    id: string;
    type: 'group';
    name: string;
    createdBy: string;
    users: string[];
    messages: Message[];
  };
}) => {
  // Add new group to chat list
  addChatToList(data.chat);
});

Create Direct Chat:

// Create direct message
socket.emit('chat:direct', {
  targetUserId: 'user-uuid'
});

// Direct chat created
socket.on('chat:direct:created', (data: {
  chat: {
    id: string;
    type: 'direct';
    users: string[];
    messages: Message[];
  };
}) => {
  // Add direct chat to list
});

// Direct chat already exists
socket.on('chat:direct:exists', (data: {
  chatId: string;
}) => {
  // Navigate to existing chat
});

Create Game Chat:

// Create game-specific chat
socket.emit('game:chat:create', {
  gameId: 'game-uuid',
  gameName: 'Quiz Battle',
  playerIds: ['player-1', 'player-2']
});

// Game chat created
socket.on('game:chat:created', (data: {
  chat: {
    id: string;
    type: 'game';
    name: string;
    gameId: string;
    users: string[];
    messages: Message[];
  };
}) => {
  // Show game chat in UI
});

// Game chat already exists
socket.on('game:chat:exists', (data: {
  chatId: string;
}) => {
  // Use existing game chat
});

Chat History & Archive

Get Chat History:

// Request full chat history
socket.emit('chat:history', { chatId: 'chat-uuid' });

// Receive chat history (active chat)
socket.on('chat:history', (data: {
  chatId: string;
  messages: Message[];
  chatInfo: {
    type: 'direct' | 'group' | 'game';
    name?: string;
    gameId?: string;
    users: string[];
  };
}) => {
  // Display full chat history
});

// Receive archived chat history
socket.on('chat:history:archived', (data: {
  chatId: string;
  messages: Message[];
  chatType: 'direct' | 'group' | 'game';
  isGameChat: boolean;
}) => {
  // Display archived chat with read-only UI
});

Message Management & Pruning

Message Retention Rules:

  • All chats: Messages older than 2 weeks are automatically deleted
  • Direct & Game chats: Maximum 10 messages per user kept
  • Group chats: No per-user message limit (time limit only)
  • Archive: Inactive chats (no activity for 30 minutes) are archived
  • Cleanup: Archived messages are cleaned up after 4 weeks

Error Handling

WebSocket Error Events:

// General errors
socket.on('error', (error: { message: string }) => {
  console.error('WebSocket error:', error.message);
  
  // Common error messages:
  // - "Chat not found"
  // - "Unauthorized to join this chat"
  // - "Unauthorized to send message to this chat"
  // - "Message must be a non-empty string"
  // - "Premium subscription required to create groups"
  // - "Group name is required"
  // - "At least one member is required"
  // - "One or more users not found"
  // - "Target user not found"
  // - "Failed to join chat"
  // - "Failed to send message"
  // - "Failed to create group"
  // - "Failed to create direct chat"
  // - "Failed to create game chat"
  // - "Failed to get chat history"
  
  // Handle error in UI
  showErrorMessage(error.message);
});

Complete WebSocket Implementation Example

// hooks/useWebSocket.ts
import { useEffect, useRef, useState } from 'react';
import io, { Socket } from 'socket.io-client';

export const useWebSocket = () => {
  const socketRef = useRef<Socket | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [chats, setChats] = useState<Chat[]>([]);

  useEffect(() => {
    const token = localStorage.getItem('auth_token');
    if (!token) return;

    // Connect to WebSocket
    socketRef.current = io('/', {
      auth: { token }
    });

    const socket = socketRef.current;

    // Connection events
    socket.on('connect', () => {
      console.log('Connected to WebSocket');
      setIsConnected(true);
    });

    socket.on('connect_error', (error) => {
      console.error('Connection failed:', error.message);
      setIsConnected(false);
    });

    socket.on('disconnect', () => {
      console.log('Disconnected from WebSocket');
      setIsConnected(false);
    });

    // Chat events
    socket.on('chats:list', (chatList) => {
      setChats(chatList);
    });

    socket.on('message:received', (data) => {
      setChats(prev => prev.map(chat => 
        chat.id === data.chatId 
          ? { ...chat, messages: [...chat.messages, data.message] }
          : chat
      ));
    });

    socket.on('chat:joined', (data) => {
      console.log('Joined chat:', data.chatId);
    });

    socket.on('error', (error) => {
      console.error('WebSocket error:', error.message);
    });

    return () => {
      socket.disconnect();
    };
  }, []);

  // Helper functions
  const joinChat = (chatId: string) => {
    socketRef.current?.emit('chat:join', { chatId });
  };

  const sendMessage = (chatId: string, message: string) => {
    socketRef.current?.emit('message:send', { chatId, message });
  };

  const createDirectChat = (targetUserId: string) => {
    socketRef.current?.emit('chat:direct', { targetUserId });
  };

  const createGroup = (name: string, userIds: string[]) => {
    socketRef.current?.emit('group:create', { name, userIds });
  };

  return {
    isConnected,
    chats,
    joinChat,
    sendMessage,
    createDirectChat,
    createGroup
  };
};

Rate Limiting & Performance

Message Rate Limits:

  • Maximum 100 messages per user per minute
  • Rate limit counters reset every minute
  • Counters cleaned up after 5 minutes of inactivity

Connection Management:

  • Automatic reconnection on disconnection
  • User status tracking (online/offline)
  • Chat room management with Redis caching
  • Inactive chat archiving after 30 minutes

Performance Considerations:

  • Messages pruned automatically (2 weeks retention)
  • Redis used for active session management
  • Database archiving for inactive chats
  • Efficient room-based message broadcasting

This documentation provides the complete API reference based on the actual backend implementation. All endpoints, data structures, and business rules are derived directly from the TypeScript source code and database schema.


Authentication & Security

JWT Token Management

// lib/auth/tokenManager.ts
export class TokenManager {
  private static readonly TOKEN_KEY = 'serpentrace_auth_token';
  private static readonly REFRESH_KEY = 'serpentrace_refresh_token';

  static setTokens(accessToken: string, refreshToken?: string) {
    localStorage.setItem(this.TOKEN_KEY, accessToken);
    if (refreshToken) {
      localStorage.setItem(this.REFRESH_KEY, refreshToken);
    }
  }

  static getAccessToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  static getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_KEY);
  }

  static clearTokens() {
    localStorage.removeItem(this.TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_KEY);
  }

  static isTokenExpired(token: string): boolean {
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      return payload.exp * 1000 < Date.now();
    } catch {
      return true;
    }
  }
}

Protected Routes

// components/layout/ProtectedRoute.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/store/auth';

interface ProtectedRouteProps {
  children: React.ReactNode;
  requiredRole?: 'admin' | 'premium' | 'verified';
}

export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
  const { user, isAuthenticated, checkAuth } = useAuthStore();
  const router = useRouter();

  useEffect(() => {
    if (!isAuthenticated) {
      checkAuth().then((authenticated) => {
        if (!authenticated) {
          router.push('/login');
        }
      });
    }
  }, [isAuthenticated, checkAuth, router]);

  useEffect(() => {
    if (isAuthenticated && user && requiredRole) {
      const hasAccess = checkUserRole(user, requiredRole);
      if (!hasAccess) {
        router.push('/unauthorized');
      }
    }
  }, [isAuthenticated, user, requiredRole, router]);

  if (!isAuthenticated || !user) {
    return <LoadingSpinner />;
  }

  if (requiredRole && !checkUserRole(user, requiredRole)) {
    return null; // Will redirect in useEffect
  }

  return <>{children}</>;
}

function checkUserRole(user: User, requiredRole: string): boolean {
  switch (requiredRole) {
    case 'admin':
      return user.state === UserState.ADMIN;
    case 'premium':
      return user.state === UserState.VERIFIED_PREMIUM || user.state === UserState.ADMIN;
    case 'verified':
      return user.state >= UserState.VERIFIED_REGULAR;
    default:
      return true;
  }
}

Authentication Context

// store/auth.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { authAPI } from '@/lib/api/auth';
import { TokenManager } from '@/lib/auth/tokenManager';

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (credentials: LoginRequest) => Promise<LoginResponse>;
  register: (userData: RegisterRequest) => Promise<User>;
  logout: () => void;
  checkAuth: () => Promise<boolean>;
  updateUser: (userData: Partial<User>) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set, get) => ({
      user: null,
      isAuthenticated: false,
      isLoading: false,

      login: async (credentials) => {
        set({ isLoading: true });
        try {
          const response = await authAPI.login(credentials);
          TokenManager.setTokens(response.data.token);
          set({
            user: response.data.user,
            isAuthenticated: true,
            isLoading: false,
          });
          return response.data;
        } catch (error) {
          set({ isLoading: false });
          throw error;
        }
      },

      register: async (userData) => {
        set({ isLoading: true });
        try {
          const response = await authAPI.register(userData);
          set({ isLoading: false });
          return response.data;
        } catch (error) {
          set({ isLoading: false });
          throw error;
        }
      },

      logout: () => {
        TokenManager.clearTokens();
        set({
          user: null,
          isAuthenticated: false,
        });
      },

      checkAuth: async () => {
        const token = TokenManager.getAccessToken();
        if (!token || TokenManager.isTokenExpired(token)) {
          set({ isAuthenticated: false, user: null });
          return false;
        }

        try {
          const response = await authAPI.getProfile();
          set({
            user: response.data,
            isAuthenticated: true,
          });
          return true;
        } catch {
          TokenManager.clearTokens();
          set({ isAuthenticated: false, user: null });
          return false;
        }
      },

      updateUser: (userData) => {
        const { user } = get();
        if (user) {
          set({ user: { ...user, ...userData } });
        }
      },
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated }),
    }
  )
);

Core Features Implementation

1. User Registration & Login Forms

// components/forms/LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useAuthStore } from '@/store/auth';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { toast } from '@/lib/toast';

const loginSchema = z.object({
  username: z.string().min(3, 'Username must be at least 3 characters'),
  password: z.string().min(6, 'Password must be at least 6 characters'),
});

type LoginForm = z.infer<typeof loginSchema>;

export function LoginForm() {
  const { login } = useAuthStore();
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginForm>({
    resolver: zodResolver(loginSchema),
  });

  const onSubmit = async (data: LoginForm) => {
    try {
      const response = await login(data);
      
      if (response.requiresOrgReauth) {
        toast.info(`Please complete authentication with ${response.organizationName}`);
        window.location.href = response.orgLoginUrl!;
        return;
      }
      
      toast.success('Login successful!');
      window.location.href = '/dashboard';
    } catch (error: any) {
      toast.error(error.response?.data?.message || 'Login failed');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <Input
          {...register('username')}
          placeholder="Username"
          error={errors.username?.message}
        />
      </div>
      <div>
        <Input
          {...register('password')}
          type="password"
          placeholder="Password"
          error={errors.password?.message}
        />
      </div>
      <Button
        type="submit"
        disabled={isSubmitting}
        className="w-full"
      >
        {isSubmitting ? 'Signing in...' : 'Sign In'}
      </Button>
    </form>
  );
}

2. Deck Creation & Management

// components/features/decks/DeckEditor.tsx
import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { DeckType, DeckVisibility, CardType } from '@/types/deck';

const cardSchema = z.object({
  id: z.string(),
  type: z.nativeEnum(CardType),
  text: z.string().min(1, 'Question text is required'),
  answer: z.union([z.string(), z.boolean(), z.null()]).optional(),
  options: z.array(z.string()).optional(),
});

const deckSchema = z.object({
  name: z.string().min(1, 'Deck name is required').max(255, 'Name too long'),
  type: z.nativeEnum(DeckType),
  ctype: z.nativeEnum(DeckVisibility),
  cards: z.array(cardSchema).min(1, 'At least one card is required').max(50, 'Maximum 50 cards allowed'),
});

type DeckForm = z.infer<typeof deckSchema>;

interface DeckEditorProps {
  initialData?: Deck;
  onSave: (data: CreateDeckRequest) => Promise<void>;
  onCancel: () => void;
}

export function DeckEditor({ initialData, onSave, onCancel }: DeckEditorProps) {
  const {
    register,
    control,
    handleSubmit,
    watch,
    formState: { errors, isSubmitting },
  } = useForm<DeckForm>({
    resolver: zodResolver(deckSchema),
    defaultValues: initialData || {
      name: '',
      type: DeckType.QUESTION,
      ctype: DeckVisibility.PUBLIC,
      cards: [{ id: crypto.randomUUID(), type: CardType.QUIZ, text: '', answer: '', options: ['', '', '', ''] }],
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: 'cards',
  });

  const deckType = watch('type');

  const addCard = () => {
    append({
      id: crypto.randomUUID(),
      type: CardType.QUIZ,
      text: '',
      answer: '',
      options: ['', '', '', ''],
    });
  };

  const onSubmit = async (data: DeckForm) => {
    try {
      await onSave(data);
      toast.success('Deck saved successfully!');
    } catch (error: any) {
      toast.error(error.response?.data?.message || 'Failed to save deck');
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6">
      <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
        {/* Deck Basic Information */}
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-xl font-semibold mb-4">Deck Information</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
            <div>
              <label className="block text-sm font-medium mb-2">Deck Name</label>
              <Input
                {...register('name')}
                placeholder="Enter deck name"
                error={errors.name?.message}
              />
            </div>
            <div>
              <label className="block text-sm font-medium mb-2">Deck Type</label>
              <Select {...register('type')}>
                <option value={DeckType.QUESTION}>Question Deck</option>
                <option value={DeckType.JOKER}>Joker Cards</option>
                <option value={DeckType.LUCK}>Luck Challenges</option>
              </Select>
            </div>
            <div>
              <label className="block text-sm font-medium mb-2">Visibility</label>
              <Select {...register('ctype')}>
                <option value={DeckVisibility.PUBLIC}>Public</option>
                <option value={DeckVisibility.PRIVATE}>Private</option>
                <option value={DeckVisibility.ORGANIZATION}>Organization</option>
              </Select>
            </div>
          </div>
        </div>

        {/* Cards Section */}
        <div className="bg-white rounded-lg shadow p-6">
          <div className="flex justify-between items-center mb-4">
            <h2 className="text-xl font-semibold">Cards ({fields.length})</h2>
            <Button
              type="button"
              onClick={addCard}
              disabled={fields.length >= 50}
              variant="outline"
            >
              Add Card
            </Button>
          </div>

          <div className="space-y-4">
            {fields.map((field, index) => (
              <CardEditor
                key={field.id}
                control={control}
                index={index}
                onRemove={() => remove(index)}
                canRemove={fields.length > 1}
                register={register}
                errors={errors.cards?.[index]}
              />
            ))}
          </div>
        </div>

        {/* Actions */}
        <div className="flex justify-end space-x-4">
          <Button type="button" variant="outline" onClick={onCancel}>
            Cancel
          </Button>
          <Button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Saving...' : 'Save Deck'}
          </Button>
        </div>
      </form>
    </div>
  );
}

3. Real-time Chat Implementation

// hooks/useSocket.ts
import { useEffect, useRef } from 'react';
import { io, Socket } from 'socket.io-client';
import { useAuthStore } from '@/store/auth';
import { TokenManager } from '@/lib/auth/tokenManager';

export function useSocket() {
  const socket = useRef<Socket | null>(null);
  const { user, isAuthenticated } = useAuthStore();

  useEffect(() => {
    if (isAuthenticated && user) {
      socket.current = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:3000', {
        auth: {
          token: TokenManager.getAccessToken(),
        },
      });

      socket.current.on('connect', () => {
        console.log('Connected to WebSocket server');
      });

      socket.current.on('disconnect', () => {
        console.log('Disconnected from WebSocket server');
      });

      return () => {
        socket.current?.disconnect();
      };
    }
  }, [isAuthenticated, user]);

  return socket.current;
}
// components/features/chat/ChatWindow.tsx
import { useState, useEffect, useRef } from 'react';
import { useSocket } from '@/hooks/useSocket';
import { useChatStore } from '@/store/chat';
import { useAuthStore } from '@/store/auth';
import { Message, Chat } from '@/types/chat';

interface ChatWindowProps {
  chatId: string;
  onClose: () => void;
}

export function ChatWindow({ chatId, onClose }: ChatWindowProps) {
  const socket = useSocket();
  const { user } = useAuthStore();
  const { currentChat, messages, sendMessage, loadChat } = useChatStore();
  const [newMessage, setNewMessage] = useState('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    loadChat(chatId);
  }, [chatId, loadChat]);

  useEffect(() => {
    if (!socket) return;

    socket.emit('join-chat', chatId);

    socket.on('new-message', (message: Message) => {
      // Handle incoming messages
      useChatStore.getState().addMessage(chatId, message);
    });

    socket.on('user-typing', (data: { userId: string; isTyping: boolean }) => {
      // Handle typing indicators
    });

    return () => {
      socket.off('new-message');
      socket.off('user-typing');
      socket.emit('leave-chat', chatId);
    };
  }, [socket, chatId]);

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  const handleSendMessage = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!newMessage.trim() || !user) return;

    const message: Omit<Message, 'id' | 'date'> = {
      userid: user.id,
      text: newMessage.trim(),
    };

    try {
      await sendMessage(chatId, message);
      setNewMessage('');
    } catch (error) {
      toast.error('Failed to send message');
    }
  };

  if (!currentChat) {
    return <div className="p-4">Loading chat...</div>;
  }

  return (
    <div className="flex flex-col h-96 bg-white rounded-lg shadow-lg">
      {/* Chat Header */}
      <div className="flex items-center justify-between p-4 border-b">
        <div>
          <h3 className="font-semibold">
            {currentChat.name || `Chat with ${currentChat.users.length} users`}
          </h3>
          <p className="text-sm text-gray-500">{currentChat.type} chat</p>
        </div>
        <Button variant="ghost" size="sm" onClick={onClose}>
          
        </Button>
      </div>

      {/* Messages */}
      <div className="flex-1 overflow-y-auto p-4 space-y-3">
        {messages.map((message) => (
          <div
            key={message.id}
            className={`flex ${
              message.userid === user?.id ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
                message.userid === user?.id
                  ? 'bg-blue-500 text-white'
                  : 'bg-gray-200 text-gray-800'
              }`}
            >
              <p className="text-sm">{message.text}</p>
              <p className="text-xs opacity-75 mt-1">
                {new Date(message.date).toLocaleTimeString()}
              </p>
            </div>
          </div>
        ))}
        <div ref={messagesEndRef} />
      </div>

      {/* Message Input */}
      <form onSubmit={handleSendMessage} className="p-4 border-t">
        <div className="flex space-x-2">
          <Input
            value={newMessage}
            onChange={(e) => setNewMessage(e.target.value)}
            placeholder="Type a message..."
            className="flex-1"
          />
          <Button type="submit" disabled={!newMessage.trim()}>
            Send
          </Button>
        </div>
      </form>
    </div>
  );
}

UI/UX Guidelines

Design System

// components/ui/Button.tsx
import { cn } from '@/lib/utils';
import { ButtonHTMLAttributes, forwardRef } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'default' | 'outline' | 'ghost' | 'destructive';
  size?: 'sm' | 'md' | 'lg';
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant = 'default', size = 'md', ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={cn(
          // Base styles
          'inline-flex items-center justify-center rounded-md font-medium transition-colors',
          'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
          'disabled:opacity-50 disabled:pointer-events-none',
          
          // Variants
          {
            'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'default',
            'border border-input hover:bg-accent hover:text-accent-foreground': variant === 'outline',
            'hover:bg-accent hover:text-accent-foreground': variant === 'ghost',
            'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
          },
          
          // Sizes
          {
            'h-9 px-3 text-sm': size === 'sm',
            'h-10 px-4 py-2': size === 'md',
            'h-11 px-6 text-lg': size === 'lg',
          },
          
          className
        )}
        {...props}
      />
    );
  }
);

Button.displayName = 'Button';

Theme Configuration

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        // Brand colors
        primary: {
          50: '#f0f9ff',
          500: '#3b82f6',
          900: '#1e3a8a',
        },
        // Semantic colors
        success: '#10b981',
        warning: '#f59e0b',
        error: '#ef4444',
        // UI colors
        background: '#ffffff',
        foreground: '#0f172a',
        muted: '#f8fafc',
        border: '#e2e8f0',
      },
      fontFamily: {
        sans: ['Inter', 'sans-serif'],
      },
      animation: {
        'fade-in': 'fadeIn 0.3s ease-in-out',
        'slide-up': 'slideUp 0.3s ease-in-out',
      },
    },
  },
  plugins: [require('@tailwindcss/forms')],
};

Responsive Layout Components

// components/layout/DashboardLayout.tsx
interface DashboardLayoutProps {
  children: React.ReactNode;
  sidebar?: React.ReactNode;
}

export function DashboardLayout({ children, sidebar }: DashboardLayoutProps) {
  return (
    <div className="min-h-screen bg-gray-50">
      <Navigation />
      
      <div className="flex">
        {sidebar && (
          <aside className="hidden lg:block w-64 bg-white shadow-sm">
            <div className="p-6">
              {sidebar}
            </div>
          </aside>
        )}
        
        <main className="flex-1 p-6">
          <div className="max-w-7xl mx-auto">
            {children}
          </div>
        </main>
      </div>
    </div>
  );
}

State Management

Redux Store Configuration

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { apiSlice } from './api/apiSlice';
import deckReducer from './slices/deckSlice';
import uiReducer from './slices/uiSlice';

export const store = configureStore({
  reducer: {
    api: apiSlice.reducer,
    decks: deckReducer,
    ui: uiReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
});

setupListeners(store.dispatch);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

RTK Query API Slice

// store/api/apiSlice.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { TokenManager } from '@/lib/auth/tokenManager';

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({
    baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api',
    prepareHeaders: (headers) => {
      const token = TokenManager.getAccessToken();
      if (token) {
        headers.set('Authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  tagTypes: ['User', 'Deck', 'Organization', 'Chat', 'Contact'],
  endpoints: () => ({}),
});
// store/api/deckApi.ts
import { apiSlice } from './apiSlice';
import { Deck, CreateDeckRequest } from '@/types/deck';

export const deckApi = apiSlice.injectEndpoints({
  endpoints: (builder) => ({
    getDecks: builder.query<{decks: Deck[], totalCount: number}, {
      from?: number;
      to?: number;
      userId?: string;
    }>({
      query: (params) => ({
        url: '/decks',
        params,
      }),
      providesTags: ['Deck'],
    }),

    getDeck: builder.query<Deck, string>({
      query: (id) => `/decks/${id}`,
      providesTags: (result, error, id) => [{ type: 'Deck', id }],
    }),

    createDeck: builder.mutation<Deck, CreateDeckRequest>({
      query: (deckData) => ({
        url: '/decks',
        method: 'POST',
        body: deckData,
      }),
      invalidatesTags: ['Deck'],
    }),

    updateDeck: builder.mutation<Deck, { id: string; data: Partial<CreateDeckRequest> }>({
      query: ({ id, data }) => ({
        url: `/decks/${id}`,
        method: 'PUT',
        body: data,
      }),
      invalidatesTags: (result, error, { id }) => [{ type: 'Deck', id }],
    }),

    deleteDeck: builder.mutation<void, string>({
      query: (id) => ({
        url: `/decks/${id}`,
        method: 'DELETE',
      }),
      invalidatesTags: ['Deck'],
    }),
  }),
});

export const {
  useGetDecksQuery,
  useGetDeckQuery,
  useCreateDeckMutation,
  useUpdateDeckMutation,
  useDeleteDeckMutation,
} = deckApi;

Real-time Features

WebSocket Integration

// lib/socket/socketManager.ts
import { io, Socket } from 'socket.io-client';
import { TokenManager } from '@/lib/auth/tokenManager';

class SocketManager {
  private socket: Socket | null = null;
  private listeners: Map<string, Function[]> = new Map();

  connect() {
    if (this.socket?.connected) return;

    this.socket = io(process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:3000', {
      auth: {
        token: TokenManager.getAccessToken(),
      },
    });

    this.socket.on('connect', () => {
      console.log('Socket connected');
      this.emit('connection', true);
    });

    this.socket.on('disconnect', () => {
      console.log('Socket disconnected');
      this.emit('connection', false);
    });

    // Set up event forwarding
    this.setupEventForwarding();
  }

  disconnect() {
    this.socket?.disconnect();
    this.socket = null;
  }

  on(event: string, callback: Function) {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }

  off(event: string, callback: Function) {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      const index = eventListeners.indexOf(callback);
      if (index > -1) {
        eventListeners.splice(index, 1);
      }
    }
  }

  emit(event: string, data?: any) {
    this.socket?.emit(event, data);
    
    // Also emit to local listeners
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      eventListeners.forEach(callback => callback(data));
    }
  }

  private setupEventForwarding() {
    if (!this.socket) return;

    const events = [
      'new-message',
      'user-typing',
      'user-online',
      'user-offline',
      'game-update',
      'deck-shared',
    ];

    events.forEach(event => {
      this.socket!.on(event, (data) => {
        this.emit(event, data);
      });
    });
  }
}

export const socketManager = new SocketManager();

Real-time Notifications

// components/features/notifications/NotificationCenter.tsx
import { useEffect, useState } from 'react';
import { socketManager } from '@/lib/socket/socketManager';
import { toast } from '@/lib/toast';

interface Notification {
  id: string;
  type: 'message' | 'deck-shared' | 'game-invite' | 'system';
  title: string;
  message: string;
  timestamp: Date;
  read: boolean;
}

export function NotificationCenter() {
  const [notifications, setNotifications] = useState<Notification[]>([]);
  const [isOpen, setIsOpen] = useState(false);

  useEffect(() => {
    const handleNewMessage = (data: any) => {
      const notification: Notification = {
        id: crypto.randomUUID(),
        type: 'message',
        title: 'New Message',
        message: `New message from ${data.senderName}`,
        timestamp: new Date(),
        read: false,
      };
      
      setNotifications(prev => [notification, ...prev]);
      toast.info(notification.message);
    };

    const handleDeckShared = (data: any) => {
      const notification: Notification = {
        id: crypto.randomUUID(),
        type: 'deck-shared',
        title: 'Deck Shared',
        message: `${data.sharedBy} shared a deck with you: ${data.deckName}`,
        timestamp: new Date(),
        read: false,
      };
      
      setNotifications(prev => [notification, ...prev]);
      toast.success(notification.message);
    };

    socketManager.on('new-message', handleNewMessage);
    socketManager.on('deck-shared', handleDeckShared);

    return () => {
      socketManager.off('new-message', handleNewMessage);
      socketManager.off('deck-shared', handleDeckShared);
    };
  }, []);

  const unreadCount = notifications.filter(n => !n.read).length;

  const markAsRead = (id: string) => {
    setNotifications(prev =>
      prev.map(n => n.id === id ? { ...n, read: true } : n)
    );
  };

  return (
    <div className="relative">
      <Button
        variant="ghost"
        onClick={() => setIsOpen(!isOpen)}
        className="relative"
      >
        🔔
        {unreadCount > 0 && (
          <span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">
            {unreadCount}
          </span>
        )}
      </Button>

      {isOpen && (
        <div className="absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border z-50">
          <div className="p-4 border-b">
            <h3 className="font-semibold">Notifications</h3>
          </div>
          
          <div className="max-h-96 overflow-y-auto">
            {notifications.length === 0 ? (
              <div className="p-4 text-center text-gray-500">
                No notifications
              </div>
            ) : (
              notifications.map(notification => (
                <div
                  key={notification.id}
                  className={`p-4 border-b cursor-pointer hover:bg-gray-50 ${
                    !notification.read ? 'bg-blue-50' : ''
                  }`}
                  onClick={() => markAsRead(notification.id)}
                >
                  <div className="flex justify-between items-start">
                    <div>
                      <h4 className="font-medium">{notification.title}</h4>
                      <p className="text-sm text-gray-600">{notification.message}</p>
                    </div>
                    {!notification.read && (
                      <div className="w-2 h-2 bg-blue-500 rounded-full"></div>
                    )}
                  </div>
                  <p className="text-xs text-gray-400 mt-2">
                    {notification.timestamp.toLocaleString()}
                  </p>
                </div>
              ))
            )}
          </div>
        </div>
      )}
    </div>
  );
}

Performance Optimization

Code Splitting & Lazy Loading

// app/dashboard/decks/page.tsx
import dynamic from 'next/dynamic';
import { Suspense } from 'react';

const DeckManagement = dynamic(() => import('@/components/features/decks/DeckManagement'), {
  loading: () => <DeckManagementSkeleton />,
});

export default function DecksPage() {
  return (
    <Suspense fallback={<DeckManagementSkeleton />}>
      <DeckManagement />
    </Suspense>
  );
}

Virtual Scrolling for Large Lists

// components/ui/VirtualizedList.tsx
import { FixedSizeList as List } from 'react-window';

interface VirtualizedListProps<T> {
  items: T[];
  height: number;
  itemHeight: number;
  renderItem: ({ index, style }: { index: number; style: React.CSSProperties }) => JSX.Element;
}

export function VirtualizedList<T>({ items, height, itemHeight, renderItem }: VirtualizedListProps<T>) {
  return (
    <List
      height={height}
      itemCount={items.length}
      itemSize={itemHeight}
      itemData={items}
    >
      {renderItem}
    </List>
  );
}

Memoization & Optimization

// hooks/useOptimizedDecks.ts
import { useMemo } from 'react';
import { useGetDecksQuery } from '@/store/api/deckApi';
import { Deck, DeckType, DeckVisibility } from '@/types/deck';

interface DeckFilters {
  search?: string;
  type?: DeckType;
  visibility?: DeckVisibility;
  sortBy?: 'name' | 'date' | 'plays';
  sortOrder?: 'asc' | 'desc';
}

export function useOptimizedDecks(filters: DeckFilters = {}) {
  const { data, isLoading, error } = useGetDecksQuery({
    from: 0,
    to: 100, // Adjust based on pagination needs
  });

  const filteredAndSortedDecks = useMemo(() => {
    if (!data?.decks) return [];

    let filtered = data.decks;

    // Apply search filter
    if (filters.search) {
      const searchLower = filters.search.toLowerCase();
      filtered = filtered.filter(deck =>
        deck.name.toLowerCase().includes(searchLower) ||
        deck.cards.some(card => card.text.toLowerCase().includes(searchLower))
      );
    }

    // Apply type filter
    if (filters.type !== undefined) {
      filtered = filtered.filter(deck => deck.type === filters.type);
    }

    // Apply visibility filter
    if (filters.visibility !== undefined) {
      filtered = filtered.filter(deck => deck.ctype === filters.visibility);
    }

    // Apply sorting
    if (filters.sortBy) {
      filtered.sort((a, b) => {
        let aValue, bValue;
        
        switch (filters.sortBy) {
          case 'name':
            aValue = a.name.toLowerCase();
            bValue = b.name.toLowerCase();
            break;
          case 'date':
            aValue = new Date(a.creationdate).getTime();
            bValue = new Date(b.creationdate).getTime();
            break;
          case 'plays':
            aValue = a.playedNumber;
            bValue = b.playedNumber;
            break;
          default:
            return 0;
        }

        if (filters.sortOrder === 'desc') {
          return aValue < bValue ? 1 : -1;
        }
        return aValue > bValue ? 1 : -1;
      });
    }

    return filtered;
  }, [data?.decks, filters]);

  return {
    decks: filteredAndSortedDecks,
    totalCount: data?.totalCount || 0,
    isLoading,
    error,
  };
}

Testing Strategy

Component Testing

// components/forms/__tests__/LoginForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { LoginForm } from '../LoginForm';
import { useAuthStore } from '@/store/auth';

// Mock the auth store
jest.mock('@/store/auth');
const mockUseAuthStore = useAuthStore as jest.MockedFunction<typeof useAuthStore>;

describe('LoginForm', () => {
  const mockLogin = jest.fn();

  beforeEach(() => {
    mockUseAuthStore.mockReturnValue({
      login: mockLogin,
      user: null,
      isAuthenticated: false,
      isLoading: false,
      register: jest.fn(),
      logout: jest.fn(),
      checkAuth: jest.fn(),
      updateUser: jest.fn(),
    });
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('renders login form fields', () => {
    render(<LoginForm />);

    expect(screen.getByPlaceholderText('Username')).toBeInTheDocument();
    expect(screen.getByPlaceholderText('Password')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
  });

  it('validates required fields', async () => {
    render(<LoginForm />);

    fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

    await waitFor(() => {
      expect(screen.getByText('Username must be at least 3 characters')).toBeInTheDocument();
      expect(screen.getByText('Password must be at least 6 characters')).toBeInTheDocument();
    });
  });

  it('submits form with valid credentials', async () => {
    mockLogin.mockResolvedValue({
      token: 'mock-token',
      user: { id: '1', username: 'testuser' },
      requiresOrgReauth: false,
    });

    render(<LoginForm />);

    fireEvent.change(screen.getByPlaceholderText('Username'), {
      target: { value: 'testuser' },
    });
    fireEvent.change(screen.getByPlaceholderText('Password'), {
      target: { value: 'password123' },
    });

    fireEvent.click(screen.getByRole('button', { name: /sign in/i }));

    await waitFor(() => {
      expect(mockLogin).toHaveBeenCalledWith({
        username: 'testuser',
        password: 'password123',
      });
    });
  });
});

API Testing

// lib/api/__tests__/deckApi.test.ts
import { deckApi } from '@/store/api/deckApi';
import { store } from '@/store';
import { setupApiMocking } from '@/test-utils/apiMocking';

const { server } = setupApiMocking();

describe('deckApi', () => {
  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  it('fetches decks successfully', async () => {
    const mockDecks = [
      { id: '1', name: 'Test Deck 1', type: 0 },
      { id: '2', name: 'Test Deck 2', type: 1 },
    ];

    const result = await store.dispatch(
      deckApi.endpoints.getDecks.initiate({ from: 0, to: 10 })
    );

    expect(result.data).toEqual({
      decks: mockDecks,
      totalCount: 2,
    });
  });

  it('creates deck successfully', async () => {
    const newDeck = {
      name: 'New Deck',
      type: 2,
      cards: [{ id: '1', type: 0, text: 'Test question' }],
    };

    const result = await store.dispatch(
      deckApi.endpoints.createDeck.initiate(newDeck)
    );

    expect(result.data).toMatchObject(newDeck);
  });
});

Deployment Guide

Environment Configuration

# .env.local
NEXT_PUBLIC_API_URL=https://api.serpentrace.com
NEXT_PUBLIC_WS_URL=wss://api.serpentrace.com
NEXTAUTH_URL=https://serpentrace.com
NEXTAUTH_SECRET=your-secret-key-here

Docker Configuration

# Dockerfile
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
COPY --from=deps /app/node_modules ./node_modules
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run test
      - run: npm run build

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Deploy to production
        run: |
          # Add your deployment commands here
          echo "Deploying to production..."

Security Considerations

Content Security Policy

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: `
      default-src 'self';
      script-src 'self' 'unsafe-eval' 'unsafe-inline';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data: https:;
      connect-src 'self' wss: ws:;
    `.replace(/\s{2,}/g, ' ').trim()
  },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

Input Sanitization

// lib/security/sanitize.ts
import DOMPurify from 'dompurify';

export function sanitizeHtml(dirty: string): string {
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'],
    ALLOWED_ATTR: [],
  });
}

export function sanitizeText(text: string): string {
  return text
    .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
    .replace(/[<>]/g, '')
    .trim();
}

This comprehensive frontend documentation provides a complete guide for implementing a modern, scalable, and secure frontend for the SerpentRace platform. The documentation covers all major aspects from setup to deployment, following best practices and industry standards.