From eb85e68fc21f844e0ed16d0a416bea56fa008e88 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 22 Aug 2025 22:01:51 +0300 Subject: [PATCH] Add documentation --- .cursor/rules/docs.mdc | 11 ++ frontend/index.html | 15 +- frontend/src/auth.ts | 152 +++++++++++++++--- frontend/src/chat.ts | 36 ++++- frontend/src/config.ts | 24 +++ frontend/src/init.ts | 8 +- frontend/src/leftpanel.ts | 93 +++++++---- frontend/src/links.ts | 11 -- frontend/src/main.ts | 10 +- frontend/src/profile.ts | 15 +- frontend/src/profile/README.md | 118 -------------- frontend/src/profile/api.ts | 47 ++++++ frontend/src/profile/editor.ts | 122 +++++++++++---- frontend/src/profile/image-cropper.ts | 107 +++++++++++++ frontend/src/profile/types.ts | 19 +++ frontend/src/profile/upload.ts | 187 ++++++++++++++--------- frontend/src/settings.ts | 125 +++++++++------ frontend/src/types.ts | 90 ++++++++++- frontend/src/utils.ts | 12 -- frontend/src/{ => utils}/material.ts | 7 + frontend/src/{ => utils}/notification.ts | 32 ++++ frontend/src/utils/utils.ts | 33 ++++ frontend/src/websocket.ts | 21 ++- 23 files changed, 931 insertions(+), 364 deletions(-) create mode 100644 .cursor/rules/docs.mdc delete mode 100644 frontend/src/links.ts delete mode 100644 frontend/src/profile/README.md delete mode 100644 frontend/src/utils.ts rename frontend/src/{ => utils}/material.ts (76%) rename frontend/src/{ => utils}/notification.ts (55%) create mode 100644 frontend/src/utils/utils.ts diff --git a/.cursor/rules/docs.mdc b/.cursor/rules/docs.mdc new file mode 100644 index 0000000..4b13faf --- /dev/null +++ b/.cursor/rules/docs.mdc @@ -0,0 +1,11 @@ +--- +description: Documentation rules +alwaysApply: false +--- + +When documenting this project, follow these rules: + +1. For TS, use JSDoc. +2. When something is self-explanatory or is a constant for a HTML element, don't document it. +3. Do NOT change the structure of the code, only add documentation. +4. When filling in the author field, say that you are Cursor. \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html index 93cd4aa..17d1d18 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -120,7 +120,7 @@
Loading...
@@ -139,10 +139,10 @@ - + - + @@ -159,17 +159,18 @@
-
+
Avatar
-

Общий чат

+

Общий чат

- Онлайн + + Онлайн

- Свернуть чат + Свернуть чат
diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index 7a355da..98a01ec 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -1,14 +1,36 @@ +/** + * @fileoverview Authentication system implementation + * @description Handles user authentication, registration, and session management + * @author Cursor + * @version 1.0.0 + */ + import { loadMessages } from "./chat"; import { initializeProfile } from "./profile"; import type { Headers, ErrorResponse, User, LoginResponse, LoginRequest, RegisterRequest } from "./types"; import { API_BASE_URL } from "./config"; -// Authentication and navigation handling +/** + * Current authenticated user information + * @type {User | null} + */ export let currentUser: User | null = null; + +/** + * JWT authentication token + * @type {string | null} + */ export let authToken: string | null = null; - -// Helper function to get auth headers +/** + * Generates authentication headers for API requests + * @param {boolean} json - Whether to include JSON content type header + * @returns {Headers} Headers object with authentication and content type + * @function getAuthHeaders + * @example + * const headers = getAuthHeaders(); + * fetch('/api/endpoint', { headers }); + */ export function getAuthHeaders(json: boolean = true): Headers { const headers: Headers = {}; @@ -22,38 +44,65 @@ export function getAuthHeaders(json: boolean = true): Headers { return headers; } -// Show login form -export function showLogin() { +/** + * Shows the login form and hides other interfaces + * @function showLogin + * @example + * showLogin(); + */ +export function showLogin(): void { document.getElementById('login-form')!.style.display = 'flex'; document.getElementById('register-form')!.style.display = 'none'; document.getElementById('chat-interface')!.style.display = 'none'; clearAlerts(); } -// Show register form -export function showRegister() { +/** + * Shows the registration form and hides other interfaces + * @function showRegister + * @example + * showRegister(); + */ +export function showRegister(): void { document.getElementById('login-form')!.style.display = 'none'; document.getElementById('register-form')!.style.display = 'flex'; document.getElementById('chat-interface')!.style.display = 'none'; clearAlerts(); } -// Show chat interface -export function showChat() { +/** + * Shows the chat interface and hides authentication forms + * @function showChat + * @example + * showChat(); + */ +export function showChat(): void { document.getElementById('login-form')!.style.display = 'none'; document.getElementById('register-form')!.style.display = 'none'; document.getElementById('chat-interface')!.style.display = 'block'; loadMessages(); } -// Clear all alerts -export function clearAlerts() { +/** + * Clears all alert messages from authentication forms + * @function clearAlerts + * @private + */ +export function clearAlerts(): void { document.getElementById('login-alerts')!.innerHTML = ''; document.getElementById('register-alerts')!.innerHTML = ''; } -// Show alert message -export function showAlert(containerId: string, message: string, type: "success" | "danger" = 'danger') { +/** + * Shows an alert message in the specified container + * @param {string} containerId - ID of the container to show the alert in + * @param {string} message - Alert message to display + * @param {'success' | 'danger'} type - Type of alert (success or danger) + * @function showAlert + * @example + * showAlert('login-alerts', 'Login successful!', 'success'); + */ +export function showAlert(containerId: string, message: string, type: "success" | "danger" = 'danger'): void { const container = document.getElementById(containerId)!; const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type}`; @@ -61,8 +110,14 @@ export function showAlert(containerId: string, message: string, type: "success" container.appendChild(alertDiv); } -// Handle login form submission -document.getElementById('login-form-element')!.addEventListener('submit', async (e) => { +/** + * Handles login form submission + * @async + * @function handleLogin + * @param {Event} e - Form submission event + * @private + */ +async function handleLogin(e: Event): Promise { e.preventDefault(); const usernameElement = document.getElementById('login-username') as HTMLInputElement; @@ -105,10 +160,16 @@ document.getElementById('login-form-element')!.addEventListener('submit', async } catch (error) { showAlert('login-alerts', 'Ошибка соединения с сервером', 'danger'); } -}); +} -// Handle register form submission -document.getElementById('register-form-element')!.addEventListener('submit', async (e) => { +/** + * Handles registration form submission + * @async + * @function handleRegister + * @param {Event} e - Form submission event + * @private + */ +async function handleRegister(e: Event): Promise { e.preventDefault(); const usernameElement = document.getElementById('register-username') as HTMLInputElement; @@ -167,10 +228,16 @@ document.getElementById('register-form-element')!.addEventListener('submit', asy } catch (error) { showAlert('register-alerts', 'Ошибка соединения с сервером', 'danger'); } -}); +} -// Handle logout -export async function logout() { +/** + * Logs out the current user and clears session data + * @async + * @function logout + * @example + * await logout(); + */ +export async function logout(): Promise { try { await fetch(`${API_BASE_URL}/logout`, { method: 'GET', @@ -186,15 +253,50 @@ export async function logout() { clearAlerts(); } -// Load chat interface -export function loadChat() { +/** + * Loads the chat interface and initializes messaging + * @function loadChat + * @example + * loadChat(); + */ +export function loadChat(): void { showChat(); loadMessages(); } -// Check authentication status on page load -export async function checkAuthStatus() { +/** + * Checks authentication status on page load + * @async + * @function checkAuthStatus + * @example + * await checkAuthStatus(); + */ +export async function checkAuthStatus(): Promise { // For JWT, we don't have a persistent token on page load // So we'll just show the login form showLogin(); } + +/** + * Sets up authentication form event listeners + * @function setupAuthForms + * @private + */ +function setupAuthForms(): void { + document.getElementById('login-form-element')!.addEventListener('submit', handleLogin); + document.getElementById('register-form-element')!.addEventListener('submit', handleRegister); +} + +/** + * Initializes links + * @function setupLinks + * @private + */ +function setupLinks(): void { + document.getElementById("login-link")!.addEventListener("click", showLogin); + document.getElementById("register-link")!.addEventListener("click", showRegister); +} + +// Initialize authentication forms +setupAuthForms(); +setupLinks(); \ No newline at end of file diff --git a/frontend/src/chat.ts b/frontend/src/chat.ts index 1d5f6d1..85502e2 100644 --- a/frontend/src/chat.ts +++ b/frontend/src/chat.ts @@ -1,11 +1,25 @@ +/** + * @fileoverview Chat functionality and message management + * @description Handles message display, loading, sending, and real-time updates + * @author Cursor + * @version 1.0.0 + */ + import { getAuthHeaders, currentUser, authToken } from "./auth"; import { API_BASE_URL } from "./config"; import { websocket } from "./websocket"; import type { Message, Messages, WebSocketMessage } from "./types"; -import { formatTime } from "./utils"; +import { formatTime } from "./utils/utils"; - -export function addMessage(message: Message, isAuthor: boolean) { +/** + * Adds a new message to the chat interface + * @param {Message} message - Message object to display + * @param {boolean} isAuthor - Whether the current user is the message author + * @function addMessage + * @example + * addMessage(messageData, messageData.username === currentUser.username); + */ +export function addMessage(message: Message, isAuthor: boolean): void { const messagesContainer = document.querySelector('.chat-messages') as HTMLElement; const messageDiv = document.createElement('div'); messageDiv.classList.add("message"); @@ -64,7 +78,13 @@ export function addMessage(message: Message, isAuthor: boolean) { messagesContainer.scrollTop = messagesContainer.scrollHeight; } -export function loadMessages() { +/** + * Loads chat messages from the server + * @function loadMessages + * @example + * loadMessages(); + */ +export function loadMessages(): void { fetch(`${API_BASE_URL}/get_messages`, { headers: getAuthHeaders() }) @@ -89,7 +109,13 @@ export function loadMessages() { }); } -export function sendMessage() { +/** + * Sends a message via WebSocket + * @function sendMessage + * @example + * sendMessage(); + */ +export function sendMessage(): void { const input = document.querySelector('.message-input') as HTMLInputElement; const message = input.value.trim(); diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 5de6c3e..f46c8a0 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,3 +1,27 @@ +/** + * @fileoverview Application configuration constants + * @description Contains all configuration values used throughout the application + * @author Cursor + * @version 1.0.0 + */ + +/** + * Base API endpoint for all backend requests + * @type {string} + * @constant + */ export const API_BASE_URL: string = '/api'; + +/** + * Full API URL including hostname and port for WebSocket connections + * @type {string} + * @constant + */ export const API_FULL_BASE_URL: string = `${location.hostname}:8301/api`; + +/** + * Application name displayed in UI and document title + * @type {string} + * @constant + */ export const PRODUCT_NAME: string = "FromChat"; \ No newline at end of file diff --git a/frontend/src/init.ts b/frontend/src/init.ts index 87305c2..47d1fe1 100644 --- a/frontend/src/init.ts +++ b/frontend/src/init.ts @@ -1,7 +1,13 @@ +/** + * @fileoverview Application initialization logic + * @description Handles initial application setup and state + * @author FromChat Team + * @version 1.0.0 + */ + import { showLogin } from "./auth"; import { PRODUCT_NAME } from "./config"; showLogin(); - document.getElementById("productname")!.textContent = PRODUCT_NAME; document.title = PRODUCT_NAME; \ No newline at end of file diff --git a/frontend/src/leftpanel.ts b/frontend/src/leftpanel.ts index e7fa548..62806b1 100644 --- a/frontend/src/leftpanel.ts +++ b/frontend/src/leftpanel.ts @@ -1,39 +1,70 @@ +/** + * @fileoverview Left panel UI controls and interactions + * @description Handles chat collapse/expand, chat switching, and profile dialog + * @author Cursor + * @version 1.0.0 + */ + import type { Dialog } from "mdui/components/dialog"; import { loadProfilePicture } from "./profile/upload"; // сварачивание и разворачивание чата -const but = document.getElementById('chat-recrol')!; -const but_list1 = document.getElementById('chat1but')!; -const but_list2 = document.getElementById('chat1but2')!; -const cont1 = document.getElementById('conteinerchat')!; -const namechat = document.getElementById('namechat')!; - -but.addEventListener('click', () => { - but.style.display = 'none'; - cont1.style.display = 'none'; -}); -but_list1.addEventListener('click', () => { - but.style.display = 'flex'; - cont1.style.display = 'flex'; - namechat.textContent = 'общий чат'; -}); -but_list2.addEventListener('click', () => { - but.style.display = 'flex'; - cont1.style.display = 'flex'; - namechat.textContent = 'общий чат 2'; -}); - -// открытие профиля -const butprofile = document.getElementById('profbut')!; +const chatCollapseBtn = document.getElementById('hide-chat')!; +const chat1 = document.getElementById('chat-list-chat-1')!; +const chat2 = document.getElementById('chat-list-chat-2')!; +const chatInner = document.getElementById('chat-inner')!; +const chatName = document.getElementById('chat-name')!; +const profileButton = document.getElementById('profile-open')!; const dialog = document.getElementById("profile-dialog") as Dialog; const dialogClose = document.getElementById("profile-dialog-close")!; -butprofile.addEventListener('click', () => { - dialog.open = true; - // Load profile picture when dialog opens - loadProfilePicture(); -}); +/** + * Sets up chat collapse functionality + * @function setupChatCollapse + * @private + */ +function setupChatCollapse(): void { + chatCollapseBtn.addEventListener('click', () => { + chatCollapseBtn.style.display = 'none'; + chatInner.style.display = 'none'; + }); +} -dialogClose.addEventListener("click", () => { - dialog.open = false; -}); \ No newline at end of file +/** + * Sets up chat switching functionality + * @function setupChatSwitching + * @private + */ +function setupChatSwitching(): void { + chat1.addEventListener('click', () => { + chatCollapseBtn.style.display = 'flex'; + chatInner.style.display = 'flex'; + chatName.textContent = 'общий чат'; + }); + + chat2.addEventListener('click', () => { + chatCollapseBtn.style.display = 'flex'; + chatInner.style.display = 'flex'; + chatName.textContent = 'общий чат 2'; + }); +} + +/** + * Sets up profile dialog functionality + * @function setupProfileDialog + * @private + */ +function setupProfileDialog(): void { + profileButton.addEventListener('click', () => { + dialog.open = true; + loadProfilePicture(); + }); + + dialogClose.addEventListener("click", () => { + dialog.open = false; + }); +} + +setupChatCollapse(); +setupChatSwitching(); +setupProfileDialog(); \ No newline at end of file diff --git a/frontend/src/links.ts b/frontend/src/links.ts deleted file mode 100644 index b051b28..0000000 --- a/frontend/src/links.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { /* loadChat, */ /* logout, */ showLogin, showRegister } from "./auth"; - -const login = document.getElementById("login-link")!; -const register = document.getElementById("register-link")!; -// const chat = document.getElementById("chat-link")!; -// const logoutLink = document.getElementById("logout-link")!; - -login.addEventListener("click", showLogin); -register.addEventListener("click", showRegister); -// chat.addEventListener("click", loadChat); -// logoutLink.addEventListener("click", logout); diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 0a92896..f089f78 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,8 +1,14 @@ +/** + * @fileoverview Application entry point for FromChat frontend + * @description Main module that initializes all required components and styles + * @author Cursor + * @version 1.0.0 + */ + import './css/style.scss'; import "mdui/mdui.css"; -import "./links"; -import "./material"; +import "./utils/material"; import "./chat"; import "./settings"; import "./leftpanel"; diff --git a/frontend/src/profile.ts b/frontend/src/profile.ts index cbcf271..2ff08f4 100644 --- a/frontend/src/profile.ts +++ b/frontend/src/profile.ts @@ -1,3 +1,10 @@ +/** + * @fileoverview Profile module entry point and initialization + * @description Coordinates profile system initialization and form handling + * @author Cursor + * @version 1.0.0 + */ + import type { Dialog } from "mdui/components/dialog"; import { loadProfileData } from './profile/editor'; import { loadProfilePicture, initializeProfileUpload } from "./profile/upload"; @@ -15,7 +22,13 @@ form.addEventListener("submit", async (e) => { dialog.open = false; }); -// Initialize profile functionality after login +/** + * Initializes profile functionality after user login + * @function initializeProfile + * @example + * // Called after successful authentication + * initializeProfile(); + */ export function initializeProfile(): void { // Initialize profile modules initializeProfileUpload(); diff --git a/frontend/src/profile/README.md b/frontend/src/profile/README.md deleted file mode 100644 index 67aabce..0000000 --- a/frontend/src/profile/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# Profile Module Structure - -This directory contains the modularized profile functionality for the FromChat application. - -## Structure - -``` -profile/ -├── types.ts # Type definitions -├── notification.ts # Notification system -├── image-cropper.ts # Image cropping functionality -├── profile-service.ts # API service layer -├── profile-upload.ts # Upload management -├── profile-editor.ts # Profile editing functionality -└── README.md # This file -``` - -## Modules - -### `types.ts` -Contains all TypeScript interfaces and types used across the profile module: -- `ProfileData` - User profile data structure -- `UploadResponse` - API response for uploads -- `NotificationType` - Notification types -- `CropPosition` - Image cropping position -- `DragStart` - Drag operation start position - -### `notification.ts` -Top-level notification functions for displaying success/error messages: -- `showSuccess(message: string)` - Show success notification -- `showError(message: string)` - Show error notification - -### `image-cropper.ts` -Canvas-based image cropper for profile pictures: -- `ImageCropper` - Main cropper class with touch/mouse support -- Handles circular cropping with drag functionality - -### `profile-service.ts` -API service layer for profile operations: -- `loadProfile()` - Load user profile data -- `uploadProfilePicture(file: Blob)` - Upload profile picture -- `updateProfile(data: Partial)` - Update profile data - -### `profile-upload.ts` -Manages profile picture upload workflow: -- `loadProfilePicture()` - Load and display profile picture -- Global variables and event listeners for upload UI -- Handles file selection, cropping, and upload - -### `profile-editor.ts` -Manages profile text editing (nickname, description): -- `loadProfileData()` - Load profile text data -- `setNicknameValue(value: string)` - Set nickname value -- `setDescriptionValue(value: string)` - Set description value -- `getNicknameValue()` - Get current nickname -- `getDescriptionValue()` - Get current description -- Global event listeners for edit buttons -- Supports Enter to save, Escape to cancel - -## Usage - -### Direct Imports -```typescript -// Import specific functions from each module -import { loadProfile, uploadProfilePicture, updateProfile } from './profile/profile-service'; -import { loadProfilePicture } from './profile/profile-upload'; -import { loadProfileData, setNicknameValue } from './profile/profile-editor'; -import { showSuccess, showError } from './profile/notification'; -import { ImageCropper } from './profile/image-cropper'; -``` - -### Loading Profile Data -```typescript -// Load profile picture -await loadProfilePicture(); - -// Load profile text data -await loadProfileData(); -``` - -### Uploading Profile Picture -The upload process is handled automatically by the global event listeners in `profile-upload.ts` when users interact with the upload UI. - -### Editing Profile Text -The editing process is handled automatically by the global event listeners in `profile-editor.ts` when users interact with the edit buttons. - -### Showing Notifications -```typescript -showSuccess('Operation completed successfully!'); -showError('Something went wrong!'); -``` - -### API Operations -```typescript -// Load profile data -const profileData = await loadProfile(); - -// Update profile -const success = await updateProfile({ nickname: 'New Name' }); - -// Upload profile picture -const result = await uploadProfilePicture(blob); -``` - -## Benefits of This Structure - -1. **Separation of Concerns**: Each module has a single responsibility -2. **Reusability**: Functions can be used independently -3. **Maintainability**: Easier to find and fix issues -4. **Testability**: Each function can be tested in isolation -5. **Type Safety**: Strong TypeScript typing throughout -6. **Top-level Functions**: Simple function calls instead of class instances -7. **Direct Imports**: Import only what you need from specific modules -8. **No Index File**: Direct imports reduce complexity and improve tree shaking - -## Migration from Original Files - -The original `profile.ts` and `profile-upload.ts` files have been refactored to use this modular structure. The functionality remains the same, but it's now better organized and more maintainable. diff --git a/frontend/src/profile/api.ts b/frontend/src/profile/api.ts index 7b5ba4e..32c8220 100644 --- a/frontend/src/profile/api.ts +++ b/frontend/src/profile/api.ts @@ -1,6 +1,24 @@ +/** + * @fileoverview Profile-related API calls + * @description Handles all profile-related HTTP requests to the backend + * @author Cursor + * @version 1.0.0 + */ + import { getAuthHeaders } from '../auth'; import type { ProfileData, UploadResponse } from './types'; +/** + * Loads user profile data from the server + * @async + * @function loadProfile + * @returns {Promise} User profile data or null if failed + * @example + * const profile = await loadProfile(); + * if (profile) { + * console.log('User nickname:', profile.nickname); + * } + */ export async function loadProfile(): Promise { try { const response = await fetch('/api/user/profile', { @@ -18,6 +36,20 @@ export async function loadProfile(): Promise { } } +/** + * Uploads a profile picture to the server + * @async + * @function uploadProfilePicture + * @param {Blob} file - The image file to upload + * @returns {Promise} Upload response with URL or null if failed + * @example + * const fileInput = document.getElementById('file-input'); + * const file = fileInput.files[0]; + * const result = await uploadProfilePicture(file); + * if (result) { + * console.log('Uploaded to:', result.profile_picture_url); + * } + */ export async function uploadProfilePicture(file: Blob): Promise { try { const formData = new FormData(); @@ -39,6 +71,21 @@ export async function uploadProfilePicture(file: Blob): Promise} data - Profile data to update + * @returns {Promise} True if update was successful, false otherwise + * @example + * const success = await updateProfile({ + * nickname: 'New Name', + * description: 'Updated bio' + * }); + * if (success) { + * console.log('Profile updated successfully'); + * } + */ export async function updateProfile(data: Partial): Promise { try { const response = await fetch('/api/user/profile', { diff --git a/frontend/src/profile/editor.ts b/frontend/src/profile/editor.ts index 40422ff..d37aebf 100644 --- a/frontend/src/profile/editor.ts +++ b/frontend/src/profile/editor.ts @@ -1,26 +1,59 @@ +/** + * @fileoverview Profile editing functionality + * @description Handles profile form editing and MDUI text field integration + * @author Cursor + * @version 1.0.0 + */ + import { updateProfile } from './api'; import { loadProfile } from './api'; -import { showSuccess, showError } from '../notification'; +import { showSuccess, showError } from '../utils/notification'; import type { TextField } from 'mdui/components/text-field'; -// DOM elements -let profileForm: HTMLElement; -let nicknameField: TextField; // MDUI TextField -let descriptionField: TextField; // MDUI TextField +let profileForm = document.getElementById('profile-form')!; +let nicknameField = document.getElementById('username-field') as unknown as TextField; +let descriptionField = document.getElementById('description-field') as unknown as TextField; + +/** + * Initialization state flag + * @type {boolean} + */ let isInitialized = false; +/** + * Sets the username field value + * @param {string} value - The username value to set + * @function setUsernameValue + * @example + * setUsernameValue('John Doe'); + */ export function setUsernameValue(value: string): void { if (nicknameField && nicknameField.value !== undefined) { nicknameField.value = value; } } +/** + * Sets the description field value + * @param {string} value - The description value to set + * @function setDescriptionValue + * @example + * setDescriptionValue('Software Developer'); + */ export function setDescriptionValue(value: string): void { if (descriptionField && descriptionField.value !== undefined) { descriptionField.value = value; } } +/** + * Gets the current username field value + * @returns {string} The current username value + * @function getUsernameValue + * @example + * const username = getUsernameValue(); + * console.log('Current username:', username); + */ export function getUsernameValue(): string { if (nicknameField && nicknameField.value !== undefined) { return nicknameField.value; @@ -28,6 +61,14 @@ export function getUsernameValue(): string { return ''; } +/** + * Gets the current description field value + * @returns {string} The current description value + * @function getDescriptionValue + * @example + * const description = getDescriptionValue(); + * console.log('Current description:', description); + */ export function getDescriptionValue(): string { if (descriptionField && descriptionField.value !== undefined) { return descriptionField.value; @@ -35,6 +76,13 @@ export function getDescriptionValue(): string { return ''; } +/** + * Loads profile data from the server and populates the form fields + * @async + * @function loadProfileData + * @example + * await loadProfileData(); + */ export async function loadProfileData(): Promise { const userData = await loadProfile(); if (userData) { @@ -47,39 +95,57 @@ export async function loadProfileData(): Promise { } } -// Setup form submission handler +/** + * Handles profile form submission + * @async + * @function handleFormSubmission + * @param {Event} e - Form submission event + * @private + */ +async function handleFormSubmission(e: Event): Promise { + e.preventDefault(); + + const nickname = getUsernameValue(); + const description = getDescriptionValue(); + + if (nickname || description) { + const success = await updateProfile({ + nickname: nickname || undefined, + description: description || undefined + }); + + if (success) { + showSuccess('Профиль обновлен!'); + } else { + showError('Ошибка при обновлении профиля'); + } + } +} + +/** + * Sets up form submission handler + * @function setupFormHandler + * @private + */ function setupFormHandler(): void { if (isInitialized) return; // Get DOM elements profileForm = document.getElementById('profile-form')!; - nicknameField = document.getElementById('username-field') as any; // MDUI TextField - descriptionField = document.getElementById('description-field') as any; // MDUI TextField + nicknameField = document.getElementById('username-field') as any; + descriptionField = document.getElementById('description-field') as any; - profileForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const nickname = getUsernameValue(); - const description = getDescriptionValue(); - - if (nickname || description) { - const success = await updateProfile({ - nickname: nickname || undefined, - description: description || undefined - }); - - if (success) { - showSuccess('Профиль обновлен!'); - } else { - showError('Ошибка при обновлении профиля'); - } - } - }); + profileForm.addEventListener('submit', handleFormSubmission); isInitialized = true; } -// Initialize editor functionality +/** + * Initializes profile editor functionality + * @function initializeProfileEditor + * @example + * initializeProfileEditor(); + */ export function initializeProfileEditor(): void { setupFormHandler(); } diff --git a/frontend/src/profile/image-cropper.ts b/frontend/src/profile/image-cropper.ts index 45ac17c..c4468d1 100644 --- a/frontend/src/profile/image-cropper.ts +++ b/frontend/src/profile/image-cropper.ts @@ -1,14 +1,56 @@ +/** + * @fileoverview Canvas-based image cropping component + * @description Provides circular image cropping functionality with drag support + * @author Cursor + * @version 1.0.0 + */ + import type { Size2D } from "../types"; +/** + * Image cropper class for circular profile picture cropping + * @class ImageCropper + */ export class ImageCropper { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; + + /** + * Image element to be cropped + * @type {HTMLImageElement} + * @private + */ private image!: HTMLImageElement; + + /** + * Size of the crop area (diameter) + * @type {number} + * @private + */ private cropSize: number = 200; private isDragging: boolean = false; + + /** + * Starting position of the drag operation + * @type {Size2D} + * @private + */ private dragStart: Size2D = { x: 0, y: 0 }; + + /** + * Current position of the crop area + * @type {Size2D} + * @private + */ private cropPosition: Size2D = { x: 0, y: 0 }; + /** + * Creates a new ImageCropper instance + * @param {HTMLElement} container - Container element to append the canvas to + * @constructor + * @example + * const cropper = new ImageCropper(document.getElementById('cropper-area')); + */ constructor(container: HTMLElement) { this.canvas = document.createElement('canvas'); this.canvas.width = this.cropSize; @@ -19,6 +61,11 @@ export class ImageCropper { this.setupEventListeners(); } + /** + * Sets up mouse and touch event listeners + * @function setupEventListeners + * @private + */ private setupEventListeners(): void { this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this)); @@ -28,11 +75,23 @@ export class ImageCropper { this.canvas.addEventListener('touchend', this.onTouchEnd.bind(this)); } + /** + * Handles mouse down events + * @param {MouseEvent} e - Mouse event + * @function onMouseDown + * @private + */ private onMouseDown(e: MouseEvent): void { this.isDragging = true; this.dragStart = { x: e.clientX, y: e.clientY }; } + /** + * Handles mouse move events during dragging + * @param {MouseEvent} e - Mouse event + * @function onMouseMove + * @private + */ private onMouseMove(e: MouseEvent): void { if (!this.isDragging) return; @@ -46,10 +105,21 @@ export class ImageCropper { this.render(); } + /** + * Handles mouse up events + * @function onMouseUp + * @private + */ private onMouseUp(): void { this.isDragging = false; } + /** + * Handles touch start events + * @param {TouchEvent} e - Touch event + * @function onTouchStart + * @private + */ private onTouchStart(e: TouchEvent): void { e.preventDefault(); const touch = e.touches[0]; @@ -57,6 +127,12 @@ export class ImageCropper { this.dragStart = { x: touch.clientX, y: touch.clientY }; } + /** + * Handles touch move events during dragging + * @param {TouchEvent} e - Touch event + * @function onTouchMove + * @private + */ private onTouchMove(e: TouchEvent): void { e.preventDefault(); if (!this.isDragging) return; @@ -72,10 +148,23 @@ export class ImageCropper { this.render(); } + /** + * Handles touch end events + * @function onTouchEnd + * @private + */ private onTouchEnd(): void { this.isDragging = false; } + /** + * Loads an image file for cropping + * @param {File} file - Image file to load + * @returns {Promise} Promise that resolves when image is loaded + * @async + * @example + * await cropper.loadImage(fileInput.files[0]); + */ loadImage(file: File): Promise { return new Promise((resolve) => { this.image = new Image(); @@ -87,6 +176,11 @@ export class ImageCropper { }); } + /** + * Renders the image with circular crop overlay + * @function render + * @private + */ private render(): void { if (!this.image) return; @@ -119,10 +213,23 @@ export class ImageCropper { this.ctx.restore(); } + /** + * Gets the cropped image as a data URL + * @returns {string} Data URL of the cropped image + * @example + * const croppedImage = cropper.getCroppedImage(); + * // Use croppedImage as src for an img element + */ getCroppedImage(): string { return this.canvas.toDataURL('image/jpeg', 0.8); } + /** + * Destroys the cropper and removes the canvas from DOM + * @function destroy + * @example + * cropper.destroy(); + */ destroy(): void { if (this.canvas.parentNode) { this.canvas.parentNode.removeChild(this.canvas); diff --git a/frontend/src/profile/types.ts b/frontend/src/profile/types.ts index 7a9aa34..ec134c6 100644 --- a/frontend/src/profile/types.ts +++ b/frontend/src/profile/types.ts @@ -1,9 +1,28 @@ +/** + * @fileoverview Profile-specific type definitions + * @description Contains type definitions for profile-related functionality + * @author Cursor + * @version 1.0.0 + */ + +/** + * User profile data structure + * @interface ProfileData + * @property {string} [profile_picture] - URL to user's profile picture + * @property {string} [nickname] - User's display name + * @property {string} [description] - User's bio or description + */ export interface ProfileData { profile_picture?: string; nickname?: string; description?: string; } +/** + * Profile picture upload response structure + * @interface UploadResponse + * @property {string} profile_picture_url - URL to the uploaded profile picture + */ export interface UploadResponse { profile_picture_url: string; } \ No newline at end of file diff --git a/frontend/src/profile/upload.ts b/frontend/src/profile/upload.ts index 889e5c4..0745eb7 100644 --- a/frontend/src/profile/upload.ts +++ b/frontend/src/profile/upload.ts @@ -1,34 +1,111 @@ +/** + * @fileoverview Profile picture upload functionality + * @description Handles file selection, image cropping, and profile picture upload + * @author Cursor + * @version 1.0.0 + */ + import type { Dialog } from "mdui/components/dialog"; import { ImageCropper } from './image-cropper'; import { uploadProfilePicture } from './api'; import { loadProfile } from './api'; -import { showSuccess, showError } from '../notification'; +import { showSuccess, showError } from '../utils/notification'; -// Global variables +/** + * Global image cropper instance + * @type {ImageCropper | null} + */ let cropper: ImageCropper | null = null; + +/** + * Initialization state flag + * @type {boolean} + */ let isInitialized = false; -// DOM elements -let cropperDialog: Dialog; -let fileInput: HTMLInputElement; -let uploadBtn: HTMLElement; -let cropSaveBtn: HTMLElement; -let cropCancelBtn: HTMLElement; -let cropperCloseBtn: HTMLElement; -let cropperArea: HTMLElement; +let cropperDialog = document.getElementById('cropper-dialog') as Dialog; +let fileInput = document.getElementById('pfp-file-input') as HTMLInputElement; +let uploadBtn = document.getElementById('upload-pfp-btn')!; +let cropSaveBtn = document.getElementById('crop-save')!; +let cropCancelBtn = document.getElementById('crop-cancel')!; +let cropperCloseBtn = document.getElementById('cropper-close')!; +let cropperArea = document.getElementById('cropper-area')!; -// Setup event listeners +/** + * Opens the image cropper with the selected file + * @async + * @function openCropper + * @param {File} file - The image file to crop + * @private + */ +async function openCropper(file: File): Promise { + // Clear previous cropper + cropperArea.innerHTML = ''; + + // Create new cropper + cropper = new ImageCropper(cropperArea); + + // Load image + await cropper.loadImage(file); + + // Open dialog + cropperDialog.open = true; +} + +/** + * Closes the image cropper and cleans up resources + * @function closeCropper + * @private + */ +function closeCropper(): void { + cropperDialog.open = false; + cropperArea.innerHTML = ''; + if (cropper) { + cropper.destroy(); + cropper = null; + } + fileInput.value = ''; +} + +/** + * Saves the cropped image and uploads it to the server + * @async + * @function saveCroppedImage + * @private + */ +async function saveCroppedImage(): Promise { + if (!cropper) return; + + const croppedImageData = cropper.getCroppedImage(); + + // Convert data URL to blob + const response = await fetch(croppedImageData); + const blob = await response.blob(); + + const result = await uploadProfilePicture(blob); + + if (result) { + // Update profile picture display + const profilePicture = document.getElementById('profile-picture') as HTMLImageElement; + profilePicture.src = result.profile_picture_url + '?t=' + Date.now(); // Cache bust + + // Close cropper + closeCropper(); + + // Show success message + showSuccess('Фото профиля обновлено!'); + } else { + showError('Ошибка при загрузке фото'); + } +} + +/** + * Sets up event listeners for upload functionality + * @function setupEventListeners + * @private + */ function setupEventListeners(): void { if (isInitialized) return; - - // Get DOM elements - cropperDialog = document.getElementById('cropper-dialog') as Dialog; - fileInput = document.getElementById('pfp-file-input') as HTMLInputElement; - uploadBtn = document.getElementById('upload-pfp-btn')!; - cropSaveBtn = document.getElementById('crop-save')!; - cropCancelBtn = document.getElementById('crop-cancel')!; - cropperCloseBtn = document.getElementById('cropper-close')!; - cropperArea = document.getElementById('cropper-area')!; uploadBtn.addEventListener('click', () => { fileInput.click(); @@ -56,65 +133,31 @@ function setupEventListeners(): void { isInitialized = true; } -async function openCropper(file: File): Promise { - // Clear previous cropper - cropperArea.innerHTML = ''; - - // Create new cropper - cropper = new ImageCropper(cropperArea); - - // Load image - await cropper.loadImage(file); - - // Open dialog - cropperDialog.open = true; -} - -function closeCropper(): void { - cropperDialog.open = false; - cropperArea.innerHTML = ''; - if (cropper) { - cropper.destroy(); - cropper = null; - } - fileInput.value = ''; -} - -async function saveCroppedImage(): Promise { - if (!cropper) return; - - const croppedImageData = cropper.getCroppedImage(); - - // Convert data URL to blob - const response = await fetch(croppedImageData); - const blob = await response.blob(); - - const result = await uploadProfilePicture(blob); - - if (result) { - // Update profile picture display - const profilePicture = document.getElementById('profile-picture') as HTMLImageElement; - profilePicture.src = result.profile_picture_url + '?t=' + Date.now(); // Cache bust - - // Close cropper - closeCropper(); - - // Show success message - showSuccess('Фото профиля обновлено!'); - } else { - showError('Ошибка при загрузке фото'); - } -} - +/** + * Loads and displays the user's profile picture + * @async + * @function loadProfilePicture + * @example + * await loadProfilePicture(); + */ export async function loadProfilePicture(): Promise { const userData = await loadProfile(); if (userData?.profile_picture) { + const url = `${userData.profile_picture}?t=${Date.now()}`; + const profilePicture = document.getElementById('profile-picture') as HTMLImageElement; - profilePicture.src = userData.profile_picture + '?t=' + Date.now(); // Cache bust + const profilePicture2 = document.getElementById("preview1") as HTMLImageElement; + profilePicture.src = url; + profilePicture2.src = url; } } -// Initialize upload functionality +/** + * Initializes profile upload functionality + * @function initializeProfileUpload + * @example + * initializeProfileUpload(); + */ export function initializeProfileUpload(): void { setupEventListeners(); } diff --git a/frontend/src/settings.ts b/frontend/src/settings.ts index e792714..3c66910 100644 --- a/frontend/src/settings.ts +++ b/frontend/src/settings.ts @@ -1,3 +1,10 @@ +/** + * @fileoverview Settings dialog management and panel navigation + * @description Handles settings dialog functionality and dynamic panel switching + * @author Cursor + * @version 1.0.0 + */ + import type { Dialog } from "mdui/components/dialog"; const dialog = document.getElementById('settings-dialog') as Dialog; @@ -8,49 +15,65 @@ const closeButton = document.getElementById('settings-close')!; const settingsList = document.querySelector('#settings-menu mdui-list')!; const settingsPanels = document.querySelectorAll('.settings-panel'); -// Initialize settings -function initializeSettings() { - // Add click listeners to all list items +/** + * Mapping between list item text and their corresponding panel IDs + * @type {Object.} + */ +const panelMapping = { + 'Уведомления': 'notifications-settings', + 'Внешний вид': 'appearance-settings', + 'Безопасность': 'security-settings', + 'Язык': 'language-settings', + 'Хранилище': 'storage-settings', + 'Помощь': 'help-settings', + 'О приложении': 'about-settings' +}; + +/** + * Handles click events on settings list items + * @param {Element} item - The clicked list item element + * @function handleListItemClick + * @private + */ +function handleListItemClick(item: Element): void { + // Remove active class from all items and panels const listItems = settingsList.querySelectorAll('mdui-list-item'); + listItems.forEach(li => li.removeAttribute('active')); + settingsPanels.forEach(panel => panel.classList.remove('active')); - // Create a mapping between list items and their corresponding panels - const panelMapping = { - 'Уведомления': 'notifications-settings', - 'Внешний вид': 'appearance-settings', - 'Безопасность': 'security-settings', - 'Язык': 'language-settings', - 'Хранилище': 'storage-settings', - 'Помощь': 'help-settings', - 'О приложении': 'about-settings' - }; + // Add active class to clicked item + item.setAttribute('active', ''); + // Show corresponding panel using the mapping + const itemText = item.textContent?.trim(); + const panelId = panelMapping[itemText as keyof typeof panelMapping]; + + if (panelId) { + const targetPanel = document.getElementById(panelId); + if (targetPanel) { + targetPanel.classList.add('active'); + } + } +} + +/** + * Sets up click listeners for all settings list items + * @function setupSettingsNavigation + * @private + */ +function setupSettingsNavigation(): void { + const listItems = settingsList.querySelectorAll('mdui-list-item'); listItems.forEach((item) => { - item.addEventListener('click', () => { - // Remove active class from all items and panels - listItems.forEach(li => li.removeAttribute('active')); - settingsPanels.forEach(panel => panel.classList.remove('active')); - - // Add active class to clicked item - item.setAttribute('active', ''); - - // Show corresponding panel using the mapping - const itemText = item.textContent?.trim(); - const panelId = panelMapping[itemText as keyof typeof panelMapping]; - - if (panelId) { - const targetPanel = document.getElementById(panelId); - if (targetPanel) { - targetPanel.classList.add('active'); - } - } - }); + item.addEventListener('click', () => handleListItemClick(item)); }); } -// Dialog event listeners -openButton.addEventListener('click', () => { - dialog.open = true; - // Reset to first panel when opening +/** + * Resets settings dialog to show the first panel + * @function resetToFirstPanel + * @private + */ +function resetToFirstPanel(): void { const firstItem = settingsList.querySelector('mdui-list-item'); const firstPanel = document.querySelector('.settings-panel'); if (firstItem && firstPanel) { @@ -59,15 +82,23 @@ openButton.addEventListener('click', () => { firstItem.setAttribute('active', ''); firstPanel.classList.add('active'); } -}); - -closeButton.addEventListener('click', () => { - dialog.open = false; -}); - -// Initialize when DOM is loaded -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializeSettings); -} else { - initializeSettings(); } + +/** + * Sets up dialog event listeners + * @function setupDialogListeners + * @private + */ +function setupDialogListeners(): void { + openButton.addEventListener('click', () => { + dialog.open = true; + resetToFirstPanel(); + }); + + closeButton.addEventListener('click', () => { + dialog.open = false; + }); +} + +setupSettingsNavigation(); +setupDialogListeners(); \ No newline at end of file diff --git a/frontend/src/types.ts b/frontend/src/types.ts index acb61c5..abbecc2 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -1,16 +1,48 @@ +/** + * @fileoverview Global TypeScript type definitions + * @description Contains all type definitions used throughout the application + * @author Cursor + * @version 1.0.0 + */ + +/** + * HTTP headers object type + * @typedef {Object.} Headers + */ export type Headers = {[x: string]: string} +/** + * API error response structure + * @interface ErrorResponse + * @property {string} message - Error message from the server + */ export interface ErrorResponse { message: string; } +/** + * 2D coordinate structure + * @interface Size2D + * @property {number} x - X coordinate + * @property {number} y - Y coordinate + */ export interface Size2D { x: number; y: number; } - // App types + +/** + * Chat message structure + * @interface Message + * @property {number} id - Unique message identifier + * @property {string} username - Username of the message sender + * @property {string} content - Message content + * @property {boolean} is_read - Whether the message has been read + * @property {string} timestamp - ISO timestamp of the message + * @property {string} [profile_picture] - URL to sender's profile picture + */ export interface Message { id: number; username: string; @@ -20,10 +52,24 @@ export interface Message { profile_picture?: string; } +/** + * Collection of messages + * @interface Messages + * @property {Message[]} messages - Array of message objects + */ export interface Messages { messages: Message[]; } +/** + * User information structure + * @interface User + * @property {number} id - Unique user identifier + * @property {string} created_at - ISO timestamp of account creation + * @property {string} last_seen - ISO timestamp of last activity + * @property {boolean} online - Whether the user is currently online + * @property {string} username - Username + */ export interface User { id: number; created_at: string; @@ -32,17 +78,30 @@ export interface User { username: string; } - // ---------- // API models // ---------- // Requests + +/** + * Login request structure + * @interface LoginRequest + * @property {string} username - Username for authentication + * @property {string} password - Password for authentication + */ export interface LoginRequest { username: string; password: string; } +/** + * Registration request structure + * @interface RegisterRequest + * @property {string} username - Desired username + * @property {string} password - Desired password + * @property {string} confirm_password - Password confirmation + */ export interface RegisterRequest { username: string; password: string; @@ -50,6 +109,13 @@ export interface RegisterRequest { } // Responses + +/** + * Login response structure + * @interface LoginResponse + * @property {User} user - User information + * @property {string} token - JWT authentication token + */ export interface LoginResponse { user: User; token: string; @@ -59,6 +125,14 @@ export interface LoginResponse { // WebSocket types // --------------- +/** + * WebSocket message structure + * @interface WebSocketMessage + * @property {string} type - Message type identifier + * @property {WebSocketCredentials} [credentials] - Authentication credentials + * @property {any} [data] - Message payload data + * @property {WebSocketError} [error] - Error information if applicable + */ export interface WebSocketMessage { type: string; credentials?: WebSocketCredentials; @@ -66,11 +140,23 @@ export interface WebSocketMessage { error?: WebSocketError; } +/** + * WebSocket error structure + * @interface WebSocketError + * @property {number} code - Error code + * @property {string} detail - Error detail message + */ export interface WebSocketError { code: number; detail: string; } +/** + * WebSocket authentication credentials + * @interface WebSocketCredentials + * @property {string} scheme - Authentication scheme (e.g., "Bearer") + * @property {string} credentials - Authentication token or credentials + */ export interface WebSocketCredentials { scheme: string; credentials: string; diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts deleted file mode 100644 index 84499f3..0000000 --- a/frontend/src/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function formatTime(dateString: string) { - const date = new Date(dateString); - let hours = date.getHours(); - let minutes = date.getMinutes(); - const hoursString = hours < 10 ? '0' + hours : hours; - const minutesString = minutes < 10 ? '0' + minutes : minutes; - return hoursString + ':' + minutesString; -} - -export function delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} \ No newline at end of file diff --git a/frontend/src/material.ts b/frontend/src/utils/material.ts similarity index 76% rename from frontend/src/material.ts rename to frontend/src/utils/material.ts index a8dd8e1..265d42b 100644 --- a/frontend/src/material.ts +++ b/frontend/src/utils/material.ts @@ -1,3 +1,10 @@ +/** + * @fileoverview MDUI component imports and configuration + * @description Imports all required MDUI components and sets up the theme + * @author Cursor + * @version 1.0.0 + */ + import 'mdui/components/tabs'; import 'mdui/components/tab'; import 'mdui/components/tab-panel'; diff --git a/frontend/src/notification.ts b/frontend/src/utils/notification.ts similarity index 55% rename from frontend/src/notification.ts rename to frontend/src/utils/notification.ts index 831e0a4..b655dc1 100644 --- a/frontend/src/notification.ts +++ b/frontend/src/utils/notification.ts @@ -1,5 +1,23 @@ +/** + * @fileoverview User notification system + * @description Provides toast-style notifications for user feedback + * @author Cursor + * @version 1.0.0 + */ + +/** + * Notification type enumeration + * @typedef {'success' | 'error'} NotificationType + */ export type NotificationType = 'success' | 'error'; +/** + * Shows a notification with the specified message and type + * @param {string} message - The message to display + * @param {NotificationType} type - The type of notification (success or error) + * @function showNotification + * @private + */ function showNotification(message: string, type: NotificationType): void { const notification = document.createElement('div'); notification.textContent = message; @@ -28,10 +46,24 @@ function showNotification(message: string, type: NotificationType): void { }, 3000); } +/** + * Shows a success notification + * @param {string} message - The success message to display + * @function showSuccess + * @example + * showSuccess('Profile updated successfully!'); + */ export function showSuccess(message: string): void { showNotification(message, 'success'); } +/** + * Shows an error notification + * @param {string} message - The error message to display + * @function showError + * @example + * showError('Failed to update profile'); + */ export function showError(message: string): void { showNotification(message, 'error'); } diff --git a/frontend/src/utils/utils.ts b/frontend/src/utils/utils.ts new file mode 100644 index 0000000..2220c33 --- /dev/null +++ b/frontend/src/utils/utils.ts @@ -0,0 +1,33 @@ +/** + * @fileoverview Utility functions used throughout the application + * @description Contains helper functions for common operations + * @author Cursor + * @version 1.0.0 + */ + +/** + * Formats a timestamp string to HH:MM format + * @param {string} dateString - ISO timestamp string to format + * @returns {string} Formatted time string in HH:MM format + * @example + * formatTime('2024-01-15T14:30:00Z'); // Returns "14:30" + */ +export function formatTime(dateString: string): string { + const date = new Date(dateString); + let hours = date.getHours(); + let minutes = date.getMinutes(); + const hoursString = hours < 10 ? '0' + hours : hours; + const minutesString = minutes < 10 ? '0' + minutes : minutes; + return hoursString + ':' + minutesString; +} + +/** + * Creates a promise that resolves after a specified delay + * @param {number} ms - Delay time in milliseconds + * @returns {Promise} Promise that resolves after the delay + * @example + * await delay(1000); // Wait for 1 second + */ +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/frontend/src/websocket.ts b/frontend/src/websocket.ts index 6cafd7e..57460e9 100644 --- a/frontend/src/websocket.ts +++ b/frontend/src/websocket.ts @@ -1,13 +1,30 @@ +/** + * @fileoverview WebSocket connection management for real-time chat + * @description Handles WebSocket connections, message processing, and auto-reconnection + * @author Cursor + * @version 1.0.0 + */ + import { currentUser } from "./auth"; import { addMessage } from "./chat"; import { API_FULL_BASE_URL } from "./config"; import type { WebSocketMessage, Message } from "./types"; -import { delay } from "./utils"; +import { delay } from "./utils/utils"; -function create() { +/** + * Creates a new WebSocket connection to the chat server + * @function create + * @returns {WebSocket} New WebSocket instance + * @private + */ +function create(): WebSocket { return new WebSocket(`ws://${API_FULL_BASE_URL}/chat/ws`); } +/** + * Global WebSocket instance + * @type {WebSocket} + */ export let websocket = create(); // --------------