Add documentation

This commit is contained in:
2025-08-22 22:01:51 +03:00
Unverified
parent deb02a5e95
commit eb85e68fc2
23 changed files with 931 additions and 364 deletions
+11
View File
@@ -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.
+8 -7
View File
@@ -120,7 +120,7 @@
<header class="chat-header-left"> <header class="chat-header-left">
<div id="productname">Loading...</div> <div id="productname">Loading...</div>
<div class="profile"> <div class="profile">
<a href="#" id="profbut"> <a href="#" id="profile-open">
<img src="./src/images/default-avatar.png" alt="" id="preview1" /> <img src="./src/images/default-avatar.png" alt="" id="preview1" />
</a> </a>
</div> </div>
@@ -139,10 +139,10 @@
<mdui-tab-panel slot="panel" value="chats"> <mdui-tab-panel slot="panel" value="chats">
<mdui-list> <mdui-list>
<mdui-list-item headline="Общий чат" description="Вы: Последнее сообщение" id="chat1but"> <mdui-list-item headline="Общий чат" description="Вы: Последнее сообщение" id="chat-list-chat-1">
<img src="./src/images/default-avatar.png" alt="" slot="icon" /> <img src="./src/images/default-avatar.png" alt="" slot="icon" />
</mdui-list-item> </mdui-list-item>
<mdui-list-item headline="Общий чат 2" description="Вы: Последнее сообщение" id="chat1but2"> <mdui-list-item headline="Общий чат 2" description="Вы: Последнее сообщение" id="chat-list-chat-2">
<img src="./src/images/default-avatar.png" alt="" slot="icon" /> <img src="./src/images/default-avatar.png" alt="" slot="icon" />
</mdui-list-item> </mdui-list-item>
</mdui-list> </mdui-list>
@@ -159,17 +159,18 @@
</mdui-bottom-app-bar> </mdui-bottom-app-bar>
</div> </div>
<div class="chat-container"> <div class="chat-container">
<div class="chat-main" id="conteinerchat"> <div class="chat-main" id="chat-inner">
<div class="chat-header"> <div class="chat-header">
<img src="src/images/default-avatar.png" alt="Avatar" class="chat-header-avatar"> <img src="src/images/default-avatar.png" alt="Avatar" class="chat-header-avatar">
<div class="chat-header-info"> <div class="chat-header-info">
<div class="info-chat"> <div class="info-chat">
<h4 id="namechat">Общий чат</h4> <h4 id="chat-name">Общий чат</h4>
<p> <p>
<span class="online-status"></span> Онлайн <span class="online-status"></span>
Онлайн
</p> </p>
</div> </div>
<a href="#" id="chat-recrol">Свернуть чат</a> <a href="#" id="hide-chat">Свернуть чат</a>
</div> </div>
</div> </div>
+127 -25
View File
@@ -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 { loadMessages } from "./chat";
import { initializeProfile } from "./profile"; import { initializeProfile } from "./profile";
import type { Headers, ErrorResponse, User, LoginResponse, LoginRequest, RegisterRequest } from "./types"; import type { Headers, ErrorResponse, User, LoginResponse, LoginRequest, RegisterRequest } from "./types";
import { API_BASE_URL } from "./config"; import { API_BASE_URL } from "./config";
// Authentication and navigation handling /**
* Current authenticated user information
* @type {User | null}
*/
export let currentUser: User | null = null; export let currentUser: User | null = null;
/**
* JWT authentication token
* @type {string | null}
*/
export let authToken: string | null = 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 { export function getAuthHeaders(json: boolean = true): Headers {
const headers: Headers = {}; const headers: Headers = {};
@@ -22,38 +44,65 @@ export function getAuthHeaders(json: boolean = true): Headers {
return 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('login-form')!.style.display = 'flex';
document.getElementById('register-form')!.style.display = 'none'; document.getElementById('register-form')!.style.display = 'none';
document.getElementById('chat-interface')!.style.display = 'none'; document.getElementById('chat-interface')!.style.display = 'none';
clearAlerts(); 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('login-form')!.style.display = 'none';
document.getElementById('register-form')!.style.display = 'flex'; document.getElementById('register-form')!.style.display = 'flex';
document.getElementById('chat-interface')!.style.display = 'none'; document.getElementById('chat-interface')!.style.display = 'none';
clearAlerts(); 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('login-form')!.style.display = 'none';
document.getElementById('register-form')!.style.display = 'none'; document.getElementById('register-form')!.style.display = 'none';
document.getElementById('chat-interface')!.style.display = 'block'; document.getElementById('chat-interface')!.style.display = 'block';
loadMessages(); 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('login-alerts')!.innerHTML = '';
document.getElementById('register-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 container = document.getElementById(containerId)!;
const alertDiv = document.createElement('div'); const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type}`; alertDiv.className = `alert alert-${type}`;
@@ -61,8 +110,14 @@ export function showAlert(containerId: string, message: string, type: "success"
container.appendChild(alertDiv); 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<void> {
e.preventDefault(); e.preventDefault();
const usernameElement = document.getElementById('login-username') as HTMLInputElement; const usernameElement = document.getElementById('login-username') as HTMLInputElement;
@@ -105,10 +160,16 @@ document.getElementById('login-form-element')!.addEventListener('submit', async
} catch (error) { } catch (error) {
showAlert('login-alerts', 'Ошибка соединения с сервером', 'danger'); 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<void> {
e.preventDefault(); e.preventDefault();
const usernameElement = document.getElementById('register-username') as HTMLInputElement; const usernameElement = document.getElementById('register-username') as HTMLInputElement;
@@ -167,10 +228,16 @@ document.getElementById('register-form-element')!.addEventListener('submit', asy
} catch (error) { } catch (error) {
showAlert('register-alerts', 'Ошибка соединения с сервером', 'danger'); 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<void> {
try { try {
await fetch(`${API_BASE_URL}/logout`, { await fetch(`${API_BASE_URL}/logout`, {
method: 'GET', method: 'GET',
@@ -186,15 +253,50 @@ export async function logout() {
clearAlerts(); clearAlerts();
} }
// Load chat interface /**
export function loadChat() { * Loads the chat interface and initializes messaging
* @function loadChat
* @example
* loadChat();
*/
export function loadChat(): void {
showChat(); showChat();
loadMessages(); 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<void> {
// For JWT, we don't have a persistent token on page load // For JWT, we don't have a persistent token on page load
// So we'll just show the login form // So we'll just show the login form
showLogin(); 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();
+31 -5
View File
@@ -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 { getAuthHeaders, currentUser, authToken } from "./auth";
import { API_BASE_URL } from "./config"; import { API_BASE_URL } from "./config";
import { websocket } from "./websocket"; import { websocket } from "./websocket";
import type { Message, Messages, WebSocketMessage } from "./types"; 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 messagesContainer = document.querySelector('.chat-messages') as HTMLElement;
const messageDiv = document.createElement('div'); const messageDiv = document.createElement('div');
messageDiv.classList.add("message"); messageDiv.classList.add("message");
@@ -64,7 +78,13 @@ export function addMessage(message: Message, isAuthor: boolean) {
messagesContainer.scrollTop = messagesContainer.scrollHeight; 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`, { fetch(`${API_BASE_URL}/get_messages`, {
headers: getAuthHeaders() 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 input = document.querySelector('.message-input') as HTMLInputElement;
const message = input.value.trim(); const message = input.value.trim();
+24
View File
@@ -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'; 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`; 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"; export const PRODUCT_NAME: string = "FromChat";
+7 -1
View File
@@ -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 { showLogin } from "./auth";
import { PRODUCT_NAME } from "./config"; import { PRODUCT_NAME } from "./config";
showLogin(); showLogin();
document.getElementById("productname")!.textContent = PRODUCT_NAME; document.getElementById("productname")!.textContent = PRODUCT_NAME;
document.title = PRODUCT_NAME; document.title = PRODUCT_NAME;
+61 -30
View File
@@ -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 type { Dialog } from "mdui/components/dialog";
import { loadProfilePicture } from "./profile/upload"; import { loadProfilePicture } from "./profile/upload";
// сварачивание и разворачивание чата // сварачивание и разворачивание чата
const but = document.getElementById('chat-recrol')!; const chatCollapseBtn = document.getElementById('hide-chat')!;
const but_list1 = document.getElementById('chat1but')!; const chat1 = document.getElementById('chat-list-chat-1')!;
const but_list2 = document.getElementById('chat1but2')!; const chat2 = document.getElementById('chat-list-chat-2')!;
const cont1 = document.getElementById('conteinerchat')!; const chatInner = document.getElementById('chat-inner')!;
const namechat = document.getElementById('namechat')!; const chatName = document.getElementById('chat-name')!;
const profileButton = document.getElementById('profile-open')!;
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 dialog = document.getElementById("profile-dialog") as Dialog; const dialog = document.getElementById("profile-dialog") as Dialog;
const dialogClose = document.getElementById("profile-dialog-close")!; const dialogClose = document.getElementById("profile-dialog-close")!;
butprofile.addEventListener('click', () => { /**
dialog.open = true; * Sets up chat collapse functionality
// Load profile picture when dialog opens * @function setupChatCollapse
loadProfilePicture(); * @private
}); */
function setupChatCollapse(): void {
chatCollapseBtn.addEventListener('click', () => {
chatCollapseBtn.style.display = 'none';
chatInner.style.display = 'none';
});
}
dialogClose.addEventListener("click", () => { /**
* 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; dialog.open = false;
}); });
}
setupChatCollapse();
setupChatSwitching();
setupProfileDialog();
-11
View File
@@ -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);
+8 -2
View File
@@ -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 './css/style.scss';
import "mdui/mdui.css"; import "mdui/mdui.css";
import "./links"; import "./utils/material";
import "./material";
import "./chat"; import "./chat";
import "./settings"; import "./settings";
import "./leftpanel"; import "./leftpanel";
+14 -1
View File
@@ -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 type { Dialog } from "mdui/components/dialog";
import { loadProfileData } from './profile/editor'; import { loadProfileData } from './profile/editor';
import { loadProfilePicture, initializeProfileUpload } from "./profile/upload"; import { loadProfilePicture, initializeProfileUpload } from "./profile/upload";
@@ -15,7 +22,13 @@ form.addEventListener("submit", async (e) => {
dialog.open = false; 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 { export function initializeProfile(): void {
// Initialize profile modules // Initialize profile modules
initializeProfileUpload(); initializeProfileUpload();
-118
View File
@@ -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<ProfileData>)` - 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.
+47
View File
@@ -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 { getAuthHeaders } from '../auth';
import type { ProfileData, UploadResponse } from './types'; import type { ProfileData, UploadResponse } from './types';
/**
* Loads user profile data from the server
* @async
* @function loadProfile
* @returns {Promise<ProfileData | null>} 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<ProfileData | null> { export async function loadProfile(): Promise<ProfileData | null> {
try { try {
const response = await fetch('/api/user/profile', { const response = await fetch('/api/user/profile', {
@@ -18,6 +36,20 @@ export async function loadProfile(): Promise<ProfileData | null> {
} }
} }
/**
* Uploads a profile picture to the server
* @async
* @function uploadProfilePicture
* @param {Blob} file - The image file to upload
* @returns {Promise<UploadResponse | null>} 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<UploadResponse | null> { export async function uploadProfilePicture(file: Blob): Promise<UploadResponse | null> {
try { try {
const formData = new FormData(); const formData = new FormData();
@@ -39,6 +71,21 @@ export async function uploadProfilePicture(file: Blob): Promise<UploadResponse |
} }
} }
/**
* Updates user profile information
* @async
* @function updateProfile
* @param {Partial<ProfileData>} data - Profile data to update
* @returns {Promise<boolean>} 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<ProfileData>): Promise<boolean> { export async function updateProfile(data: Partial<ProfileData>): Promise<boolean> {
try { try {
const response = await fetch('/api/user/profile', { const response = await fetch('/api/user/profile', {
+83 -17
View File
@@ -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 { updateProfile } from './api';
import { loadProfile } 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'; import type { TextField } from 'mdui/components/text-field';
// DOM elements let profileForm = document.getElementById('profile-form')!;
let profileForm: HTMLElement; let nicknameField = document.getElementById('username-field') as unknown as TextField;
let nicknameField: TextField; // MDUI TextField let descriptionField = document.getElementById('description-field') as unknown as TextField;
let descriptionField: TextField; // MDUI TextField
/**
* Initialization state flag
* @type {boolean}
*/
let isInitialized = false; 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 { export function setUsernameValue(value: string): void {
if (nicknameField && nicknameField.value !== undefined) { if (nicknameField && nicknameField.value !== undefined) {
nicknameField.value = value; 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 { export function setDescriptionValue(value: string): void {
if (descriptionField && descriptionField.value !== undefined) { if (descriptionField && descriptionField.value !== undefined) {
descriptionField.value = value; 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 { export function getUsernameValue(): string {
if (nicknameField && nicknameField.value !== undefined) { if (nicknameField && nicknameField.value !== undefined) {
return nicknameField.value; return nicknameField.value;
@@ -28,6 +61,14 @@ export function getUsernameValue(): string {
return ''; 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 { export function getDescriptionValue(): string {
if (descriptionField && descriptionField.value !== undefined) { if (descriptionField && descriptionField.value !== undefined) {
return descriptionField.value; return descriptionField.value;
@@ -35,6 +76,13 @@ export function getDescriptionValue(): string {
return ''; return '';
} }
/**
* Loads profile data from the server and populates the form fields
* @async
* @function loadProfileData
* @example
* await loadProfileData();
*/
export async function loadProfileData(): Promise<void> { export async function loadProfileData(): Promise<void> {
const userData = await loadProfile(); const userData = await loadProfile();
if (userData) { if (userData) {
@@ -47,16 +95,14 @@ export async function loadProfileData(): Promise<void> {
} }
} }
// Setup form submission handler /**
function setupFormHandler(): void { * Handles profile form submission
if (isInitialized) return; * @async
* @function handleFormSubmission
// Get DOM elements * @param {Event} e - Form submission event
profileForm = document.getElementById('profile-form')!; * @private
nicknameField = document.getElementById('username-field') as any; // MDUI TextField */
descriptionField = document.getElementById('description-field') as any; // MDUI TextField async function handleFormSubmission(e: Event): Promise<void> {
profileForm.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
const nickname = getUsernameValue(); const nickname = getUsernameValue();
@@ -74,12 +120,32 @@ function setupFormHandler(): void {
showError('Ошибка при обновлении профиля'); 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;
descriptionField = document.getElementById('description-field') as any;
profileForm.addEventListener('submit', handleFormSubmission);
isInitialized = true; isInitialized = true;
} }
// Initialize editor functionality /**
* Initializes profile editor functionality
* @function initializeProfileEditor
* @example
* initializeProfileEditor();
*/
export function initializeProfileEditor(): void { export function initializeProfileEditor(): void {
setupFormHandler(); setupFormHandler();
} }
+107
View File
@@ -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"; import type { Size2D } from "../types";
/**
* Image cropper class for circular profile picture cropping
* @class ImageCropper
*/
export class ImageCropper { export class ImageCropper {
private canvas: HTMLCanvasElement; private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D;
/**
* Image element to be cropped
* @type {HTMLImageElement}
* @private
*/
private image!: HTMLImageElement; private image!: HTMLImageElement;
/**
* Size of the crop area (diameter)
* @type {number}
* @private
*/
private cropSize: number = 200; private cropSize: number = 200;
private isDragging: boolean = false; private isDragging: boolean = false;
/**
* Starting position of the drag operation
* @type {Size2D}
* @private
*/
private dragStart: Size2D = { x: 0, y: 0 }; private dragStart: Size2D = { x: 0, y: 0 };
/**
* Current position of the crop area
* @type {Size2D}
* @private
*/
private cropPosition: Size2D = { x: 0, y: 0 }; 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) { constructor(container: HTMLElement) {
this.canvas = document.createElement('canvas'); this.canvas = document.createElement('canvas');
this.canvas.width = this.cropSize; this.canvas.width = this.cropSize;
@@ -19,6 +61,11 @@ export class ImageCropper {
this.setupEventListeners(); this.setupEventListeners();
} }
/**
* Sets up mouse and touch event listeners
* @function setupEventListeners
* @private
*/
private setupEventListeners(): void { private setupEventListeners(): void {
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
this.canvas.addEventListener('mousemove', this.onMouseMove.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)); 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 { private onMouseDown(e: MouseEvent): void {
this.isDragging = true; this.isDragging = true;
this.dragStart = { x: e.clientX, y: e.clientY }; 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 { private onMouseMove(e: MouseEvent): void {
if (!this.isDragging) return; if (!this.isDragging) return;
@@ -46,10 +105,21 @@ export class ImageCropper {
this.render(); this.render();
} }
/**
* Handles mouse up events
* @function onMouseUp
* @private
*/
private onMouseUp(): void { private onMouseUp(): void {
this.isDragging = false; this.isDragging = false;
} }
/**
* Handles touch start events
* @param {TouchEvent} e - Touch event
* @function onTouchStart
* @private
*/
private onTouchStart(e: TouchEvent): void { private onTouchStart(e: TouchEvent): void {
e.preventDefault(); e.preventDefault();
const touch = e.touches[0]; const touch = e.touches[0];
@@ -57,6 +127,12 @@ export class ImageCropper {
this.dragStart = { x: touch.clientX, y: touch.clientY }; 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 { private onTouchMove(e: TouchEvent): void {
e.preventDefault(); e.preventDefault();
if (!this.isDragging) return; if (!this.isDragging) return;
@@ -72,10 +148,23 @@ export class ImageCropper {
this.render(); this.render();
} }
/**
* Handles touch end events
* @function onTouchEnd
* @private
*/
private onTouchEnd(): void { private onTouchEnd(): void {
this.isDragging = false; this.isDragging = false;
} }
/**
* Loads an image file for cropping
* @param {File} file - Image file to load
* @returns {Promise<void>} Promise that resolves when image is loaded
* @async
* @example
* await cropper.loadImage(fileInput.files[0]);
*/
loadImage(file: File): Promise<void> { loadImage(file: File): Promise<void> {
return new Promise((resolve) => { return new Promise((resolve) => {
this.image = new Image(); this.image = new Image();
@@ -87,6 +176,11 @@ export class ImageCropper {
}); });
} }
/**
* Renders the image with circular crop overlay
* @function render
* @private
*/
private render(): void { private render(): void {
if (!this.image) return; if (!this.image) return;
@@ -119,10 +213,23 @@ export class ImageCropper {
this.ctx.restore(); 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 { getCroppedImage(): string {
return this.canvas.toDataURL('image/jpeg', 0.8); return this.canvas.toDataURL('image/jpeg', 0.8);
} }
/**
* Destroys the cropper and removes the canvas from DOM
* @function destroy
* @example
* cropper.destroy();
*/
destroy(): void { destroy(): void {
if (this.canvas.parentNode) { if (this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas); this.canvas.parentNode.removeChild(this.canvas);
+19
View File
@@ -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 { export interface ProfileData {
profile_picture?: string; profile_picture?: string;
nickname?: string; nickname?: string;
description?: string; description?: string;
} }
/**
* Profile picture upload response structure
* @interface UploadResponse
* @property {string} profile_picture_url - URL to the uploaded profile picture
*/
export interface UploadResponse { export interface UploadResponse {
profile_picture_url: string; profile_picture_url: string;
} }
+115 -72
View File
@@ -1,35 +1,112 @@
/**
* @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 type { Dialog } from "mdui/components/dialog";
import { ImageCropper } from './image-cropper'; import { ImageCropper } from './image-cropper';
import { uploadProfilePicture } from './api'; import { uploadProfilePicture } from './api';
import { loadProfile } 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; let cropper: ImageCropper | null = null;
/**
* Initialization state flag
* @type {boolean}
*/
let isInitialized = false; let isInitialized = false;
// DOM elements let cropperDialog = document.getElementById('cropper-dialog') as Dialog;
let cropperDialog: Dialog; let fileInput = document.getElementById('pfp-file-input') as HTMLInputElement;
let fileInput: HTMLInputElement; let uploadBtn = document.getElementById('upload-pfp-btn')!;
let uploadBtn: HTMLElement; let cropSaveBtn = document.getElementById('crop-save')!;
let cropSaveBtn: HTMLElement; let cropCancelBtn = document.getElementById('crop-cancel')!;
let cropCancelBtn: HTMLElement; let cropperCloseBtn = document.getElementById('cropper-close')!;
let cropperCloseBtn: HTMLElement; let cropperArea = document.getElementById('cropper-area')!;
let cropperArea: HTMLElement;
// 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<void> {
// 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<void> {
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 { function setupEventListeners(): void {
if (isInitialized) return; 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', () => { uploadBtn.addEventListener('click', () => {
fileInput.click(); fileInput.click();
}); });
@@ -56,65 +133,31 @@ function setupEventListeners(): void {
isInitialized = true; isInitialized = true;
} }
async function openCropper(file: File): Promise<void> { /**
// Clear previous cropper * Loads and displays the user's profile picture
cropperArea.innerHTML = ''; * @async
* @function loadProfilePicture
// Create new cropper * @example
cropper = new ImageCropper(cropperArea); * await loadProfilePicture();
*/
// 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<void> {
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('Ошибка при загрузке фото');
}
}
export async function loadProfilePicture(): Promise<void> { export async function loadProfilePicture(): Promise<void> {
const userData = await loadProfile(); const userData = await loadProfile();
if (userData?.profile_picture) { if (userData?.profile_picture) {
const url = `${userData.profile_picture}?t=${Date.now()}`;
const profilePicture = document.getElementById('profile-picture') as HTMLImageElement; 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 { export function initializeProfileUpload(): void {
setupEventListeners(); setupEventListeners();
} }
+57 -26
View File
@@ -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"; import type { Dialog } from "mdui/components/dialog";
const dialog = document.getElementById('settings-dialog') as Dialog; const dialog = document.getElementById('settings-dialog') as Dialog;
@@ -8,13 +15,11 @@ const closeButton = document.getElementById('settings-close')!;
const settingsList = document.querySelector('#settings-menu mdui-list')!; const settingsList = document.querySelector('#settings-menu mdui-list')!;
const settingsPanels = document.querySelectorAll('.settings-panel'); const settingsPanels = document.querySelectorAll('.settings-panel');
// Initialize settings /**
function initializeSettings() { * Mapping between list item text and their corresponding panel IDs
// Add click listeners to all list items * @type {Object.<string, string>}
const listItems = settingsList.querySelectorAll('mdui-list-item'); */
const panelMapping = {
// Create a mapping between list items and their corresponding panels
const panelMapping = {
'Уведомления': 'notifications-settings', 'Уведомления': 'notifications-settings',
'Внешний вид': 'appearance-settings', 'Внешний вид': 'appearance-settings',
'Безопасность': 'security-settings', 'Безопасность': 'security-settings',
@@ -22,11 +27,17 @@ function initializeSettings() {
'Хранилище': 'storage-settings', 'Хранилище': 'storage-settings',
'Помощь': 'help-settings', 'Помощь': 'help-settings',
'О приложении': 'about-settings' 'О приложении': 'about-settings'
}; };
listItems.forEach((item) => { /**
item.addEventListener('click', () => { * 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 // Remove active class from all items and panels
const listItems = settingsList.querySelectorAll('mdui-list-item');
listItems.forEach(li => li.removeAttribute('active')); listItems.forEach(li => li.removeAttribute('active'));
settingsPanels.forEach(panel => panel.classList.remove('active')); settingsPanels.forEach(panel => panel.classList.remove('active'));
@@ -43,14 +54,26 @@ function initializeSettings() {
targetPanel.classList.add('active'); 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', () => handleListItemClick(item));
}); });
} }
// Dialog event listeners /**
openButton.addEventListener('click', () => { * Resets settings dialog to show the first panel
dialog.open = true; * @function resetToFirstPanel
// Reset to first panel when opening * @private
*/
function resetToFirstPanel(): void {
const firstItem = settingsList.querySelector('mdui-list-item'); const firstItem = settingsList.querySelector('mdui-list-item');
const firstPanel = document.querySelector('.settings-panel'); const firstPanel = document.querySelector('.settings-panel');
if (firstItem && firstPanel) { if (firstItem && firstPanel) {
@@ -59,15 +82,23 @@ openButton.addEventListener('click', () => {
firstItem.setAttribute('active', ''); firstItem.setAttribute('active', '');
firstPanel.classList.add('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();
+88 -2
View File
@@ -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.<string, string>} Headers
*/
export type Headers = {[x: string]: string} export type Headers = {[x: string]: string}
/**
* API error response structure
* @interface ErrorResponse
* @property {string} message - Error message from the server
*/
export interface ErrorResponse { export interface ErrorResponse {
message: string; message: string;
} }
/**
* 2D coordinate structure
* @interface Size2D
* @property {number} x - X coordinate
* @property {number} y - Y coordinate
*/
export interface Size2D { export interface Size2D {
x: number; x: number;
y: number; y: number;
} }
// App types // 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 { export interface Message {
id: number; id: number;
username: string; username: string;
@@ -20,10 +52,24 @@ export interface Message {
profile_picture?: string; profile_picture?: string;
} }
/**
* Collection of messages
* @interface Messages
* @property {Message[]} messages - Array of message objects
*/
export interface Messages { export interface Messages {
messages: Message[]; 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 { export interface User {
id: number; id: number;
created_at: string; created_at: string;
@@ -32,17 +78,30 @@ export interface User {
username: string; username: string;
} }
// ---------- // ----------
// API models // API models
// ---------- // ----------
// Requests // Requests
/**
* Login request structure
* @interface LoginRequest
* @property {string} username - Username for authentication
* @property {string} password - Password for authentication
*/
export interface LoginRequest { export interface LoginRequest {
username: string; username: string;
password: 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 { export interface RegisterRequest {
username: string; username: string;
password: string; password: string;
@@ -50,6 +109,13 @@ export interface RegisterRequest {
} }
// Responses // Responses
/**
* Login response structure
* @interface LoginResponse
* @property {User} user - User information
* @property {string} token - JWT authentication token
*/
export interface LoginResponse { export interface LoginResponse {
user: User; user: User;
token: string; token: string;
@@ -59,6 +125,14 @@ export interface LoginResponse {
// WebSocket types // 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 { export interface WebSocketMessage {
type: string; type: string;
credentials?: WebSocketCredentials; credentials?: WebSocketCredentials;
@@ -66,11 +140,23 @@ export interface WebSocketMessage {
error?: WebSocketError; error?: WebSocketError;
} }
/**
* WebSocket error structure
* @interface WebSocketError
* @property {number} code - Error code
* @property {string} detail - Error detail message
*/
export interface WebSocketError { export interface WebSocketError {
code: number; code: number;
detail: string; 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 { export interface WebSocketCredentials {
scheme: string; scheme: string;
credentials: string; credentials: string;
-12
View File
@@ -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<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -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/tabs';
import 'mdui/components/tab'; import 'mdui/components/tab';
import 'mdui/components/tab-panel'; import 'mdui/components/tab-panel';
@@ -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'; 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 { function showNotification(message: string, type: NotificationType): void {
const notification = document.createElement('div'); const notification = document.createElement('div');
notification.textContent = message; notification.textContent = message;
@@ -28,10 +46,24 @@ function showNotification(message: string, type: NotificationType): void {
}, 3000); }, 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 { export function showSuccess(message: string): void {
showNotification(message, 'success'); 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 { export function showError(message: string): void {
showNotification(message, 'error'); showNotification(message, 'error');
} }
+33
View File
@@ -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<void>} Promise that resolves after the delay
* @example
* await delay(1000); // Wait for 1 second
*/
export function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
+19 -2
View File
@@ -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 { currentUser } from "./auth";
import { addMessage } from "./chat"; import { addMessage } from "./chat";
import { API_FULL_BASE_URL } from "./config"; import { API_FULL_BASE_URL } from "./config";
import type { WebSocketMessage, Message } from "./types"; 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`); return new WebSocket(`ws://${API_FULL_BASE_URL}/chat/ws`);
} }
/**
* Global WebSocket instance
* @type {WebSocket}
*/
export let websocket = create(); export let websocket = create();
// -------------- // --------------