- {alerts.slice(-3).map((alert, i) => {
- return
{alert.message}
- })}
+
+
+ {displayAlerts.map((alert, i) => (
+
+ {alert.message}
+
+ ))}
+
)
}
\ No newline at end of file
diff --git a/frontend/src/pages/auth/AuthPage.tsx b/frontend/src/pages/auth/AuthPage.tsx
new file mode 100644
index 0000000..ae5f888
--- /dev/null
+++ b/frontend/src/pages/auth/AuthPage.tsx
@@ -0,0 +1,164 @@
+import { AuthContainer } from "./Auth";
+import { useState, useEffect, useRef, useLayoutEffect, useCallback, type RefObject } from "react";
+import { useNavigate, useSearchParams } from "react-router-dom";
+import { motion, AnimatePresence } from "motion/react";
+import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
+import { LoginForm } from "./LoginForm";
+import { RegisterForm } from "./RegisterForm";
+import type { Variants, Transition } from "motion/react";
+import styles from "./auth.module.scss";
+
+const slideVariants: Variants = {
+ enter: (direction: number) => ({
+ x: direction > 0 ? 300 : -300,
+ opacity: 0
+ }),
+ center: {
+ x: 0,
+ opacity: 1
+ },
+ exit: (direction: number) => ({
+ x: direction > 0 ? -300 : 300,
+ opacity: 0
+ })
+};
+
+const slideTransition: Transition = {
+ x: {
+ type: "spring",
+ stiffness: 300,
+ damping: 30
+ },
+ opacity: { duration: 0.2 }
+};
+
+
+
+export default function AuthPage() {
+ const [searchParams] = useSearchParams();
+ const { navigate: navigateDownloadApp } = useDownloadAppScreen();
+ if (navigateDownloadApp) return navigateDownloadApp;
+ const navigate = useNavigate();
+
+ const [direction, setDirection] = useState(0);
+ const prevMode = useRef(searchParams.get("mode") || "login");
+ const containerRef = useRef
(null);
+ const loginFormRef = useRef(null);
+ const registerFormRef = useRef(null);
+ const [containerHeight, setContainerHeight] = useState("auto");
+ const currentMode = searchParams.get("mode") || "login";
+ const enteringElementRef = useRef<"login" | "register" | null>(null);
+
+ useEffect(() => {
+ if (prevMode.current !== currentMode) {
+ setDirection(currentMode === "register" ? 1 : -1);
+ prevMode.current = currentMode;
+ enteringElementRef.current = currentMode as "login" | "register";
+ }
+ }, [currentMode]);
+
+ const measureActiveHeight = useCallback(() => {
+ const activeComponent = currentMode === "login" ? loginFormRef.current : registerFormRef.current;
+ if (activeComponent) {
+ const height = activeComponent.scrollHeight;
+ if (height > 0) {
+ setContainerHeight(height);
+ }
+ }
+ }, [currentMode, loginFormRef, registerFormRef]);
+
+ useLayoutEffect(() => {
+ // Always measure, but prioritize the entering element during transitions
+ // Use double requestAnimationFrame to ensure DOM is fully updated and layout is complete
+ let rafId2: number | null = null;
+ const rafId1 = requestAnimationFrame(() => {
+ rafId2 = requestAnimationFrame(() => {
+ measureActiveHeight();
+ });
+ });
+
+ return () => {
+ cancelAnimationFrame(rafId1);
+ if (rafId2 !== null) {
+ cancelAnimationFrame(rafId2);
+ }
+ };
+ }, [currentMode]);
+
+ function switchMode(newMode: "login" | "register") {
+ navigate(`/auth?mode=${newMode}`, { replace: true });
+ }
+
+ function handleAnimationComplete(
+ currentMode: "login" | "register",
+ mode: "login" | "register",
+ enteringElementRef: RefObject<"login" | "register" | null>,
+ formRef: React.RefObject,
+ setContainerHeight: (height: number) => void
+ ) {
+ return () => {
+ if (currentMode === mode && enteringElementRef.current === mode) {
+ enteringElementRef.current = null;
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ if (formRef.current && currentMode === mode) {
+ const height = formRef.current.scrollHeight;
+ if (height > 0) {
+ setContainerHeight(height);
+ }
+ }
+ });
+ });
+ }
+ }
+ }
+
+ return (
+
+
+
+ {currentMode === "login" ? (
+
+ switchMode("register")} />
+
+ ) : (
+
+ switchMode("login")} />
+
+ )}
+
+
+
+ )
+}
diff --git a/frontend/src/pages/auth/AuthTextField.tsx b/frontend/src/pages/auth/AuthTextField.tsx
new file mode 100644
index 0000000..6b6f8fe
--- /dev/null
+++ b/frontend/src/pages/auth/AuthTextField.tsx
@@ -0,0 +1,138 @@
+import { forwardRef, useImperativeHandle, useRef, useState, useEffect } from "react";
+import { motion } from "motion/react";
+import styles from "./auth.module.scss";
+
+export interface AuthTextFieldHandle {
+ value: string;
+ focus: () => void;
+ blur: () => void;
+}
+
+export interface AuthTextFieldProps {
+ label: string;
+ name?: string;
+ type?: string;
+ icon?: string;
+ autocomplete?: string;
+ required?: boolean;
+ maxlength?: number;
+ counter?: boolean;
+ "toggle-password"?: boolean;
+ defaultValue?: string;
+ value?: string;
+ onChange?: (value: string) => void;
+ className?: string;
+}
+
+export const AuthTextField = forwardRef(
+ ({
+ label,
+ name,
+ type = "text",
+ icon,
+ autocomplete,
+ required = false,
+ maxlength,
+ counter = false,
+ "toggle-password": togglePassword = false,
+ defaultValue = "",
+ value: controlledValue,
+ onChange,
+ className = ""
+ }, ref) => {
+ const [internalValue, setInternalValue] = useState(defaultValue);
+ const [isFocused, setIsFocused] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [charCount, setCharCount] = useState(0);
+ const inputRef = useRef(null);
+
+ const isControlled = controlledValue !== undefined;
+ const value = isControlled ? controlledValue : internalValue;
+ const displayType = togglePassword && type === "password" ? (showPassword ? "text" : "password") : type;
+
+ useEffect(() => {
+ if (!isControlled) {
+ setInternalValue(defaultValue);
+ }
+ }, [defaultValue, isControlled]);
+
+ useEffect(() => {
+ setCharCount(value.length);
+ }, [value]);
+
+ useImperativeHandle(ref, () => ({
+ get value() {
+ return value;
+ },
+ focus: () => {
+ inputRef.current?.focus();
+ },
+ blur: () => {
+ inputRef.current?.blur();
+ }
+ }));
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ if (!isControlled) {
+ setInternalValue(newValue);
+ }
+ onChange?.(newValue);
+ };
+
+ const hasError = false; // Can be extended for validation
+
+ return (
+
+
+ {icon && (
+
+ {icon.replace("--filled", "").replace("--outlined", "")}
+
+ )}
+
+ setIsFocused(true)}
+ onBlur={() => setIsFocused(false)}
+ autoComplete={autocomplete}
+ required={required}
+ maxLength={maxlength}
+ placeholder={label + (required ? " *" : "")}
+ className={styles.input}
+ />
+
+ {togglePassword && type === "password" && (
+
+ )}
+
+ {counter && maxlength && (
+
+ {charCount} / {maxlength}
+
+ )}
+
+ );
+ }
+);
+
+AuthTextField.displayName = "AuthTextField";
diff --git a/frontend/src/pages/auth/LoginForm.tsx b/frontend/src/pages/auth/LoginForm.tsx
new file mode 100644
index 0000000..be6b091
--- /dev/null
+++ b/frontend/src/pages/auth/LoginForm.tsx
@@ -0,0 +1,220 @@
+import { useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { motion, type Transition, type Variants } from "motion/react";
+import { useImmer } from "use-immer";
+import type { ErrorResponse, LoginRequest, LoginResponse } from "@/core/types";
+import { API_BASE_URL } from "@/core/config";
+import { useAppState } from "@/pages/chat/state";
+import { MaterialButton } from "@/utils/material";
+import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
+import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
+import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
+import { isElectron } from "@/core/electron/electron";
+import type { Alert, AlertType } from "./Auth";
+import { AuthHeader, AlertsContainer } from "./Auth";
+import styles from "./auth.module.scss";
+
+const loginFieldVariants: Variants = {
+ initial: {
+ opacity: 0,
+ y: 10
+ },
+ animate: {
+ opacity: 1,
+ y: 0
+ }
+};
+
+const loginFieldTransition: Transition = {
+ duration: 0.3,
+ ease: "easeInOut"
+};
+
+const loginButtonVariants: Variants = {
+ initial: {
+ opacity: 0,
+ y: 10
+ },
+ animate: {
+ opacity: 1,
+ y: 0
+ }
+};
+
+const loginButtonTransition: Transition = {
+ duration: 0.3,
+ delay: 0.4,
+ ease: "easeInOut"
+};
+
+interface LoginFormProps {
+ onSwitchMode: () => void;
+}
+
+export function LoginForm({ onSwitchMode }: LoginFormProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [alerts, updateAlerts] = useImmer([]);
+ const setUser = useAppState(state => state.setUser);
+ const navigate = useNavigate();
+
+ function showAlert(type: AlertType, message: string) {
+ updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
+ }
+
+ const usernameElement = useRef(null);
+ const passwordElement = useRef(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+
+ if (isLoading) return;
+
+ const username = usernameElement.current!.value.trim();
+ const password = passwordElement.current!.value.trim();
+
+ if (!username || !password) {
+ showAlert("danger", "Пожалуйста, заполните все поля");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const derived = await deriveAuthSecret(username, password);
+ const request: LoginRequest = {
+ username: username,
+ password: derived
+ }
+
+ const response = await fetch(`${API_BASE_URL}/login`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(request)
+ });
+
+ if (response.ok) {
+ const data: LoginResponse = await response.json();
+ setUser(data.token, data.user);
+
+ try {
+ await ensureKeysOnLogin(password, data.token);
+ } catch (e) {
+ console.error("Key setup failed:", e);
+ }
+
+ navigate("/chat");
+
+ try {
+ if (isSupported()) {
+ const initialized = await initialize();
+ if (initialized) {
+ await subscribe(data.token);
+
+ if (isElectron) {
+ await startElectronReceiver();
+ }
+
+ console.log("Notifications enabled");
+ } else {
+ console.log("Notification permission denied");
+ }
+ } else {
+ console.log("Notifications not supported");
+ }
+ } catch (e) {
+ console.error("Notification setup failed:", e);
+ }
+ } else {
+ const data: ErrorResponse = await response.json();
+
+ if (response.status === 403 && response.headers.get("suspension_reason")) {
+ const suspensionReason = response.headers.get("suspension_reason");
+ const setSuspended = useAppState.getState().setSuspended;
+ setSuspended(suspensionReason || "No reason provided");
+ return;
+ }
+
+ showAlert("danger", data.message || "Неверное имя пользователя или пароль");
+ }
+ } catch (error) {
+ showAlert("danger", "Ошибка соединения с сервером");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx
deleted file mode 100644
index 15e8c60..0000000
--- a/frontend/src/pages/auth/LoginPage.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { useImmer } from "use-immer";
-import { AlertsContainer, type Alert, type AlertType } from "./Auth";
-import { AuthContainer, AuthHeader } from "./Auth";
-import type { ErrorResponse, LoginRequest, LoginResponse } from "@/core/types";
-import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
-import { API_BASE_URL } from "@/core/config";
-import { useRef } from "react";
-import type { TextField } from "mdui/components/text-field";
-import { useAppState } from "@/pages/chat/state";
-import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
-import { isElectron } from "@/core/electron/electron";
-import { useNavigate } from "react-router-dom";
-import styles from "./auth.module.scss";
-import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
-import { MaterialButton, MaterialTextField } from "@/utils/material";
-
-export default function LoginPage() {
- const [alerts, updateAlerts] = useImmer([]);
- const setUser = useAppState(state => state.setUser);
- const navigate = useNavigate();
- const { navigate: navigateDownloadApp } = useDownloadAppScreen();
- if (navigateDownloadApp) return navigateDownloadApp;
-
- function showAlert(type: AlertType, message: string) {
- updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
- }
-
- const usernameElement = useRef(null);
- const passwordElement = useRef(null);
-
- return (
-
-
-
-
- )
-}
diff --git a/frontend/src/pages/auth/RegisterForm.tsx b/frontend/src/pages/auth/RegisterForm.tsx
new file mode 100644
index 0000000..b3e5c02
--- /dev/null
+++ b/frontend/src/pages/auth/RegisterForm.tsx
@@ -0,0 +1,249 @@
+import { useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { motion, type Transition, type Variants } from "motion/react";
+import { useImmer } from "use-immer";
+import type { ErrorResponse, RegisterRequest, LoginResponse } from "@/core/types";
+import { API_BASE_URL } from "@/core/config";
+import { useAppState } from "@/pages/chat/state";
+import { MaterialButton, MaterialIconButton } from "@/utils/material";
+import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
+import { AuthTextField, type AuthTextFieldHandle } from "./AuthTextField";
+import type { Alert, AlertType } from "./Auth";
+import { AuthHeader, AlertsContainer } from "./Auth";
+import styles from "./auth.module.scss";
+
+const registerFieldVariants: Variants = {
+ initial: {
+ opacity: 0,
+ y: 10
+ },
+ animate: {
+ opacity: 1,
+ y: 0
+ }
+};
+
+const registerFieldTransition: Transition = {
+ duration: 0.3,
+ ease: "easeInOut"
+};
+
+const registerButtonVariants: Variants = {
+ initial: {
+ opacity: 0,
+ y: 10
+ },
+ animate: {
+ opacity: 1,
+ y: 0
+ }
+};
+
+const registerButtonTransition: Transition = {
+ duration: 0.3,
+ delay: 0.6,
+ ease: "easeInOut"
+};
+
+interface RegisterFormProps {
+ onSwitchMode: () => void;
+}
+
+export function RegisterForm({ onSwitchMode }: RegisterFormProps) {
+ const [isLoading, setIsLoading] = useState(false);
+ const [alerts, updateAlerts] = useImmer([]);
+ const setUser = useAppState(state => state.setUser);
+ const navigate = useNavigate();
+
+ function showAlert(type: AlertType, message: string) {
+ updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
+ }
+
+ const displayNameElement = useRef(null);
+ const usernameElement = useRef(null);
+ const passwordElement = useRef(null);
+ const confirmPasswordElement = useRef(null);
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+
+ if (isLoading) return;
+
+ const displayName = displayNameElement.current!.value.trim();
+ const username = usernameElement.current!.value.trim();
+ const password = passwordElement.current!.value.trim();
+ const confirmPassword = confirmPasswordElement.current!.value.trim();
+
+ if (!displayName || !username || !password || !confirmPassword) {
+ showAlert("danger", "Пожалуйста, заполните все поля");
+ return;
+ }
+
+ if (password !== confirmPassword) {
+ showAlert("danger", "Пароли не совпадают");
+ return;
+ }
+
+ if (displayName.length < 1 || displayName.length > 64) {
+ showAlert("danger", "Отображаемое имя должно быть от 1 до 64 символов");
+ return;
+ }
+
+ if (username.length < 3 || username.length > 20) {
+ showAlert("danger", "Имя пользователя должно быть от 3 до 20 символов");
+ return;
+ }
+
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
+ showAlert("danger", "Имя пользователя может содержать только английские буквы, цифры, дефисы и подчеркивания");
+ return;
+ }
+
+ if (password.length < 5 || password.length > 50) {
+ showAlert("danger", "Пароль должен быть от 5 до 50 символов");
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ const derived = await deriveAuthSecret(username, password);
+ const request: RegisterRequest = {
+ display_name: displayName,
+ username: username,
+ password: derived,
+ confirm_password: derived
+ }
+
+ const response = await fetch(`${API_BASE_URL}/register`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(request)
+ });
+
+ if (response.ok) {
+ const data: LoginResponse = await response.json();
+ setUser(data.token, data.user);
+
+ try {
+ await ensureKeysOnLogin(password, data.token);
+ } catch (e) {
+ console.error("Key setup failed:", e);
+ }
+
+ navigate("/chat");
+ } else {
+ const data: ErrorResponse = await response.json();
+ showAlert("danger", data.message || "Ошибка при регистрации");
+ }
+ } catch (error) {
+ showAlert("danger", "Ошибка соединения с сервером");
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading ? "Регистрация..." : "Зарегистрироваться"}
+
+
+
+
+
+
+ >
+ );
+}
+
diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx
deleted file mode 100644
index 991ec68..0000000
--- a/frontend/src/pages/auth/RegisterPage.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import { useImmer } from "use-immer";
-import { AuthContainer, AuthHeader } from "./Auth";
-import { AlertsContainer, type Alert, type AlertType } from "./Auth";
-import { useRef } from "react";
-import { TextField } from "mdui/components/text-field";
-import type { ErrorResponse, RegisterRequest, LoginResponse } from "@/core/types";
-import { API_BASE_URL } from "@/core/config";
-import { useAppState } from "@/pages/chat/state";
-import { MaterialButton, MaterialTextField } from "@/utils/material";
-import { ensureKeysOnLogin, deriveAuthSecret } from "@/core/api/authApi";
-import { useNavigate } from "react-router-dom";
-import styles from "./auth.module.scss";
-import useDownloadAppScreen from "@/core/hooks/useDownloadAppScreen";
-
-export default function RegisterPage() {
- const [alerts, updateAlerts] = useImmer([]);
- const setUser = useAppState(state => state.setUser);
- const navigate = useNavigate();
- const { navigate: navigateDownloadApp } = useDownloadAppScreen();
- if (navigateDownloadApp) return navigateDownloadApp;
-
- function showAlert(type: AlertType, message: string) {
- updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
- }
-
- const displayNameElement = useRef(null);
- const usernameElement = useRef(null);
- const passwordElement = useRef(null);
- const confirmPasswordElement = useRef(null);
-
- return (
-
-
-
-
- )
-}
diff --git a/frontend/src/pages/auth/auth.module.scss b/frontend/src/pages/auth/auth.module.scss
index ca1560f..846bdbb 100644
--- a/frontend/src/pages/auth/auth.module.scss
+++ b/frontend/src/pages/auth/auth.module.scss
@@ -1,52 +1,321 @@
+@use "sass:color";
@use "../../css/colors" as *;
@use "../../css/material" as *;
+@keyframes rotateGradient {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+@keyframes slideInDown {
+ from {
+ opacity: 0;
+ transform: translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes shake {
+ 0%, 100% {
+ transform: translateX(0);
+ }
+ 10%, 30%, 50%, 70%, 90% {
+ transform: translateX(-4px);
+ }
+ 20%, 40%, 60%, 80% {
+ transform: translateX(4px);
+ }
+}
+
.authContainer {
display: flex;
justify-content: center;
align-items: center;
- height: 100%;
+ min-height: 100vh;
+ width: 100vw;
padding: 2rem;
- background-color: $color-dark-surface;
+ position: fixed;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+ background: $color-dark-surface;
+ .gradientBackground {
+ $size: 550px;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: $size;
+ height: $size;
+ background: conic-gradient(
+ from 0deg,
+ rgba(147, 51, 234, 0.5) 0%,
+ rgba(99, 102, 241, 0.6) 12.5%,
+ rgba(59, 130, 246, 0.55) 25%,
+ rgba(168, 85, 247, 0.5) 37.5%,
+ rgba(217, 70, 239, 0.6) 50%,
+ rgba(236, 72, 153, 0.55) 62.5%,
+ rgba(192, 132, 252, 0.5) 75%,
+ rgba(126, 34, 206, 0.6) 87.5%,
+ rgba(147, 51, 234, 0.5) 100%
+ );
+ animation: rotateGradient 8s linear infinite;
+ border-radius: 50%;
+ filter: blur(80px);
+ z-index: 0;
+ will-change: transform;
+ backface-visibility: hidden;
+ }
+
.authCard {
- background-color: $color-dark-surface-container;
+ background: rgba($color-dark-surface-container, 0.7);
+ backdrop-filter: blur(20px);
color: $color-dark-on-surface;
- border-radius: 12px;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
+ border-radius: 24px;
+ border: 1px solid rgba($color-dark-outline, 0.1);
+ box-shadow:
+ 0 20px 60px rgba(0, 0, 0, 0.3),
+ 0 0 0 1px rgba($color-dark-primary, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
width: 100%;
max-width: 450px;
overflow: hidden;
- animation: authCardAnimation 0.3s ease-in-out;
- }
+ position: relative;
+ z-index: 1;
- .authHeader {
- margin: 0;
- padding: 16px;
- padding-bottom: 0;
- text-align: center;
+ .formWrapper {
+ position: absolute;
+ width: 100%;
+ top: 0;
+ left: 0;
- h2 {
- font-size: 1.8rem;
- margin: 0;
- margin-bottom: 0.5rem;
- align-items: center;
- display: flex;
- flex-direction: row;
- gap: 10px;
- justify-content: center;
- }
- }
-
- .authBody {
- padding: 25px;
- padding-bottom: 16px;
-
- form {
- display: flex;
- flex-direction: column;
- gap: 10px;
+ .authHeader {
+ margin: 0;
+ padding: 24px;
+ padding-bottom: 8px;
+ text-align: center;
+
+ h2 {
+ font-size: 1.8rem;
+ margin: 0;
+ margin-bottom: 0.5rem;
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ gap: 10px;
+ justify-content: center;
+ font-weight: 600;
+
+ .material-symbols {
+ color: $color-dark-primary;
+ filter: drop-shadow(0 0 8px rgba($color-dark-primary, 0.4));
+ }
+ }
+
+ p {
+ color: $color-dark-on-surface-variant;
+ font-size: 0.95rem;
+ margin: 0;
+ }
+ }
+
+ .authBody {
+ padding: 24px;
+ padding-bottom: 20px;
+
+ form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .authButtons {
+ display: flex;
+ flex-direction: row;
+ gap: 16px;
+ }
+ }
+
+ .registerLink {
+ text-align: center;
+ margin-top: 16px;
+ font-size: 0.9rem;
+ color: $color-dark-on-surface-variant;
+
+ a {
+ color: $color-dark-primary;
+ margin-inline-start: 3px;
+ }
+ }
+ }
}
}
}
+// AuthTextField Styles
+.authTextField {
+ position: relative;
+ width: 100%;
+
+ .fieldContainer {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ background: rgba($color-dark-surface-variant, 0.3);
+ border: 1px solid rgba($color-dark-outline, 0.2);
+ border-radius: 12px;
+ padding: 0 0 0 12px;
+ transition: all 0.3s ease;
+ min-height: 44px;
+
+ &:hover {
+ border-color: rgba($color-dark-outline, 0.4);
+ background: rgba($color-dark-surface-variant, 0.4);
+ }
+
+ &.focused {
+ border-color: $color-dark-primary;
+ background: rgba($color-dark-surface-variant, 0.5);
+ box-shadow:
+ 0 0 0 4px rgba($color-dark-primary, 0.1),
+ 0 4px 12px rgba($color-dark-primary, 0.2);
+ }
+
+ &.error {
+ border-color: $color-dark-error;
+ animation: shake 0.4s ease;
+
+ &.focused {
+ box-shadow:
+ 0 0 0 4px rgba($color-dark-error, 0.1),
+ 0 4px 12px rgba($color-dark-error, 0.2);
+ }
+ }
+
+ &.noIcon {
+ gap: 0;
+
+ .inputWrapper {
+ margin-left: 0;
+ }
+ }
+
+ &.hasToggle {
+ padding-right: 12px;
+ }
+ }
+
+ .fieldIcon {
+ color: $color-dark-on-surface-variant;
+ font-size: 18px;
+ flex-shrink: 0;
+ transition: color 0.3s ease;
+
+ .fieldContainer.focused & {
+ color: $color-dark-primary;
+ }
+ }
+
+ .inputWrapper {
+ position: relative;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ min-height: 44px;
+ }
+
+ .input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ outline: none;
+ color: $color-dark-on-surface;
+ font-size: 0.95rem;
+ font-family: inherit;
+ padding: 12px 0 12px 0;
+ line-height: 1.4;
+ height: auto;
+ min-height: 20px;
+
+ &::placeholder {
+ color: $color-dark-on-surface-variant;
+ opacity: 0.7;
+ }
+
+ &:focus::placeholder {
+ opacity: 0.5;
+ }
+ }
+
+ .togglePassword {
+ background: none;
+ border: none;
+ color: $color-dark-on-surface-variant;
+ cursor: pointer;
+ padding: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ transition: all 0.2s ease;
+ flex-shrink: 0;
+
+ &:hover {
+ background: rgba($color-dark-on-surface, 0.1);
+ color: $color-dark-on-surface;
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+
+ .material-symbols {
+ font-size: 18px;
+ }
+ }
+
+ .counter {
+ margin-top: 4px;
+ padding-left: 16px;
+ font-size: 0.75rem;
+ color: $color-dark-on-surface-variant;
+ text-align: right;
+ }
+}
+
+// Alert Styles
+.alertContainer {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+
+ .alert {
+ padding: 12px 16px;
+ border-radius: 12px;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ animation: slideInDown 0.3s ease;
+
+ &.alert-success {
+ background: rgba($color-dark-primary-container, 0.3);
+ color: $color-dark-on-primary-container;
+ border: 1px solid rgba($color-dark-primary, 0.3);
+ }
+
+ &.alert-danger {
+ background: rgba($color-dark-error-container, 0.3);
+ color: $color-dark-on-error-container;
+ border: 1px solid rgba($color-dark-error, 0.3);
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/pages/chat/css/ChatInput.module.scss b/frontend/src/pages/chat/css/ChatInput.module.scss
index 40a5108..cdd1f52 100644
--- a/frontend/src/pages/chat/css/ChatInput.module.scss
+++ b/frontend/src/pages/chat/css/ChatInput.module.scss
@@ -116,7 +116,7 @@
width: 50px;
height: 50px;
border-radius: 50%;
- background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%));
+ background-color: $color-dark-primary;
color: $color-dark-on-primary;
border: 1px solid rgba($color-dark-primary, 0.5);
cursor: pointer;
diff --git a/frontend/src/pages/chat/css/Message.module.scss b/frontend/src/pages/chat/css/Message.module.scss
index 6b61916..f1cfe50 100644
--- a/frontend/src/pages/chat/css/Message.module.scss
+++ b/frontend/src/pages/chat/css/Message.module.scss
@@ -210,7 +210,7 @@
left: 0;
right: 0;
bottom: 0;
- background: linear-gradient(135deg, rgba($color-dark-primary, 0.05), rgba($color-dark-tertiary, 0.03));
+ background: linear-gradient(135deg, rgba(147, 51, 234, 0.05), rgba(99, 102, 241, 0.03));
pointer-events: none;
z-index: 0;
}
@@ -232,7 +232,7 @@
flex-direction: row-reverse;
.messageInner {
- background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%));
+ background: linear-gradient(135deg, #9333EA, #6366F1, #3B82F6);
color: $color-dark-on-primary;
border-top-right-radius: 5px;
box-shadow: 0 0 20px rgba($color-dark-primary, 0.4);
diff --git a/frontend/src/pages/chat/css/callWindow.module.scss b/frontend/src/pages/chat/css/callWindow.module.scss
index 473058f..85602a1 100644
--- a/frontend/src/pages/chat/css/callWindow.module.scss
+++ b/frontend/src/pages/chat/css/callWindow.module.scss
@@ -29,7 +29,6 @@
height: 100vh;
background-color: rgba($color-dark-surface, 0.98);
backdrop-filter: blur(40px);
- -webkit-backdrop-filter: blur(40px);
border: none;
border-radius: 0;
cursor: default;
@@ -64,7 +63,6 @@
height: 300px;
background-color: rgba($color-dark-surface, 0.95);
backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
border: 2px solid rgba($color-dark-outline, 0.4);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
@@ -415,7 +413,6 @@
font-weight: 600;
border-radius: 8px;
backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
}
&.localVideo {
@@ -455,7 +452,6 @@
color: $color-dark-on-primary;
border-radius: 8px;
backdrop-filter: blur(10px);
- -webkit-backdrop-filter: blur(10px);
pointer-events: none;
z-index: 1;
}
diff --git a/frontend/src/pages/chat/css/layout.module.scss b/frontend/src/pages/chat/css/layout.module.scss
index b07ecfb..165e9b3 100644
--- a/frontend/src/pages/chat/css/layout.module.scss
+++ b/frontend/src/pages/chat/css/layout.module.scss
@@ -4,13 +4,14 @@
.chatInterface {
height: 100%;
- background: linear-gradient(135deg, $color-dark-background 0%, $color-dark-surface-container 70%, rgba($color-dark-primary-container, 0.3) 100%);
+ background: $color-dark-background;
+ // background: linear-gradient(135deg, $color-dark-background 0%, $color-dark-surface-container 70%, rgba($color-dark-primary-container, 0.3) 100%);
position: relative;
overflow: hidden;
&::before {
content: '';
- position: fixed;
+ position: absolute;
top: 0;
left: 0;
right: 0;
@@ -20,7 +21,7 @@
radial-gradient(circle at 80% 20%, rgba($color-dark-tertiary, 0.15) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba($color-dark-secondary, 0.1) 0%, transparent 50%);
pointer-events: none;
- z-index: 0;
+ z-index: 10;
}
.allContainer {
diff --git a/frontend/src/pages/chat/css/left-panel.module.scss b/frontend/src/pages/chat/css/left-panel.module.scss
index d0e998d..098c72e 100644
--- a/frontend/src/pages/chat/css/left-panel.module.scss
+++ b/frontend/src/pages/chat/css/left-panel.module.scss
@@ -30,11 +30,11 @@
flex-grow: 1;
font-size: 1.8rem;
font-weight: 700;
- background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary);
- -webkit-background-clip: text;
+ background: linear-gradient(45deg, #9333EA, #6366F1, #3B82F6, #A855F7, #D946EF, #EC4899, #C084FC, #7E22CE);
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
- text-shadow: 0 0 20px rgba($color-dark-primary, 0.5);
+ text-shadow: 0 0 20px rgba(147, 51, 234, 0.5);
}
.profile {
diff --git a/frontend/src/pages/chat/css/profile-dialog.module.scss b/frontend/src/pages/chat/css/profile-dialog.module.scss
index e10b4c9..3a1d9a8 100644
--- a/frontend/src/pages/chat/css/profile-dialog.module.scss
+++ b/frontend/src/pages/chat/css/profile-dialog.module.scss
@@ -53,6 +53,9 @@
.usernameWithBadge {
gap: 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
.usernameInput {
background: none;
diff --git a/frontend/src/pages/home/home.module.scss b/frontend/src/pages/home/home.module.scss
index 40327db..7284c6a 100644
--- a/frontend/src/pages/home/home.module.scss
+++ b/frontend/src/pages/home/home.module.scss
@@ -58,7 +58,7 @@
font-weight: 700;
margin: 0;
background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary);
- -webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba($color-dark-primary, 0.5);
@@ -99,7 +99,7 @@
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary, $color-dark-secondary);
- -webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba($color-dark-primary, 0.5);
@@ -246,7 +246,7 @@
font-weight: 700;
margin-bottom: 3rem;
background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary);
- -webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba($color-dark-primary, 0.5);
@@ -348,7 +348,7 @@
font-weight: 700;
margin-bottom: 1.5rem;
background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary);
- -webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba($color-dark-primary, 0.5);
@@ -387,7 +387,7 @@
font-weight: 700;
margin-bottom: 1.5rem;
background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary);
- -webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 20px rgba($color-dark-primary, 0.5);
diff --git a/frontend/src/pages/not-found/not-found.module.scss b/frontend/src/pages/not-found/not-found.module.scss
index 82cefbb..69a33c9 100644
--- a/frontend/src/pages/not-found/not-found.module.scss
+++ b/frontend/src/pages/not-found/not-found.module.scss
@@ -3,7 +3,7 @@
align-items: center;
justify-content: center;
min-height: 100vh;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ background: linear-gradient(135deg, #9333EA 0%, #6366F1 100%);
padding: 2rem;
}
@@ -42,7 +42,7 @@
.errorCode {
font-size: 6rem;
font-weight: 900;
- color: #667eea;
+ color: #9333EA;
line-height: 1;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
@@ -58,7 +58,7 @@
display: flex;
align-items: center;
justify-content: center;
- color: #667eea;
+ color: #9333EA;
opacity: 0.7;
}
diff --git a/frontend/src/utils/material.tsx b/frontend/src/utils/material.tsx
index bd698fc..31203d8 100644
--- a/frontend/src/utils/material.tsx
+++ b/frontend/src/utils/material.tsx
@@ -40,7 +40,7 @@ import type { Badge } from 'mdui/components/badge';
import type { CircularProgress } from 'mdui/components/circular-progress';
import type { BottomAppBar } from 'mdui/components/bottom-app-bar';
-setColorScheme("#91cef4");
+setColorScheme("#9333EA");
type BasePropCustomization = Override, {
ref?: Ref;