mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add profile
This commit is contained in:
+43
-21
@@ -193,19 +193,50 @@
|
||||
</div>
|
||||
|
||||
<mdui-dialog id="profile-dialog" close-on-overlay-click close-on-esc>
|
||||
<div class="header-top">
|
||||
<div class="left">
|
||||
<img id="profile-picture" src="./src/images/default-avatar.png" alt="Ваше фото" />
|
||||
<div class="profile-dialog-content">
|
||||
<div class="header-top">
|
||||
<div class="profile-picture-container">
|
||||
<img id="profile-picture" src="./src/images/default-avatar.png" alt="Ваше фото" />
|
||||
<mdui-button-icon icon="camera_alt--filled" id="upload-pfp-btn" class="upload-overlay" variant="filled"></mdui-button-icon>
|
||||
<input type="file" id="pfp-file-input" accept="image/*" style="display: none;">
|
||||
</div>
|
||||
<mdui-text-field id="username-field" label="Имя пользователя" variant="outlined" value="user123" autocomplete="username"></mdui-text-field>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div id="profile-username">Loading...</div>
|
||||
|
||||
<form id="profile-form">
|
||||
<mdui-text-field
|
||||
id="description-field"
|
||||
label="О себе"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows="3"
|
||||
placeholder="Расскажите о себе..."
|
||||
autocomplete="none"></mdui-text-field>
|
||||
<div class="dialog-actions">
|
||||
<mdui-button type="submit" id="profile-submit">Сохранить изменения</mdui-button>
|
||||
<mdui-button id="profile-dialog-close" variant="outlined">Закрыть</mdui-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<mdui-button id="profile-dialog-close">Закрыть</mdui-button>
|
||||
</p>
|
||||
</mdui-dialog>
|
||||
|
||||
<!-- Profile Picture Cropper Dialog -->
|
||||
<mdui-dialog id="cropper-dialog" close-on-overlay-click close-on-esc>
|
||||
<div class="cropper-dialog-content">
|
||||
<div class="cropper-header">
|
||||
<h3>Обрезать фото профиля</h3>
|
||||
<mdui-button-icon icon="close" id="cropper-close"></mdui-button-icon>
|
||||
</div>
|
||||
<div class="cropper-container">
|
||||
<div id="cropper-area"></div>
|
||||
</div>
|
||||
<div class="cropper-actions">
|
||||
<mdui-button id="crop-cancel" variant="outlined">Отмена</mdui-button>
|
||||
<mdui-button id="crop-save">Сохранить</mdui-button>
|
||||
</div>
|
||||
</div>
|
||||
</mdui-dialog>
|
||||
|
||||
<mdui-dialog id="settings-dialog" close-on-overlay-click close-on-esc fullscreen>
|
||||
<div class="fullscreen-wrapper">
|
||||
<div id="settings-dialog-inner">
|
||||
@@ -215,8 +246,7 @@
|
||||
</div>
|
||||
<div id="settings-menu">
|
||||
<mdui-list>
|
||||
<mdui-list-item icon="account_circle--filled" rounded active>Профиль</mdui-list-item>
|
||||
<mdui-list-item icon="notifications--filled" rounded>Уведомления</mdui-list-item>
|
||||
<mdui-list-item icon="notifications--filled" rounded active>Уведомления</mdui-list-item>
|
||||
<mdui-list-item icon="palette--filled" rounded>Внешний вид</mdui-list-item>
|
||||
<mdui-list-item icon="security--filled" rounded>Безопасность</mdui-list-item>
|
||||
<mdui-list-item icon="language--filled" rounded>Язык</mdui-list-item>
|
||||
@@ -225,15 +255,7 @@
|
||||
<mdui-list-item icon="info--filled" rounded>О приложении</mdui-list-item>
|
||||
</mdui-list>
|
||||
<div class="screen">
|
||||
<div id="profile-settings" class="settings-panel active">
|
||||
<h3>Профиль</h3>
|
||||
<mdui-text-field label="Имя пользователя" variant="outlined" value="user123"></mdui-text-field>
|
||||
<mdui-text-field label="Email" variant="outlined" type="email" value="user@example.com"></mdui-text-field>
|
||||
<mdui-text-field label="О себе" variant="outlined" multiline rows="3" placeholder="Расскажите о себе..."></mdui-text-field>
|
||||
<mdui-button>Сохранить изменения</mdui-button>
|
||||
</div>
|
||||
|
||||
<div id="notifications-settings" class="settings-panel">
|
||||
<div id="notifications-settings" class="settings-panel active">
|
||||
<h3>Уведомления</h3>
|
||||
<mdui-switch checked>Новые сообщения</mdui-switch>
|
||||
<mdui-switch checked>Звуковые уведомления</mdui-switch>
|
||||
@@ -288,7 +310,7 @@
|
||||
<div id="about-settings" class="settings-panel">
|
||||
<h3>О приложении</h3>
|
||||
<p>Версия: 1.0.0</p>
|
||||
<p>© 2024 Boost Chat. Все права защищены.</p>
|
||||
<p>© 2024 From Chat. Все права защищены.</p>
|
||||
<mdui-button variant="outlined">Политика конфиденциальности</mdui-button>
|
||||
<mdui-button variant="outlined">Условия использования</mdui-button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loadMessages } from "./chat";
|
||||
import { initializeProfile } from "./profile";
|
||||
import type { Headers, ErrorResponse, User, LoginResponse, LoginRequest, RegisterRequest } from "./types";
|
||||
import { API_BASE_URL } from "./config";
|
||||
|
||||
@@ -8,10 +9,13 @@ export let authToken: string | null = null;
|
||||
|
||||
|
||||
// Helper function to get auth headers
|
||||
export function getAuthHeaders(): Headers {
|
||||
const headers: Headers = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
export function getAuthHeaders(json: boolean = true): Headers {
|
||||
const headers: Headers = {};
|
||||
|
||||
if (json) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
if (authToken) {
|
||||
headers['Authorization'] = `Bearer ${authToken}`;
|
||||
}
|
||||
@@ -93,6 +97,7 @@ document.getElementById('login-form-element')!.addEventListener('submit', async
|
||||
currentUser = data.user;
|
||||
showChat();
|
||||
loadMessages(); // Start loading messages
|
||||
initializeProfile(); // Initialize profile after login
|
||||
} else {
|
||||
const data: ErrorResponse = await response.json();
|
||||
showAlert('login-alerts', data.message || 'Неверное имя пользователя или пароль', 'danger');
|
||||
|
||||
@@ -19,6 +19,22 @@ export function addMessage(message: Message, isAuthor: boolean) {
|
||||
const messageInner = document.createElement('div');
|
||||
messageInner.classList.add('message-inner');
|
||||
|
||||
// Add profile picture for received messages
|
||||
if (!isAuthor) {
|
||||
const profilePicDiv = document.createElement('div');
|
||||
profilePicDiv.classList.add('message-profile-pic');
|
||||
|
||||
const profileImg = document.createElement('img');
|
||||
profileImg.src = message.profile_picture || './src/images/default-avatar.png';
|
||||
profileImg.alt = message.username;
|
||||
profileImg.onerror = () => {
|
||||
profileImg.src = './src/images/default-avatar.png';
|
||||
};
|
||||
|
||||
profilePicDiv.appendChild(profileImg);
|
||||
messageDiv.appendChild(profilePicDiv);
|
||||
}
|
||||
|
||||
if (!isAuthor) {
|
||||
const usernameDiv = document.createElement('div');
|
||||
usernameDiv.classList.add('message-username');
|
||||
|
||||
@@ -151,6 +151,23 @@
|
||||
max-width: 70%;
|
||||
position: relative;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
.message-profile-pic {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.message-inner {
|
||||
padding: 0.8rem 1rem;
|
||||
@@ -174,6 +191,7 @@
|
||||
|
||||
&.sent {
|
||||
margin-left: auto;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-inner {
|
||||
background-color: $color-dark-primary-container;
|
||||
|
||||
@@ -2,33 +2,117 @@
|
||||
@use "common/material" as *;
|
||||
|
||||
#profile-dialog {
|
||||
.profile-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid $color-dark-outline;
|
||||
|
||||
.left {
|
||||
.profile-picture-container {
|
||||
position: relative;
|
||||
$size: 70px;
|
||||
|
||||
width: $size;
|
||||
height: $size;
|
||||
flex-shrink: 0;
|
||||
|
||||
#profile-picture {
|
||||
width: $size;
|
||||
height: $size;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.upload-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
mdui-text-field {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#profile-username {
|
||||
font-size: larger;
|
||||
#profile-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
mdui-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid $color-dark-outline;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cropper Dialog Styles
|
||||
#cropper-dialog {
|
||||
.cropper-dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 500px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.cropper-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid $color-dark-outline;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: $color-dark-on-surface;
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400px;
|
||||
background: $color-dark-surface-container;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
#cropper-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid $color-dark-outline;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 960 960" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M864.189 226.74C858.953 322.858 754.737 374.315 695.538 438.255C587.28 548.523 471.983 651.236 357.786 755.237C336.782 774.521 312.167 786.817 286.061 797.151C218.876 823.397 152.288 851.277 84.9569 877.096C33.4679 887.917 33.5929 837.125 51.9969 803.274C82.4819 737.544 111.888 671.506 134.694 602.632C140.147 588.119 146.394 574.096 158.351 563.118C316.129 418.731 472.524 272.847 629.387 127.473C671.701 84.4735 735.485 58.8855 787.599 101.237C828.067 131.816 858.407 175.705 866.226 226.252C865.547 226.415 864.868 226.577 864.189 226.74ZM577.502 375.208C475.229 474.325 371.382 574.967 267.068 676.062C282.355 693.01 298.422 710.233 316.028 726.33C423.063 629.117 532.768 534.003 635.414 429.155C615.93 411.005 596.96 393.333 577.502 375.208ZM511.033 300.112C405.345 398.52 300.168 496.453 194.564 594.783C208.416 610.826 222.61 627.264 238.338 645.479C342.681 543.636 445.97 442.823 549.75 341.53C536.093 326.92 523.852 313.825 511.033 300.112ZM740.017 332.746C771.509 304.027 835.255 253.663 812.494 206.585C746.706 89.9645 700.949 116.001 619.406 199.264C659.925 244.107 699.781 288.217 740.017 332.746ZM274.408 749.697C240.205 713.159 206.909 677.589 174.655 643.132C159.169 679.946 143.72 716.957 127.99 753.849C124.389 762.296 118.674 770.287 129.712 777.657C134.223 784.437 138.046 793.07 142.584 800.069C186.793 783.177 230.302 766.551 274.408 749.697ZM544.049 269.928C584.723 313.301 624.972 356.22 665.379 399.309C680.179 385.866 693.882 373.42 707.709 360.86C667.185 317.314 626.926 274.051 586.479 230.588C571.828 244.172 558.447 256.578 544.049 269.928Z" fill="#000000"/>
|
||||
<path d="M638.673 709.515C639.343 676.351 677.174 677.225 692.387 700.363C726.843 744.221 760.844 788.44 794.87 832.637C817.027 858.762 782.4 890.608 758.307 866.545C717.849 825.832 684.536 778.554 648.479 734.013C643.193 727.217 640.425 718.463 636.496 710.613C637.221 710.246 637.947 709.88 638.673 709.515Z" fill="#000000"/>
|
||||
<path d="M151.066 137.732C153.494 108.423 188.706 102.337 203.647 124.81C235.905 172.013 273.59 216.181 301.895 265.799C313.411 289.311 283.416 311.351 264.894 292.57C231.379 256.708 205.952 213.945 175.929 175.18C165.617 163.929 156.995 151.904 151.066 137.732Z" fill="#000000"/>
|
||||
<path d="M891.911 614.01C862.482 608.358 833.494 602.751 804.26 596.663C802.393 596.28 800.531 595.828 798.711 595.265C761.812 585.318 773.326 533.411 810.404 541.287C840.924 548.38 871.139 556.895 901.224 565.701C912.077 568.878 919.347 576.915 919.717 589.183C920.689 605.42 906.16 612.951 891.911 614.01Z" fill="#000000"/>
|
||||
<path d="M149.196 434.169C123.591 424.884 58.974 428.513 68.036 388.304C71.15 376.308 84.587 367.941 98.537 370.701C119.14 374.778 139.766 379.065 159.972 384.727C193.482 395.201 179.49 434.51 149.196 434.169Z" fill="#000000"/>
|
||||
<path d="M492.843 843.595C492.129 822.836 489.637 787.689 517.329 787.075C530.926 786.658 542.651 796.779 542.97 812.531C541.532 834.435 545.506 868.675 534.546 887.253C523.096 905.866 493.562 897.454 492.893 875.571C492.58 864.936 493.061 854.249 492.896 843.595C492.878 843.594 492.86 843.595 492.843 843.595Z" fill="#000000"/>
|
||||
<path d="M486.449 126.168C492.884 164.558 451.635 176.738 440.787 145.638C436.983 126.526 434.554 106.662 433.855 87.1565C434.131 56.1805 477.956 53.1725 482.89 83.7265C484.932 97.7525 485.576 112.019 486.967 126.142C486.794 126.151 486.621 126.159 486.449 126.168Z" fill="#000000"/>
|
||||
<path d="M577.5 375.203C596.958 393.329 615.928 411 635.411 429.15C532.766 533.997 423.06 629.111 316.026 726.324C298.428 710.235 282.345 692.996 267.066 676.056C371.38 574.962 475.228 474.319 577.5 375.203Z" fill="white"/>
|
||||
<path d="M511.031 300.109C523.849 313.822 536.091 326.917 549.748 341.527C445.968 442.82 342.68 543.633 238.336 645.476C222.608 627.26 208.415 610.823 194.562 594.78C300.166 496.45 405.344 398.517 511.031 300.109Z" fill="white"/>
|
||||
<path d="M740.018 332.744C699.782 288.215 659.926 244.106 619.406 199.263C701.068 115.89 746.72 90.0456 812.493 206.586C835.234 253.723 771.572 303.932 740.018 332.744Z" fill="#000000"/>
|
||||
<path d="M274.409 749.697C230.303 766.551 186.794 783.177 142.586 800.07C138.05 793.075 134.222 784.437 129.714 777.657C118.675 770.288 124.39 762.297 127.991 753.85C143.72 716.958 159.169 679.947 174.656 643.133C206.91 677.589 240.207 713.159 274.409 749.697Z" fill="white"/>
|
||||
<path d="M544.051 269.926C558.449 256.577 571.83 244.17 586.481 230.586C626.928 274.049 667.187 317.312 707.711 360.858C693.884 373.418 680.181 385.864 665.381 399.307C624.974 356.218 584.725 313.298 544.051 269.926Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@@ -1,4 +1,5 @@
|
||||
import type { Dialog } from "mdui/components/dialog";
|
||||
import { loadProfilePicture } from "./profile/upload";
|
||||
|
||||
// сварачивание и разворачивание чата
|
||||
const but = document.getElementById('chat-recrol')!;
|
||||
@@ -29,6 +30,8 @@ const dialogClose = document.getElementById("profile-dialog-close")!;
|
||||
|
||||
butprofile.addEventListener('click', () => {
|
||||
dialog.open = true;
|
||||
// Load profile picture when dialog opens
|
||||
loadProfilePicture();
|
||||
});
|
||||
|
||||
dialogClose.addEventListener("click", () => {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export type NotificationType = 'success' | 'error';
|
||||
|
||||
function showNotification(message: string, type: NotificationType): void {
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = message;
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
background: ${type === 'success' ? '#4caf50' : '#f44336'};
|
||||
z-index: 10000;
|
||||
font-family: inherit;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||
transition: opacity 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Fade out and remove
|
||||
setTimeout(() => {
|
||||
notification.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
notification.remove();
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
export function showSuccess(message: string): void {
|
||||
showNotification(message, 'success');
|
||||
}
|
||||
|
||||
export function showError(message: string): void {
|
||||
showNotification(message, 'error');
|
||||
}
|
||||
+28
-60
@@ -1,63 +1,31 @@
|
||||
// const fileInput = document.getElementById('fileInput') as HTMLInputElement;
|
||||
// const preview = document.getElementById('preview') as HTMLImageElement;
|
||||
// const preview1 = document.getElementById('preview1') as HTMLImageElement;
|
||||
// const button = document.getElementById('uploadButton')!;
|
||||
import type { Dialog } from "mdui/components/dialog";
|
||||
import { loadProfileData } from './profile/editor';
|
||||
import { loadProfilePicture, initializeProfileUpload } from "./profile/upload";
|
||||
import { initializeProfileEditor } from './profile/editor';
|
||||
|
||||
// if (fileInput && preview && button) {
|
||||
// // при клике по кнопке откроем диалог выбора файла
|
||||
// button.addEventListener('click', () => {
|
||||
// fileInput.click();
|
||||
// });
|
||||
// Handle profile form submission
|
||||
const form = document.getElementById("profile-form")!;
|
||||
const dialog = document.getElementById("profile-dialog") as Dialog;
|
||||
|
||||
// // при выборе файла — показываем его
|
||||
// fileInput.addEventListener('change', () => {
|
||||
// const file: File | null = fileInput.files ? fileInput.files[0] : null;
|
||||
// if (file) {
|
||||
// const reader: FileReader = new FileReader();
|
||||
// reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
// if (e.target && typeof e.target.result === 'string') {
|
||||
// preview.src = e.target.result;
|
||||
// preview1.src = e.target.result;
|
||||
// preview.style.display = 'block';
|
||||
// }
|
||||
// };
|
||||
// reader.readAsDataURL(file);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// TODO: Process form data if needed
|
||||
// For now, just close the dialog
|
||||
dialog.open = false;
|
||||
});
|
||||
|
||||
// const textDiv = document.getElementById('text-nik')!;
|
||||
// const input = document.getElementById('nik') as HTMLInputElement;
|
||||
// const button1 = document.getElementById('change-name')!;
|
||||
|
||||
// button1.addEventListener('click', () => {
|
||||
// if (input.style.display === 'none') {
|
||||
// // Переводим в режим редактирования
|
||||
// input.value = textDiv.textContent || '';
|
||||
// textDiv.style.display = 'none';
|
||||
// input.style.display = 'flex';
|
||||
// } else {
|
||||
// // Сохраняем изменения
|
||||
// textDiv.textContent = input.value;
|
||||
// textDiv.style.display = 'flex';
|
||||
// input.style.display = 'none';
|
||||
// }
|
||||
// });
|
||||
|
||||
// const textDiv2 = document.getElementById('text-discr')!;
|
||||
// const input2 = document.getElementById('discr') as HTMLInputElement;
|
||||
// const button2 = document.getElementById('change-discription')!;
|
||||
|
||||
// button2.addEventListener('click', () => {
|
||||
// if (input2.style.display === 'none') {
|
||||
// // Переводим в режим редактирования
|
||||
// input2.value = textDiv2.textContent || '';
|
||||
// textDiv2.style.display = 'none';
|
||||
// input2.style.display = 'flex';
|
||||
// } else {
|
||||
// // Сохраняем изменения
|
||||
// textDiv2.textContent = input2.value;
|
||||
// textDiv2.style.display = 'flex';
|
||||
// input2.style.display = 'none';
|
||||
// }
|
||||
// });
|
||||
// Initialize profile functionality after login
|
||||
export function initializeProfile(): void {
|
||||
// Initialize profile modules
|
||||
initializeProfileUpload();
|
||||
initializeProfileEditor();
|
||||
|
||||
// Load profile data
|
||||
Promise.all([
|
||||
loadProfilePicture(),
|
||||
loadProfileData()
|
||||
]).catch(error => {
|
||||
console.error('Error initializing profile:', error);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getAuthHeaders } from '../auth';
|
||||
import type { ProfileData, UploadResponse } from './types';
|
||||
|
||||
export async function loadProfile(): Promise<ProfileData | null> {
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadProfilePicture(file: Blob): Promise<UploadResponse | null> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('profile_picture', file, 'profile_picture.jpg');
|
||||
|
||||
const response = await fetch('/api/upload-profile-picture', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: getAuthHeaders(false)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfile(data: Partial<ProfileData>): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { updateProfile } from './api';
|
||||
import { loadProfile } from './api';
|
||||
import { showSuccess, showError } from '../notification';
|
||||
import type { TextField } from 'mdui/components/text-field';
|
||||
|
||||
// DOM elements
|
||||
let profileForm: HTMLElement;
|
||||
let nicknameField: TextField; // MDUI TextField
|
||||
let descriptionField: TextField; // MDUI TextField
|
||||
let isInitialized = false;
|
||||
|
||||
export function setUsernameValue(value: string): void {
|
||||
if (nicknameField && nicknameField.value !== undefined) {
|
||||
nicknameField.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function setDescriptionValue(value: string): void {
|
||||
if (descriptionField && descriptionField.value !== undefined) {
|
||||
descriptionField.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUsernameValue(): string {
|
||||
if (nicknameField && nicknameField.value !== undefined) {
|
||||
return nicknameField.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getDescriptionValue(): string {
|
||||
if (descriptionField && descriptionField.value !== undefined) {
|
||||
return descriptionField.value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function loadProfileData(): Promise<void> {
|
||||
const userData = await loadProfile();
|
||||
if (userData) {
|
||||
if (userData.nickname) {
|
||||
setUsernameValue(userData.nickname);
|
||||
}
|
||||
if (userData.description) {
|
||||
setDescriptionValue(userData.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup form submission handler
|
||||
function setupFormHandler(): void {
|
||||
if (isInitialized) return;
|
||||
|
||||
// Get DOM elements
|
||||
profileForm = document.getElementById('profile-form')!;
|
||||
nicknameField = document.getElementById('username-field') as any; // MDUI TextField
|
||||
descriptionField = document.getElementById('description-field') as any; // MDUI TextField
|
||||
|
||||
profileForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const nickname = getUsernameValue();
|
||||
const description = getDescriptionValue();
|
||||
|
||||
if (nickname || description) {
|
||||
const success = await updateProfile({
|
||||
nickname: nickname || undefined,
|
||||
description: description || undefined
|
||||
});
|
||||
|
||||
if (success) {
|
||||
showSuccess('Профиль обновлен!');
|
||||
} else {
|
||||
showError('Ошибка при обновлении профиля');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
// Initialize editor functionality
|
||||
export function initializeProfileEditor(): void {
|
||||
setupFormHandler();
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { Size2D } from "../types";
|
||||
|
||||
export class ImageCropper {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private image!: HTMLImageElement;
|
||||
private cropSize: number = 200;
|
||||
private isDragging: boolean = false;
|
||||
private dragStart: Size2D = { x: 0, y: 0 };
|
||||
private cropPosition: Size2D = { x: 0, y: 0 };
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = this.cropSize;
|
||||
this.canvas.height = this.cropSize;
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
|
||||
container.appendChild(this.canvas);
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
private setupEventListeners(): void {
|
||||
this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this));
|
||||
this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
|
||||
this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this));
|
||||
this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this));
|
||||
this.canvas.addEventListener('touchmove', this.onTouchMove.bind(this));
|
||||
this.canvas.addEventListener('touchend', this.onTouchEnd.bind(this));
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: e.clientX, y: e.clientY };
|
||||
}
|
||||
|
||||
private onMouseMove(e: MouseEvent): void {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const deltaX = e.clientX - this.dragStart.x;
|
||||
const deltaY = e.clientY - this.dragStart.y;
|
||||
|
||||
this.cropPosition.x += deltaX;
|
||||
this.cropPosition.y += deltaY;
|
||||
|
||||
this.dragStart = { x: e.clientX, y: e.clientY };
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onMouseUp(): void {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
private onTouchStart(e: TouchEvent): void {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
this.isDragging = true;
|
||||
this.dragStart = { x: touch.clientX, y: touch.clientY };
|
||||
}
|
||||
|
||||
private onTouchMove(e: TouchEvent): void {
|
||||
e.preventDefault();
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const deltaX = touch.clientX - this.dragStart.x;
|
||||
const deltaY = touch.clientY - this.dragStart.y;
|
||||
|
||||
this.cropPosition.x += deltaX;
|
||||
this.cropPosition.y += deltaY;
|
||||
|
||||
this.dragStart = { x: touch.clientX, y: touch.clientY };
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onTouchEnd(): void {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
loadImage(file: File): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
this.image = new Image();
|
||||
this.image.onload = () => {
|
||||
this.render();
|
||||
resolve();
|
||||
};
|
||||
this.image.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
if (!this.image) return;
|
||||
|
||||
// Clear canvas
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Calculate crop area
|
||||
const scale = Math.max(this.cropSize / this.image.width, this.cropSize / this.image.height);
|
||||
const scaledWidth = this.image.width * scale;
|
||||
const scaledHeight = this.image.height * scale;
|
||||
|
||||
// Draw image
|
||||
this.ctx.save();
|
||||
this.ctx.globalCompositeOperation = 'source-over';
|
||||
this.ctx.drawImage(
|
||||
this.image,
|
||||
this.cropPosition.x,
|
||||
this.cropPosition.y,
|
||||
scaledWidth,
|
||||
scaledHeight
|
||||
);
|
||||
this.ctx.restore();
|
||||
|
||||
// Draw crop overlay
|
||||
this.ctx.save();
|
||||
this.ctx.globalCompositeOperation = 'destination-in';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(this.cropSize / 2, this.cropSize / 2, this.cropSize / 2, 0, 2 * Math.PI);
|
||||
this.ctx.fill();
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
getCroppedImage(): string {
|
||||
return this.canvas.toDataURL('image/jpeg', 0.8);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.canvas.parentNode) {
|
||||
this.canvas.parentNode.removeChild(this.canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface ProfileData {
|
||||
profile_picture?: string;
|
||||
nickname?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UploadResponse {
|
||||
profile_picture_url: string;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { Dialog } from "mdui/components/dialog";
|
||||
import { ImageCropper } from './image-cropper';
|
||||
import { uploadProfilePicture } from './api';
|
||||
import { loadProfile } from './api';
|
||||
import { showSuccess, showError } from '../notification';
|
||||
|
||||
// Global variables
|
||||
let cropper: ImageCropper | null = null;
|
||||
let isInitialized = false;
|
||||
|
||||
// DOM elements
|
||||
let cropperDialog: Dialog;
|
||||
let fileInput: HTMLInputElement;
|
||||
let uploadBtn: HTMLElement;
|
||||
let cropSaveBtn: HTMLElement;
|
||||
let cropCancelBtn: HTMLElement;
|
||||
let cropperCloseBtn: HTMLElement;
|
||||
let cropperArea: HTMLElement;
|
||||
|
||||
// Setup event listeners
|
||||
function setupEventListeners(): void {
|
||||
if (isInitialized) return;
|
||||
|
||||
// Get DOM elements
|
||||
cropperDialog = document.getElementById('cropper-dialog') as Dialog;
|
||||
fileInput = document.getElementById('pfp-file-input') as HTMLInputElement;
|
||||
uploadBtn = document.getElementById('upload-pfp-btn')!;
|
||||
cropSaveBtn = document.getElementById('crop-save')!;
|
||||
cropCancelBtn = document.getElementById('crop-cancel')!;
|
||||
cropperCloseBtn = document.getElementById('cropper-close')!;
|
||||
cropperArea = document.getElementById('cropper-area')!;
|
||||
|
||||
uploadBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
openCropper(file);
|
||||
}
|
||||
});
|
||||
|
||||
cropSaveBtn.addEventListener('click', () => {
|
||||
saveCroppedImage();
|
||||
});
|
||||
|
||||
cropCancelBtn.addEventListener('click', () => {
|
||||
closeCropper();
|
||||
});
|
||||
|
||||
cropperCloseBtn.addEventListener('click', () => {
|
||||
closeCropper();
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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> {
|
||||
const userData = await loadProfile();
|
||||
if (userData?.profile_picture) {
|
||||
const profilePicture = document.getElementById('profile-picture') as HTMLImageElement;
|
||||
profilePicture.src = userData.profile_picture + '?t=' + Date.now(); // Cache bust
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize upload functionality
|
||||
export function initializeProfileUpload(): void {
|
||||
setupEventListeners();
|
||||
}
|
||||
@@ -15,7 +15,6 @@ function initializeSettings() {
|
||||
|
||||
// Create a mapping between list items and their corresponding panels
|
||||
const panelMapping = {
|
||||
'Профиль': 'profile-settings',
|
||||
'Уведомления': 'notifications-settings',
|
||||
'Внешний вид': 'appearance-settings',
|
||||
'Безопасность': 'security-settings',
|
||||
|
||||
@@ -4,6 +4,11 @@ export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Size2D {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
|
||||
// App types
|
||||
export interface Message {
|
||||
@@ -12,6 +17,7 @@ export interface Message {
|
||||
content: string;
|
||||
is_read: boolean;
|
||||
timestamp: string;
|
||||
profile_picture?: string;
|
||||
}
|
||||
|
||||
export interface Messages {
|
||||
|
||||
Reference in New Issue
Block a user