Добавить новый дизайн

This commit is contained in:
2025-08-20 23:37:47 +03:00
Unverified
20 changed files with 590 additions and 329 deletions
+127 -53
View File
@@ -1,35 +1,51 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru"> <html lang="ru" class="mdui-theme-dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Loading...</title> <title>Loading...</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>"> <link rel="icon" href="./src/images/logo.png" />
</head> </head>
<body> <body>
<!-- Login Form --> <!-- Login Form -->
<div id="login-form" class="auth-container"> <div id="login-form" class="auth-container">
<div class="auth-card fade-in"> <div class="auth-card fade-in">
<div class="auth-header"> <div class="auth-header">
<h2><span class="material-symbols filled large">login</span> Добро пожаловать!</h2> <h2>
<span class="material-symbols filled large">login</span>
Добро пожаловать!
</h2>
<p>Войдите в свой аккаунт</p> <p>Войдите в свой аккаунт</p>
</div> </div>
<div class="auth-body"> <div class="auth-body">
<div id="login-alerts"></div> <div id="login-alerts"></div>
<form id="login-form-element"> <form id="login-form-element">
<div class="form-group"> <mdui-text-field
<label for="login-username">Имя пользователя</label> label="Имя пользователя"
<input type="text" class="form-control" id="login-username" name="username" required> id="login-username"
</div> name="username"
<div class="form-group"> variant="outlined"
<label for="login-password">Пароль</label> icon="person--filled"
<input type="password" class="form-control" id="login-password" name="password" required> autocomplete="username"
</div> required>
<button type="submit" class="btn btn-block">Войти</button> </mdui-text-field>
<mdui-text-field
label="Пароль"
id="login-password"
name="password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="current-password"
required>
</mdui-text-field>
<mdui-button type="submit">Войти</mdui-button>
</form> </form>
<div class="text-center mt-3"> <div class="text-center">
<p>Ещё нет аккаунта? <a href="#" id="register-link" class="link">Зарегистрируйтесь</a></p> <p>Ещё нет аккаунта? <a href="#" id="register-link" class="link">Зарегистрируйтесь</a></p>
</div> </div>
</div> </div>
@@ -40,31 +56,54 @@
<div id="register-form" class="auth-container" style="display: none;"> <div id="register-form" class="auth-container" style="display: none;">
<div class="auth-card fade-in"> <div class="auth-card fade-in">
<div class="auth-header"> <div class="auth-header">
<h2><span class="material-symbols filled large">person_add</span> Регистрация</h2> <h2>
<span class="material-symbols filled large">person_add</span>
Регистрация
</h2>
<p>Создайте новый аккаунт</p> <p>Создайте новый аккаунт</p>
</div> </div>
<div class="auth-body"> <div class="auth-body">
<div id="register-alerts"></div> <div id="register-alerts"></div>
<form id="register-form-element"> <form id="register-form-element">
<div class="form-group"> <mdui-text-field
<label for="register-username">Имя пользователя</label> label="Имя пользователя"
<input type="text" class="form-control" id="register-username" name="username" required> id="register-username"
<small class="text-muted">От 3 до 20 символов, без пробелов</small> name="username"
</div> variant="outlined"
<div class="form-group"> icon="person--filled"
<label for="register-password">Пароль</label> autocomplete="username"
<input type="password" class="form-control" id="register-password" name="password" required> maxlength="20"
<small class="text-muted">Минимум 5 символов, без пробелов</small> counter
</div> required>
<div class="form-group"> </mdui-text-field>
<label for="register-confirm-password">Подтвердите пароль</label> <mdui-text-field
<input type="password" class="form-control" id="register-confirm-password" name="confirm_password" required> label="Пароль"
</div> id="register-password"
<button type="submit" class="btn btn-block">Зарегистрироваться</button> name="password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="new-password"
required>
</mdui-text-field>
<mdui-text-field
label="Подтвердите пароль"
id="register-confirm-password"
name="confirm_password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="new-password"
required>
</mdui-text-field>
<mdui-button type="submit">Зарегистрироваться</mdui-button>
</form> </form>
<div class="text-center mt-3"> <div class="text-center">
<p> <p>
Уже есть аккаунт? Уже есть аккаунт?
<a href="#" id="login-link" class="link">Войдите</a> <a href="#" id="login-link" class="link">Войдите</a>
@@ -76,33 +115,59 @@
<!-- Chat Interface --> <!-- Chat Interface -->
<div id="chat-interface" style="display: none;"> <div id="chat-interface" style="display: none;">
<div class="header"> <div class="all-container">
<div class="header-content"> <div class="chat-list" id="chat-list">
<div class="logo"> <header class="chat-header-left">
<span id="productname">Loading...</span> <div id="productname">Loading...</div>
</div> <div class="profile">
<ul class="nav-links"> <a href="#" id="profbut"><img src="./src/images/default-avatar.png" alt=""></a>
<li>
<a href="#" id="chat-link">
<span class="material-symbols filled">chat_bubble</span> Общий чат
</a>
</li>
<li>
<a href="#" id="logout-link">
<span class="material-symbols filled">logout</span> Выйти
</a>
</li>
</ul>
</div>
</div> </div>
</header>
<div class="chat-tabs">
<mdui-tabs value="chats" full-width>
<mdui-tab value="chats">
Чаты
</mdui-tab>
<mdui-tab value="channels">
Каналы
</mdui-tab>
<mdui-tab value="contacts">
Контакты
</mdui-tab>
<mdui-tab-panel slot="panel" value="chats">
<mdui-list>
<mdui-list-item headline="Общий чат" description="Вы: Последнее сообщение" id="chat1but">
<img src="./src/images/default-avatar.png" alt="" slot="icon" />
</mdui-list-item>
<mdui-list-item headline="Общий чат 2" description="Вы: Последнее сообщение" id="chat1but2">
<img src="./src/images/default-avatar.png" alt="" slot="icon" />
</mdui-list-item>
</mdui-list>
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
</mdui-tabs>
</div>
<mdui-bottom-app-bar>
<mdui-button-icon icon="settings--filled"></mdui-button-icon>
<mdui-button-icon icon="group_add--filled"></mdui-button-icon>
<div style="flex-grow: 1"></div>
<mdui-fab icon="edit--filled"></mdui-fab>
</mdui-bottom-app-bar>
</div>
<div class="chat-container"> <div class="chat-container">
<div class="chat-main"> <div class="chat-main" id="conteinerchat">
<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">
<h4>Общий чат</h4> <div class="info-chat">
<p><span class="online-status"></span> Онлайн</p> <h4 id="namechat">Общий чат</h4>
<p>
<span class="online-status"></span> Онлайн
</p>
</div>
<a href="#" id="chat-recrol">Свернуть чат</a>
</div> </div>
</div> </div>
@@ -123,7 +188,16 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<mdui-dialog id="profile-dialog" close-on-overlay-click close-on-esc>
<p>
Скоро будет...
</p>
<p>
<mdui-button id="profile-dialog-close">Закрыть</mdui-button>
</p>
</mdui-dialog>
<script src="src/main.ts" type="module"></script> <script src="src/main.ts" type="module"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -1,4 +1,4 @@
import { loadMessages } from "./main"; import { loadMessages } from "./chat";
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";
+111
View File
@@ -0,0 +1,111 @@
import { getAuthHeaders, currentUser, authToken } from "./auth";
import { API_BASE_URL } from "./config";
import { websocket } from "./websocket";
import type { Message, Messages, WebSocketMessage } from "./types";
import { formatTime } from "./utils";
export function addMessage(message: Message, isAuthor: boolean) {
const messagesContainer = document.querySelector('.chat-messages') as HTMLElement;
const messageDiv = document.createElement('div');
messageDiv.classList.add("message");
if (isAuthor) {
messageDiv.classList.add("sent");
} else {
messageDiv.classList.add("received");
}
messageDiv.dataset.id = `${message.id}`;
const messageInner = document.createElement('div');
messageInner.classList.add('message-inner');
if (!isAuthor) {
const usernameDiv = document.createElement('div');
usernameDiv.classList.add('message-username');
usernameDiv.textContent = message.username;
messageInner.appendChild(usernameDiv);
}
const contentDiv = document.createElement('div');
contentDiv.textContent = message.content;
messageInner.appendChild(contentDiv);
const timeDiv = document.createElement('div');
timeDiv.classList.add('message-time');
timeDiv.textContent = formatTime(message.timestamp);
if (isAuthor && message.is_read) {
const checkIcon = document.createElement('span');
checkIcon.classList.add("material-symbols", "outlined");
timeDiv.appendChild(checkIcon);
}
messageInner.appendChild(timeDiv);
messageDiv.appendChild(messageInner);
messagesContainer.appendChild(messageDiv);
// Прокрутка к новому сообщению
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
export function loadMessages() {
fetch(`${API_BASE_URL}/get_messages`, {
headers: getAuthHeaders()
})
.then(response => response.json())
.then((data: Messages) => {
if (data.messages && data.messages.length > 0) {
const messagesContainer = document.querySelector('.chat-messages') as HTMLElement;
const lastMessage = messagesContainer.lastElementChild as HTMLElement
let lastMessageId: number = 0
if (lastMessage) {
lastMessageId = Number(lastMessage.dataset.id)
}
// Добавляем только новые сообщения
data.messages.forEach(msg => {
if (msg.id > lastMessageId) {
addMessage(msg, msg.username == currentUser!.username);
}
});
}
});
}
export function sendMessage() {
const input = document.querySelector('.message-input') as HTMLInputElement;
const message = input.value.trim();
if (message) {
const payload: WebSocketMessage = {
data: {
content: message
},
credentials: {
scheme: "Bearer",
credentials: authToken!
},
type: "sendMessage"
}
let callback: ((e: MessageEvent) => void) | null = null
callback = (e) => {
websocket.removeEventListener("message", callback!);
const response: WebSocketMessage = JSON.parse(e.data)
console.log(response)
if (!response.error) {
input.value = "";
}
}
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
}
}
document.getElementById('message-form')!.addEventListener('submit', (e) => {
e.preventDefault();
sendMessage();
});
+16 -11
View File
@@ -1,4 +1,5 @@
@use "common/colors" as *; @use "common/colors" as *;
@use "common/material" as *;
.auth-container { .auth-container {
display: flex; display: flex;
@@ -6,30 +7,27 @@
align-items: center; align-items: center;
min-height: 100vh; min-height: 100vh;
padding: 2rem; padding: 2rem;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); background-color: $color-dark-surface;
.auth-card { .auth-card {
background: white; background-color: $color-dark-surface-container;
color: $color-dark-on-surface;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
max-width: 450px; max-width: 450px;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
}
} }
.auth-header { .auth-header {
background: linear-gradient(135deg, $primary, $primary-dark); margin: 0;
color: white; padding: 16px;
padding: 1.5rem; padding-bottom: 0;
text-align: center; text-align: center;
h2 { h2 {
font-size: 1.8rem; font-size: 1.8rem;
margin: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
align-items: center; align-items: center;
display: flex; display: flex;
@@ -40,6 +38,13 @@
} }
.auth-body { .auth-body {
padding: 2rem; padding: 25px;
padding-bottom: 16px;
form {
display: flex;
flex-direction: column;
gap: 10px;
}
} }
} }
+40 -10
View File
@@ -2,15 +2,18 @@
@use "common/material" as *; @use "common/material" as *;
#chat-interface { #chat-interface {
/* Header */
.header { .header {
display: flex;
background-color: $color-dark-surface-container; background-color: $color-dark-surface-container;
color: white; color: white;
padding: 16px 16px; padding: 16px 16px;
position: fixed; position: static;
width: 100%; justify-content: end;
top: 0; width: fit-content;
z-index: 1000; z-index: 1000;
position: absolute;
top: 0;
right: 0;
.header-content { .header-content {
display: flex; display: flex;
@@ -24,8 +27,8 @@
align-items: center; align-items: center;
} }
.nav-links { #logouts {
display: flex; display: none;
list-style: none; list-style: none;
gap: 10px; gap: 10px;
@@ -53,13 +56,14 @@
.chat-container { .chat-container {
display: flex; display: flex;
height: calc(100vh - 70px); width: 100%;
margin-top: 70px; flex-direction: column;
.chat-main { .chat-main {
flex: 1; flex-grow: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
.chat-header { .chat-header {
padding: 16px; padding: 16px;
@@ -71,21 +75,30 @@
.chat-header-avatar { .chat-header-avatar {
width: 45px; width: 45px;
height: 45px; height: 45px;
border-radius: 50%; border-radius: 20%;
object-fit: cover; object-fit: cover;
margin-right: 1rem; margin-right: 1rem;
} }
.chat-header-info { .chat-header-info {
display: flex;
.info-chat {
display: flex;
flex-direction: column;
h4 { h4 {
font-size: 1.1rem; font-size: 1.1rem;
margin: 0;
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
p { p {
margin: 0;
font-size: 0.8rem; font-size: 0.8rem;
color: #718096; color: #718096;
} }
}
.online-status { .online-status {
display: inline-block; display: inline-block;
@@ -95,6 +108,23 @@
background-color: $success; background-color: $success;
margin-right: 5px; margin-right: 5px;
} }
a {
display: flex;
flex-direction: row;
text-decoration: none;
color: white;
justify-content: end;
padding: 0;
margin: 0;
position: absolute;
right: 2%;
top: 2%;
&:hover {
border: none;
}
}
} }
} }
+123
View File
@@ -0,0 +1,123 @@
@use "common/colors" as *;
@use "common/material" as *;
// контейнер чата и панели с чатами
.all-container {
display: flex;
flex-direction: row;
width: 100%;
height: 100vh;
}
#profile {
display: none;
flex-direction: column;
z-index: 2000;
top: 0;
left: 0;
position: fixed;
height: 100vh;
width: 27%;
background-color: $color-dark-surface-container;
position: relative;
.profileheader {
display: flex;
gap: 200px;
p {
color: white;
}
a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
}
}
#chat-list {
display: flex;
flex-direction: column;
flex-grow: 0;
width: 40%;
background-color: $color-dark-surface-container;
height: 100%;
z-index: 1000;
.chat-header-left {
display: flex;
color: white;
font-size: 25px;
background-color: $color-dark-surface-container;
width: 100%;
justify-content: center;
gap: 110px;
align-items: center;
padding: 16px;
#productname {
flex-grow: 1;
}
.profile {
font-size: 24px;
display: flex;
justify-content: start;
flex-shrink: 0;
flex-grow: 0;
#closeprofile a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
img {
$size: 45px;
display: flex;
border-radius: 50%;
width: $size;
height: $size;
}
}
}
.chat-tabs {
margin-top: 5px;
width: 100%;
--mdui-color-surface: $color-dark-surface-container;
--mdui-color-surface-variant: transparent;
img {
width: 45px;
height: 45px;
border-radius: 20%;
object-fit: cover;
margin-right: 1rem;
}
}
mdui-bottom-app-bar {
position: relative;
width: 100%;
padding-left: 16px;
padding-right: 16px;
margin-top: auto;
}
}
-6
View File
@@ -1,8 +1,2 @@
$primary: #6C63FF;
$primary-dark: #564FD9;
$secondary: #FF6584;
$dark: #2D3748;
$light: #F7FAFC;
$gray: #E2E8F0;
$success: #48BB78; $success: #48BB78;
$danger: #F56565; $danger: #F56565;
+2 -60
View File
@@ -1,62 +1,4 @@
@use "colors" as *; @use "material" as *;
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 15px;
}
.form-group {
margin-bottom: 1.5rem;
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: $dark;
}
}
.form-control {
width: 100%;
padding: 0.8rem 1rem;
border: 1px solid $gray;
border-radius: 6px;
font-size: 1rem;
transition: all 0.3s ease;
&:focus {
outline: none;
border-color: $primary;
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.2);
}
}
.btn {
display: inline-block;
background: linear-gradient(135deg, $primary, $primary-dark);
color: white;
border: none;
padding: 0.8rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
text-decoration: none;
&:hover {
background: linear-gradient(135deg, $primary-dark, $primary);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
}
.btn-block {
display: block;
width: 100%;
}
.text-center { .text-center {
text-align: center; text-align: center;
@@ -83,7 +25,7 @@
} }
.link { .link {
color: $primary; color: $color-dark-primary;
font-weight: 600; font-weight: 600;
} }
+5 -1
View File
@@ -50,7 +50,11 @@ $color-dark-surface-container-low: rgb(24 28 31);
$color-dark-surface-container: rgb(28 32 36); $color-dark-surface-container: rgb(28 32 36);
$color-dark-surface-container-high: rgb(38 43 46); $color-dark-surface-container-high: rgb(38 43 46);
$color-dark-surface-container-highest: rgb(49 53 57); $color-dark-surface-container-highest: rgb(49 53 57);
// custom colors
$color-1: rgb(82, 109, 246);
$color-2: rgb(65, 11, 113);
$color-4: rgb(95, 26, 198);
$color-3: rgb(49, 71, 179);
// Light // Light
$color-light-primary: rgb(31 101 134); $color-light-primary: rgb(31 101 134);
$color-light-surface-tint: rgb(31 101 134); $color-light-surface-tint: rgb(31 101 134);
@@ -1,11 +1,25 @@
/* fallback */ @mixin iconsFontStyle() {
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(material-symbols.woff2) format('woff2'); src: url(material-symbols.woff2) format('woff2');
} }
/* fallback */
@font-face {
font-family: 'Material Symbols Outlined';
@include iconsFontStyle();
}
@font-face {
font-family: 'Material Icons Outlined';
@include iconsFontStyle();
}
@font-face {
font-family: 'Material Icons';
@include iconsFontStyle();
}
.material-symbols { .material-symbols {
font-family: 'Material Symbols Outlined'; font-family: 'Material Symbols Outlined';
font-weight: normal; font-weight: normal;
+15 -2
View File
@@ -1,5 +1,6 @@
@use "auth"; @use "auth";
@use "chat"; @use "chat";
@use "panelchat";
@use "common/animations"; @use "common/animations";
@use "common/components"; @use "common/components";
@use "common/colors" as *; @use "common/colors" as *;
@@ -8,9 +9,8 @@
@use "lib/fonts/montserrat"; @use "lib/fonts/montserrat";
@use "lib/fonts/material-symbols"; @use "lib/fonts/material-symbols";
* { * {
margin: 0;
padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -19,4 +19,17 @@ body {
background-color: $color-dark-surface; background-color: $color-dark-surface;
color: $color-dark-on-surface; color: $color-dark-on-surface;
line-height: 1.6; line-height: 1.6;
height: 100vh;
position: relative;
margin: 0;
}
mdui-dialog {
> *:first-child {
margin-block-start: 0;
}
> *:last-child {
margin-block-end: 0;
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+7
View File
@@ -0,0 +1,7 @@
import { showLogin } from "./auth";
import { PRODUCT_NAME } from "./config";
showLogin();
document.getElementById("productname")!.textContent = PRODUCT_NAME;
document.title = PRODUCT_NAME;
+36
View File
@@ -0,0 +1,36 @@
import type { Dialog } from "mdui/components/dialog";
// сварачивание и разворачивание чата
const but = document.getElementById('chat-recrol')!;
const but_list1 = document.getElementById('chat1but')!;
const but_list2 = document.getElementById('chat1but2')!;
const cont1 = document.getElementById('conteinerchat')!;
const namechat = document.getElementById('namechat')!;
but.addEventListener('click', () => {
but.style.display = 'none';
cont1.style.display = 'none';
});
but_list1.addEventListener('click', () => {
but.style.display = 'flex';
cont1.style.display = 'flex';
namechat.textContent = 'общий чат';
});
but_list2.addEventListener('click', () => {
but.style.display = 'flex';
cont1.style.display = 'flex';
namechat.textContent = 'общий чат 2';
});
// открытие профиля
const butprofile = document.getElementById('profbut')!;
const dialog = document.getElementById("profile-dialog") as Dialog;
const dialogClose = document.getElementById("profile-dialog-close")!;
butprofile.addEventListener('click', () => {
dialog.open = true;
});
dialogClose.addEventListener("click", () => {
dialog.open = false;
});
+5 -5
View File
@@ -1,11 +1,11 @@
import { loadChat, logout, showLogin, showRegister } from "./auth"; import { /* loadChat, */ /* logout, */ showLogin, showRegister } from "./auth";
const login = document.getElementById("login-link")!; const login = document.getElementById("login-link")!;
const register = document.getElementById("register-link")!; const register = document.getElementById("register-link")!;
const chat = document.getElementById("chat-link")!; // const chat = document.getElementById("chat-link")!;
const logoutLink = document.getElementById("logout-link")!; // const logoutLink = document.getElementById("logout-link")!;
login.addEventListener("click", showLogin); login.addEventListener("click", showLogin);
register.addEventListener("click", showRegister); register.addEventListener("click", showRegister);
chat.addEventListener("click", loadChat); // chat.addEventListener("click", loadChat);
logoutLink.addEventListener("click", logout); // logoutLink.addEventListener("click", logout);
+6 -144
View File
@@ -1,146 +1,8 @@
import './css/style.scss'; import './css/style.scss';
import { showLogin, getAuthHeaders, authToken, currentUser } from './auth'; import "mdui/mdui.css";
import { API_BASE_URL, API_FULL_BASE_URL, PRODUCT_NAME } from './config';
import type { Message, Messages, WebSocketMessage } from './types';
import "./links"; import "./links";
import "./material";
const websocket = new WebSocket(`ws://${API_FULL_BASE_URL}/chat/ws`); import "./chat";
import "./leftpanel";
import "./init";
// Функция для форматирования времени
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 addMessage(message: Message, isAuthor: boolean) {
const messagesContainer = document.querySelector('.chat-messages') as HTMLElement;
const messageDiv = document.createElement('div');
messageDiv.classList.add("message");
if (isAuthor) {
messageDiv.classList.add("sent");
} else {
messageDiv.classList.add("received");
}
messageDiv.dataset.id = `${message.id}`;
const messageInner = document.createElement('div');
messageInner.classList.add('message-inner');
if (!isAuthor) {
const usernameDiv = document.createElement('div');
usernameDiv.classList.add('message-username');
usernameDiv.textContent = message.username;
messageInner.appendChild(usernameDiv);
}
const contentDiv = document.createElement('div');
contentDiv.textContent = message.content;
messageInner.appendChild(contentDiv);
const timeDiv = document.createElement('div');
timeDiv.classList.add('message-time');
timeDiv.textContent = formatTime(message.timestamp);
if (isAuthor && message.is_read) {
const checkIcon = document.createElement('span');
checkIcon.classList.add("material-symbols", "outlined");
timeDiv.appendChild(checkIcon);
}
messageInner.appendChild(timeDiv);
messageDiv.appendChild(messageInner);
messagesContainer.appendChild(messageDiv);
// Прокрутка к новому сообщению
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// Загрузка сообщений
export function loadMessages() {
fetch(`${API_BASE_URL}/get_messages`, {
headers: getAuthHeaders()
})
.then(response => response.json())
.then((data: Messages) => {
if (data.messages && data.messages.length > 0) {
const messagesContainer = document.querySelector('.chat-messages') as HTMLElement;
const lastMessage = messagesContainer.lastElementChild as HTMLElement
let lastMessageId: number = 0
if (lastMessage) {
lastMessageId = Number(lastMessage.dataset.id)
}
// Добавляем только новые сообщения
data.messages.forEach(msg => {
if (msg.id > lastMessageId) {
addMessage(msg, msg.username == currentUser!.username);
}
});
}
});
}
// Отправка сообщения
export function sendMessage() {
const input = document.querySelector('.message-input') as HTMLInputElement;
const message = input.value.trim();
if (message) {
const payload: WebSocketMessage = {
data: {
content: message
},
credentials: {
scheme: "Bearer",
credentials: authToken!
},
type: "sendMessage"
}
let callback: ((e: MessageEvent) => void) | null = null
callback = (e) => {
websocket.removeEventListener("message", callback!);
const response: WebSocketMessage = JSON.parse(e.data)
console.log(response)
if (!response.error) {
input.value = "";
}
}
websocket.addEventListener("message", callback);
websocket.send(JSON.stringify(payload));
}
}
// --------------
// Initialization
// --------------
websocket.addEventListener("message", (e) => {
const message: WebSocketMessage = JSON.parse(e.data);
switch (message.type) {
case "newMessage": {
const newMessage: Message = message.data;
addMessage(newMessage, newMessage.username == currentUser!.username);
break;
}
}
});
showLogin();
document.getElementById('message-form')!.addEventListener('submit', (e) => {
e.preventDefault();
sendMessage();
});
document.getElementById("productname")!.textContent = PRODUCT_NAME;
document.title = PRODUCT_NAME;
+15
View File
@@ -0,0 +1,15 @@
import 'mdui/components/tabs';
import 'mdui/components/tab';
import 'mdui/components/tab-panel';
import 'mdui/components/list';
import 'mdui/components/list-item';
import 'mdui/components/bottom-app-bar';
import 'mdui/components/button-icon';
import 'mdui/components/fab';
import 'mdui/components/dialog';
import 'mdui/components/button';
import 'mdui/components/text-field';
import { setColorScheme } from 'mdui/functions/setColorScheme.js';
setColorScheme("#91cef4");
+8
View File
@@ -0,0 +1,8 @@
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;
}
+22
View File
@@ -0,0 +1,22 @@
import { currentUser } from "./auth";
import { addMessage } from "./chat";
import { API_FULL_BASE_URL } from "./config";
import type { WebSocketMessage, Message } from "./types";
export const websocket = new WebSocket(`ws://${API_FULL_BASE_URL}/chat/ws`);
// --------------
// Initialization
// --------------
websocket.addEventListener("message", (e) => {
const message: WebSocketMessage = JSON.parse(e.data);
switch (message.type) {
case "newMessage": {
const newMessage: Message = message.data;
addMessage(newMessage, newMessage.username == currentUser!.username);
break;
}
}
});
+7 -6
View File
@@ -7,24 +7,25 @@
"backend:run": "cd backend && ../.venv/bin/fastapi dev --port 8300 main.py", "backend:run": "cd backend && ../.venv/bin/fastapi dev --port 8300 main.py",
"backend:dependencies": "python3 -m venv .venv && ./.venv/bin/pip3 install -r backend/requirements.txt", "backend:dependencies": "python3 -m venv .venv && ./.venv/bin/pip3 install -r backend/requirements.txt",
"backend:reinstall": "rm -rf .venv && npm run backend:dependencies", "backend:reinstall": "rm -rf .venv && npm run backend:dependencies",
"frontend:dev": "vite frontend", "frontend:dev": "vite frontend",
"frontend:build": "tsc --project frontend && vite build frontend", "frontend:build": "tsc --project frontend && vite build frontend",
"frontend:preview": "vite preview frontend", "frontend:preview": "vite preview frontend",
"frontend:dependencies": "npm install", "frontend:dependencies": "npm install",
"dev": "concurrently 'npm run frontend:dev' 'npm run backend:run'", "dev": "concurrently 'npm run frontend:dev' 'npm run backend:run'",
"build": "npm run frontend:build", "build": "npm run frontend:build",
"install": "npm run backend:dependencies" "install": "npm run backend:dependencies"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.21",
"concurrently": "^9.2.0", "concurrently": "^9.2.0",
"sass-embedded": "^1.90.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"sass-embedded": "^1.90.0",
"terser": "^5.43.1",
"typescript": "~5.9.2", "typescript": "~5.9.2",
"vite": "^7.1.2", "vite": "^7.1.2",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2"
"terser": "^5.43.1", },
"autoprefixer": "^10.4.21" "dependencies": {
"mdui": "^2.1.4"
} }
} }