mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add documentation
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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;
|
||||||
+62
-31
@@ -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", () => {
|
/**
|
||||||
dialog.open = false;
|
* 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();
|
||||||
@@ -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);
|
|
||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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,39 +95,57 @@ export async function loadProfileData(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup form submission handler
|
/**
|
||||||
|
* Handles profile form submission
|
||||||
|
* @async
|
||||||
|
* @function handleFormSubmission
|
||||||
|
* @param {Event} e - Form submission event
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async function handleFormSubmission(e: Event): Promise<void> {
|
||||||
|
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 {
|
function setupFormHandler(): void {
|
||||||
if (isInitialized) return;
|
if (isInitialized) return;
|
||||||
|
|
||||||
// Get DOM elements
|
// Get DOM elements
|
||||||
profileForm = document.getElementById('profile-form')!;
|
profileForm = document.getElementById('profile-form')!;
|
||||||
nicknameField = document.getElementById('username-field') as any; // MDUI TextField
|
nicknameField = document.getElementById('username-field') as any;
|
||||||
descriptionField = document.getElementById('description-field') as any; // MDUI TextField
|
descriptionField = document.getElementById('description-field') as any;
|
||||||
|
|
||||||
profileForm.addEventListener('submit', async (e) => {
|
profileForm.addEventListener('submit', handleFormSubmission);
|
||||||
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('Ошибка при обновлении профиля');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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 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();
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-47
@@ -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,49 +15,65 @@ 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 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');
|
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
|
// Add active class to clicked item
|
||||||
const panelMapping = {
|
item.setAttribute('active', '');
|
||||||
'Уведомления': 'notifications-settings',
|
|
||||||
'Внешний вид': 'appearance-settings',
|
|
||||||
'Безопасность': 'security-settings',
|
|
||||||
'Язык': 'language-settings',
|
|
||||||
'Хранилище': 'storage-settings',
|
|
||||||
'Помощь': 'help-settings',
|
|
||||||
'О приложении': 'about-settings'
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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) => {
|
listItems.forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => handleListItemClick(item));
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
|
|||||||
Reference in New Issue
Block a user