mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Добавить новый дизайн
This commit is contained in:
+144
-70
@@ -1,35 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<html lang="ru" class="mdui-theme-dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
<body>
|
||||
<!-- Login Form -->
|
||||
<div id="login-form" class="auth-container">
|
||||
<div class="auth-card fade-in">
|
||||
<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>
|
||||
</div>
|
||||
<div class="auth-body">
|
||||
<div id="login-alerts"></div>
|
||||
|
||||
<form id="login-form-element">
|
||||
<div class="form-group">
|
||||
<label for="login-username">Имя пользователя</label>
|
||||
<input type="text" class="form-control" id="login-username" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Пароль</label>
|
||||
<input type="password" class="form-control" id="login-password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-block">Войти</button>
|
||||
<mdui-text-field
|
||||
label="Имя пользователя"
|
||||
id="login-username"
|
||||
name="username"
|
||||
variant="outlined"
|
||||
icon="person--filled"
|
||||
autocomplete="username"
|
||||
required>
|
||||
</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>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<div class="text-center">
|
||||
<p>Ещё нет аккаунта? <a href="#" id="register-link" class="link">Зарегистрируйтесь</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,31 +56,54 @@
|
||||
<div id="register-form" class="auth-container" style="display: none;">
|
||||
<div class="auth-card fade-in">
|
||||
<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>
|
||||
</div>
|
||||
<div class="auth-body">
|
||||
<div id="register-alerts"></div>
|
||||
|
||||
<form id="register-form-element">
|
||||
<div class="form-group">
|
||||
<label for="register-username">Имя пользователя</label>
|
||||
<input type="text" class="form-control" id="register-username" name="username" required>
|
||||
<small class="text-muted">От 3 до 20 символов, без пробелов</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-password">Пароль</label>
|
||||
<input type="password" class="form-control" id="register-password" name="password" required>
|
||||
<small class="text-muted">Минимум 5 символов, без пробелов</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-confirm-password">Подтвердите пароль</label>
|
||||
<input type="password" class="form-control" id="register-confirm-password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-block">Зарегистрироваться</button>
|
||||
<mdui-text-field
|
||||
label="Имя пользователя"
|
||||
id="register-username"
|
||||
name="username"
|
||||
variant="outlined"
|
||||
icon="person--filled"
|
||||
autocomplete="username"
|
||||
maxlength="20"
|
||||
counter
|
||||
required>
|
||||
</mdui-text-field>
|
||||
<mdui-text-field
|
||||
label="Пароль"
|
||||
id="register-password"
|
||||
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>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<div class="text-center">
|
||||
<p>
|
||||
Уже есть аккаунт?
|
||||
<a href="#" id="login-link" class="link">Войдите</a>
|
||||
@@ -76,54 +115,89 @@
|
||||
|
||||
<!-- Chat Interface -->
|
||||
<div id="chat-interface" style="display: none;">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<span id="productname">Loading...</span>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<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 class="chat-container">
|
||||
<div class="chat-main">
|
||||
<div class="chat-header">
|
||||
<img src="src/images/default-avatar.png" alt="Avatar" class="chat-header-avatar">
|
||||
<div class="chat-header-info">
|
||||
<h4>Общий чат</h4>
|
||||
<p><span class="online-status"></span> Онлайн</p>
|
||||
<div class="all-container">
|
||||
<div class="chat-list" id="chat-list">
|
||||
<header class="chat-header-left">
|
||||
<div id="productname">Loading...</div>
|
||||
<div class="profile">
|
||||
<a href="#" id="profbut"><img src="./src/images/default-avatar.png" alt=""></a>
|
||||
</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>
|
||||
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<!-- Messages will be loaded here dynamically -->
|
||||
<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-main" id="conteinerchat">
|
||||
<div class="chat-header">
|
||||
<img src="src/images/default-avatar.png" alt="Avatar" class="chat-header-avatar">
|
||||
<div class="chat-header-info">
|
||||
<div class="info-chat">
|
||||
<h4 id="namechat">Общий чат</h4>
|
||||
<p>
|
||||
<span class="online-status"></span> Онлайн
|
||||
</p>
|
||||
</div>
|
||||
<a href="#" id="chat-recrol">Свернуть чат</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-wrapper">
|
||||
<div class="chat-input">
|
||||
<form class="input-group" id="message-form">
|
||||
<input type="text" class="message-input" id="message-input" placeholder="Напишите сообщение..." autocomplete="off">
|
||||
<button type="submit" class="send-btn">
|
||||
<span class="material-symbols filled">send</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<!-- Messages will be loaded here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="chat-input-wrapper">
|
||||
<div class="chat-input">
|
||||
<form class="input-group" id="message-form">
|
||||
<input type="text" class="message-input" id="message-input" placeholder="Напишите сообщение..." autocomplete="off">
|
||||
<button type="submit" class="send-btn">
|
||||
<span class="material-symbols filled">send</span>
|
||||
</button>
|
||||
</form>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { loadMessages } from "./main";
|
||||
import { loadMessages } from "./chat";
|
||||
import type { Headers, ErrorResponse, User, LoginResponse, LoginRequest, RegisterRequest } from "./types";
|
||||
import { API_BASE_URL } from "./config";
|
||||
|
||||
|
||||
@@ -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
@@ -1,4 +1,5 @@
|
||||
@use "common/colors" as *;
|
||||
@use "common/material" as *;
|
||||
|
||||
.auth-container {
|
||||
display: flex;
|
||||
@@ -6,30 +7,27 @@
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
background-color: $color-dark-surface;
|
||||
|
||||
.auth-card {
|
||||
background: white;
|
||||
background-color: $color-dark-surface-container;
|
||||
color: $color-dark-on-surface;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 450px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
background: linear-gradient(135deg, $primary, $primary-dark);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-size: 1.8rem;
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -40,6 +38,13 @@
|
||||
}
|
||||
|
||||
.auth-body {
|
||||
padding: 2rem;
|
||||
padding: 25px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
-17
@@ -2,15 +2,18 @@
|
||||
@use "common/material" as *;
|
||||
|
||||
#chat-interface {
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
background-color: $color-dark-surface-container;
|
||||
color: white;
|
||||
padding: 16px 16px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: static;
|
||||
justify-content: end;
|
||||
width: fit-content;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
@@ -24,8 +27,8 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
#logouts {
|
||||
display: none;
|
||||
list-style: none;
|
||||
gap: 10px;
|
||||
|
||||
@@ -53,13 +56,14 @@
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 70px);
|
||||
margin-top: 70px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.chat-header {
|
||||
padding: 16px;
|
||||
@@ -71,20 +75,29 @@
|
||||
.chat-header-avatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-radius: 50%;
|
||||
border-radius: 20%;
|
||||
object-fit: cover;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.chat-header-info {
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
display: flex;
|
||||
|
||||
p {
|
||||
font-size: 0.8rem;
|
||||
color: #718096;
|
||||
.info-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h4 {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
color: #718096;
|
||||
}
|
||||
}
|
||||
|
||||
.online-status {
|
||||
@@ -95,6 +108,23 @@
|
||||
background-color: $success;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,2 @@
|
||||
$primary: #6C63FF;
|
||||
$primary-dark: #564FD9;
|
||||
$secondary: #FF6584;
|
||||
$dark: #2D3748;
|
||||
$light: #F7FAFC;
|
||||
$gray: #E2E8F0;
|
||||
$success: #48BB78;
|
||||
$danger: #F56565;
|
||||
@@ -1,62 +1,4 @@
|
||||
@use "colors" 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%;
|
||||
}
|
||||
@use "material" as *;
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
@@ -83,7 +25,7 @@
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $primary;
|
||||
color: $color-dark-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-high: rgb(38 43 46);
|
||||
$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
|
||||
$color-light-primary: rgb(31 101 134);
|
||||
$color-light-surface-tint: rgb(31 101 134);
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
@mixin iconsFontStyle() {
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
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 {
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-weight: normal;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@use "auth";
|
||||
@use "chat";
|
||||
@use "panelchat";
|
||||
@use "common/animations";
|
||||
@use "common/components";
|
||||
@use "common/colors" as *;
|
||||
@@ -8,9 +9,8 @@
|
||||
@use "lib/fonts/montserrat";
|
||||
@use "lib/fonts/material-symbols";
|
||||
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -19,4 +19,17 @@ body {
|
||||
background-color: $color-dark-surface;
|
||||
color: $color-dark-on-surface;
|
||||
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 |
@@ -0,0 +1,7 @@
|
||||
import { showLogin } from "./auth";
|
||||
import { PRODUCT_NAME } from "./config";
|
||||
|
||||
showLogin();
|
||||
|
||||
document.getElementById("productname")!.textContent = PRODUCT_NAME;
|
||||
document.title = PRODUCT_NAME;
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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 register = document.getElementById("register-link")!;
|
||||
const chat = document.getElementById("chat-link")!;
|
||||
const logoutLink = document.getElementById("logout-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);
|
||||
// chat.addEventListener("click", loadChat);
|
||||
// logoutLink.addEventListener("click", logout);
|
||||
|
||||
+6
-144
@@ -1,146 +1,8 @@
|
||||
import './css/style.scss';
|
||||
import { showLogin, getAuthHeaders, authToken, currentUser } from './auth';
|
||||
import { API_BASE_URL, API_FULL_BASE_URL, PRODUCT_NAME } from './config';
|
||||
import type { Message, Messages, WebSocketMessage } from './types';
|
||||
import "mdui/mdui.css";
|
||||
|
||||
import "./links";
|
||||
|
||||
const websocket = new WebSocket(`ws://${API_FULL_BASE_URL}/chat/ws`);
|
||||
|
||||
|
||||
// Функция для форматирования времени
|
||||
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;
|
||||
import "./material";
|
||||
import "./chat";
|
||||
import "./leftpanel";
|
||||
import "./init";
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -7,24 +7,25 @@
|
||||
"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:reinstall": "rm -rf .venv && npm run backend:dependencies",
|
||||
|
||||
"frontend:dev": "vite frontend",
|
||||
"frontend:build": "tsc --project frontend && vite build frontend",
|
||||
"frontend:preview": "vite preview frontend",
|
||||
"frontend:dependencies": "npm install",
|
||||
|
||||
"dev": "concurrently 'npm run frontend:dev' 'npm run backend:run'",
|
||||
"build": "npm run frontend:build",
|
||||
"install": "npm run backend:dependencies"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.2.0",
|
||||
"sass-embedded": "^1.90.0",
|
||||
"postcss": "^8.5.6",
|
||||
"sass-embedded": "^1.90.0",
|
||||
"terser": "^5.43.1",
|
||||
"typescript": "~5.9.2",
|
||||
"vite": "^7.1.2",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"terser": "^5.43.1",
|
||||
"autoprefixer": "^10.4.21"
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mdui": "^2.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user