diff --git a/config.json b/config.json index 8887c1c2..8d6379df 100644 --- a/config.json +++ b/config.json @@ -1,28 +1,13 @@ { - "defaultHomeserver": 1, - "homeserverList": ["converser.eu", "matrix.org", "mozilla.org", "unredacted.org", "xmr.se"], + "defaultHomeserver": 0, + "homeserverList": ["vojo.chat"], "allowCustomHomeservers": true, "featuredCommunities": { "openAsDefault": false, - "spaces": [ - "#cinny-space:matrix.org", - "#community:matrix.org", - "#space:unredacted.org", - "#science-space:matrix.org", - "#libregaming-games:tchncs.de", - "#mathematics-on:matrix.org", - "#stickers-and-emojis:tastytea.de" - ], - "rooms": [ - "#cinny:matrix.org", - "#freesoftware:matrix.org", - "#pcapdroid:matrix.org", - "#gentoo:matrix.org", - "#PrivSec.dev:arcticfoxes.net", - "#disroot:aria-net.org" - ], - "servers": ["matrixrooms.info", "matrix.org", "mozilla.org", "unredacted.org"] + "spaces": [], + "rooms": [], + "servers": ["vojo.chat"] }, "hashRouter": { diff --git a/public/locales/de.json b/public/locales/de.json index 43a37160..1bf43f7d 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -3,5 +3,53 @@ "RoomCommon": { "changed_room_name": " hat den Raum Name geändert" } + }, + "Auth": { + "title_login": "Anmelden", + "title_register": "Registrieren", + "title_reset_password": "Passwort zurücksetzen", + "homeserver": "Homeserver", + "homeserver_edit": "Bearbeiten", + "homeserver_dialog_title": "Homeserver", + "homeserver_dialog_desc": "Geben Sie die Adresse des Matrix-Homeservers ein, mit dem Sie sich verbinden möchten.", + "homeserver_dialog_placeholder": "example.com", + "homeserver_dialog_cancel": "Abbrechen", + "homeserver_dialog_confirm": "Weiter", + "username_placeholder": "Benutzername", + "password_placeholder": "Passwort", + "forgot_password": "Passwort vergessen?", + "login_button": "Anmelden", + "new_here": "Neu hier?", + "create_account": "Konto erstellen", + "already_have_account": "Bereits ein Konto?", + "remember_password": "Passwort wieder eingefallen?", + "loading_server": "Server wird gesucht...", + "loading_connecting": "Verbindung zu {{url}}...", + "loading_auth_flows": "Laden...", + "error_server_not_found": "Server konnte nicht gefunden werden.", + "error_server_config_invalid": "Verbindung fehlgeschlagen. Die Konfiguration des Servers {{host}} ist ungültig.", + "error_server_base_url_invalid": "Verbindung fehlgeschlagen. Ungültige base_url des Servers.", + "error_server_unavailable": "Verbindung zum Server konnte nicht hergestellt werden.", + "error_auth_flows": "Autorisierungsabläufe konnten nicht geladen werden.", + "error_client_unsupported": "Dieser Client unterstützt keine Autorisierung auf dem Server \"{{server}}\".", + "error_custom_server_not_allowed": "Anmeldung mit benutzerdefiniertem Server ist nicht erlaubt.", + "error_matrix_id_server": "Matrix-ID-Server konnte nicht gefunden werden.", + "error_invalid_credentials": "Ungültiger Benutzername oder Passwort.", + "error_account_deactivated": "Dieses Konto wurde deaktiviert.", + "error_invalid_request": "Anmeldung fehlgeschlagen. Ein Teil der Anfragedaten ist ungültig.", + "error_rate_limited": "Zu viele Versuche. Bitte versuchen Sie es später erneut.", + "error_unknown": "Anmeldung fehlgeschlagen. Unbekannter Fehler.", + "register_disabled": "Die Registrierung ist auf diesem Server deaktiviert.", + "register_rate_limited": "Zu viele Versuche. Bitte versuchen Sie es später erneut.", + "register_invalid_request": "Ungültige Anfrage. Registrierungsparameter konnten nicht abgerufen werden.", + "register_unsupported": "Diese Anwendung unterstützt keine Registrierung auf diesem Server.", + "reset_description": "Der Homeserver {{server}} sendet Ihnen eine E-Mail, um Ihr Passwort zurückzusetzen.", + "reset_email_label": "E-Mail", + "reset_new_password": "Neues Passwort", + "reset_confirm_password": "Passwort bestätigen", + "reset_button": "Passwort zurücksetzen", + "reset_error_fallback": "Passwort konnte nicht zurückgesetzt werden.", + "reset_success_message": "Das Passwort wurde erfolgreich zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.", + "reset_success_login": "Anmelden" } } diff --git a/public/locales/en.json b/public/locales/en.json index 7a2534b8..771ecc9b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -3,5 +3,53 @@ "RoomCommon": { "changed_room_name": " changed room name" } + }, + "Auth": { + "title_login": "Log In", + "title_register": "Register", + "title_reset_password": "Reset Password", + "homeserver": "Homeserver", + "homeserver_edit": "Edit", + "homeserver_dialog_title": "Homeserver", + "homeserver_dialog_desc": "Enter the address of the Matrix homeserver you want to connect to.", + "homeserver_dialog_placeholder": "example.com", + "homeserver_dialog_cancel": "Cancel", + "homeserver_dialog_confirm": "Continue", + "username_placeholder": "Username", + "password_placeholder": "Password", + "forgot_password": "Forgot password?", + "login_button": "Log In", + "new_here": "New here?", + "create_account": "Create account", + "already_have_account": "Already have an account?", + "remember_password": "Remember your password?", + "loading_server": "Looking for server...", + "loading_connecting": "Connecting to {{url}}...", + "loading_auth_flows": "Loading...", + "error_server_not_found": "Failed to find server.", + "error_server_config_invalid": "Failed to connect. Server configuration for {{host}} is invalid.", + "error_server_base_url_invalid": "Failed to connect. Invalid server base_url.", + "error_server_unavailable": "Failed to connect to the server.", + "error_auth_flows": "Failed to load authorization flows.", + "error_client_unsupported": "This client does not support authorization on server \"{{server}}\".", + "error_custom_server_not_allowed": "Login with a custom server is not allowed.", + "error_matrix_id_server": "Failed to find Matrix ID server.", + "error_invalid_credentials": "Invalid username or password.", + "error_account_deactivated": "This account has been deactivated.", + "error_invalid_request": "Failed to log in. Part of the request data is invalid.", + "error_rate_limited": "Too many attempts. Please try again later.", + "error_unknown": "Failed to log in. Unknown error.", + "register_disabled": "Registration is disabled on this server.", + "register_rate_limited": "Too many attempts. Please try again later.", + "register_invalid_request": "Invalid request. Failed to get registration parameters.", + "register_unsupported": "This application does not support registration on this server.", + "reset_description": "Homeserver {{server}} will send you an email to let you reset your password.", + "reset_email_label": "Email", + "reset_new_password": "New Password", + "reset_confirm_password": "Confirm Password", + "reset_button": "Reset Password", + "reset_error_fallback": "Failed to reset password.", + "reset_success_message": "Password has been reset successfully. Please login with your new password.", + "reset_success_login": "Login" } } diff --git a/public/locales/ru.json b/public/locales/ru.json new file mode 100644 index 00000000..d1abb4ff --- /dev/null +++ b/public/locales/ru.json @@ -0,0 +1,55 @@ +{ + "Organisms": { + "RoomCommon": { + "changed_room_name": " изменил(а) название комнаты" + } + }, + "Auth": { + "title_login": "Войти", + "title_register": "Регистрация", + "title_reset_password": "Сброс пароля", + "homeserver": "Домашний сервер", + "homeserver_edit": "Редактировать", + "homeserver_dialog_title": "Домашний сервер", + "homeserver_dialog_desc": "Укажите адрес домашнего сервера Matrix, к которому хотите подключиться.", + "homeserver_dialog_placeholder": "example.com", + "homeserver_dialog_cancel": "Отмена", + "homeserver_dialog_confirm": "Продолжить", + "username_placeholder": "Имя пользователя", + "password_placeholder": "Пароль", + "forgot_password": "Забыли пароль?", + "login_button": "Войти", + "new_here": "Впервые здесь?", + "create_account": "Создать учётную запись", + "already_have_account": "Уже есть аккаунт?", + "remember_password": "Вспомнили пароль?", + "loading_server": "Поиск сервера...", + "loading_connecting": "Подключение к {{url}}...", + "loading_auth_flows": "Загрузка...", + "error_server_not_found": "Не удалось найти сервер.", + "error_server_config_invalid": "Не удалось подключиться. Конфигурация сервера {{host}} некорректна.", + "error_server_base_url_invalid": "Не удалось подключиться. Неверный base_url сервера.", + "error_server_unavailable": "Не удалось подключиться к серверу.", + "error_auth_flows": "Не удалось загрузить потоки авторизации.", + "error_client_unsupported": "Этот клиент не поддерживает авторизацию на сервере \"{{server}}\".", + "error_custom_server_not_allowed": "Вход с пользовательским сервером не разрешён.", + "error_matrix_id_server": "Не удалось найти сервер Matrix ID.", + "error_invalid_credentials": "Неверное имя пользователя или пароль.", + "error_account_deactivated": "Эта учётная запись была деактивирована.", + "error_invalid_request": "Не удалось войти. Часть данных запроса некорректна.", + "error_rate_limited": "Слишком много попыток. Попробуйте позже.", + "error_unknown": "Не удалось войти. Неизвестная ошибка.", + "register_disabled": "Регистрация отключена на этом сервере.", + "register_rate_limited": "Слишком много попыток. Попробуйте позже.", + "register_invalid_request": "Неверный запрос. Не удалось получить параметры регистрации.", + "register_unsupported": "Это приложение не поддерживает регистрацию на данном сервере.", + "reset_description": "Сервер {{server}} отправит вам письмо для сброса пароля.", + "reset_email_label": "Email", + "reset_new_password": "Новый пароль", + "reset_confirm_password": "Подтвердите пароль", + "reset_button": "Сбросить пароль", + "reset_error_fallback": "Не удалось сбросить пароль.", + "reset_success_message": "Пароль успешно сброшен. Войдите с новым паролем.", + "reset_success_login": "Войти" + } +} diff --git a/public/res/img/mascot.png b/public/res/img/mascot.png new file mode 100644 index 00000000..4e5cf074 Binary files /dev/null and b/public/res/img/mascot.png differ diff --git a/public/res/img/mascot.webm b/public/res/img/mascot.webm new file mode 100644 index 00000000..46eebf1d Binary files /dev/null and b/public/res/img/mascot.webm differ diff --git a/src/app/hooks/useParsedLoginFlows.ts b/src/app/hooks/useParsedLoginFlows.ts index 088a514e..5fcf44b8 100644 --- a/src/app/hooks/useParsedLoginFlows.ts +++ b/src/app/hooks/useParsedLoginFlows.ts @@ -1,29 +1,16 @@ import { useMemo } from 'react'; -import { ILoginFlow, IPasswordFlow, ISSOFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth'; - -export const getSSOFlow = (loginFlows: LoginFlow[]): ISSOFlow | undefined => - loginFlows.find((flow) => flow.type === 'm.login.sso' || flow.type === 'm.login.cas') as - | ISSOFlow - | undefined; +import { IPasswordFlow, LoginFlow } from 'matrix-js-sdk/lib/@types/auth'; export const getPasswordFlow = (loginFlows: LoginFlow[]): IPasswordFlow | undefined => loginFlows.find((flow) => flow.type === 'm.login.password') as IPasswordFlow; -export const getTokenFlow = (loginFlows: LoginFlow[]): LoginFlow | undefined => - loginFlows.find((flow) => flow.type === 'm.login.token') as ILoginFlow & { - type: 'm.login.token'; - }; export type ParsedLoginFlows = { password?: LoginFlow; - token?: LoginFlow; - sso?: ISSOFlow; }; export const useParsedLoginFlows = (loginFlows: LoginFlow[]) => { const parsedFlow: ParsedLoginFlows = useMemo( () => ({ password: getPasswordFlow(loginFlows), - token: getTokenFlow(loginFlows), - sso: getSSOFlow(loginFlows), }), [loginFlows] ); diff --git a/src/app/i18n.ts b/src/app/i18n.ts index 9e83805d..0616c9a1 100644 --- a/src/app/i18n.ts +++ b/src/app/i18n.ts @@ -18,7 +18,7 @@ i18n // for all options read: https://www.i18next.com/overview/configuration-options .init({ debug: false, - fallbackLng: 'en', + fallbackLng: 'ru', interpolation: { escapeValue: false, // not needed for react as it escapes by default }, diff --git a/src/app/pages/auth/AuthFooter.tsx b/src/app/pages/auth/AuthFooter.tsx index 7e509d1e..29cb49b1 100644 --- a/src/app/pages/auth/AuthFooter.tsx +++ b/src/app/pages/auth/AuthFooter.tsx @@ -1,28 +1,18 @@ -import React from 'react'; -import { Box, Text } from 'folds'; +import React, { Ref } from 'react'; import * as css from './styles.css'; -export function AuthFooter() { +interface AuthFooterProps { + footerRef?: Ref; +} + +export function AuthFooter({ footerRef }: AuthFooterProps) { return ( - - - About - - - v4.11.1 - - - Twitter - - - Powered by Matrix - - +
+ Powered by Matrix + + Hosted on Yandex Cloud +
); } diff --git a/src/app/pages/auth/AuthLayout.tsx b/src/app/pages/auth/AuthLayout.tsx index 3943f42d..1cbe9cb9 100644 --- a/src/app/pages/auth/AuthLayout.tsx +++ b/src/app/pages/auth/AuthLayout.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect } from 'react'; -import { Box, Header, Scroll, Spinner, Text, color } from 'folds'; +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import { Box, Button, Input, Spinner, Text, color } from 'folds'; import { Outlet, generatePath, @@ -8,11 +8,10 @@ import { useNavigate, useParams, } from 'react-router-dom'; -import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; import { AuthFooter } from './AuthFooter'; import * as css from './styles.css'; -import * as PatternsCss from '../../styles/Patterns.css'; import { clientAllowedServer, clientDefaultServer, @@ -20,8 +19,6 @@ import { } from '../../hooks/useClientConfig'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { LOGIN_PATH, REGISTER_PATH, RESET_PASSWORD_PATH } from '../paths'; -import CinnySVG from '../../../../public/res/svg/cinny.svg'; -import { ServerPicker } from './ServerPicker'; import { AutoDiscoveryAction, autoDiscovery } from '../../cs-api'; import { SpecVersionsLoader } from '../../components/SpecVersionsLoader'; import { SpecVersionsProvider } from '../../hooks/useSpecVersions'; @@ -30,17 +27,13 @@ import { AuthFlowsLoader } from '../../components/AuthFlowsLoader'; import { AuthFlowsProvider } from '../../hooks/useAuthFlows'; import { AuthServerProvider } from '../../hooks/useAuthServer'; import { tryDecodeURIComponent } from '../../utils/dom'; +import mascotPoster from '../../../../public/res/img/mascot.png'; +import mascotWebm from '../../../../public/res/img/mascot.webm'; const currentAuthPath = (pathname: string): string => { - if (matchPath(LOGIN_PATH, pathname)) { - return LOGIN_PATH; - } - if (matchPath(RESET_PASSWORD_PATH, pathname)) { - return RESET_PASSWORD_PATH; - } - if (matchPath(REGISTER_PATH, pathname)) { - return REGISTER_PATH; - } + if (matchPath(LOGIN_PATH, pathname)) return LOGIN_PATH; + if (matchPath(RESET_PASSWORD_PATH, pathname)) return RESET_PASSWORD_PATH; + if (matchPath(REGISTER_PATH, pathname)) return REGISTER_PATH; return LOGIN_PATH; }; @@ -65,13 +58,128 @@ function AuthLayoutError({ message }: { message: string }) { ); } +/* ── Server edit dialog ── */ +function ServerEditDialog({ + currentServer, + onConfirm, + onCancel, +}: { + currentServer: string; + onConfirm: (server: string) => void; + onCancel: () => void; +}) { + const { t } = useTranslation(); + const [value, setValue] = useState(currentServer); + const inputRef = useRef(null); + + useEffect(() => { + inputRef.current?.focus(); + inputRef.current?.select(); + }, []); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = value.trim(); + if (trimmed) onConfirm(trimmed); + }; + + return ( +
{ if (e.key === 'Escape') onCancel(); }} + > + + ); +} + +/* ── Layout helpers (from element-web) ── */ + +function readPx(s: CSSStyleDeclaration, name: string, fallback = 0): number { + const v = Number.parseFloat(s.getPropertyValue(name)); + return Number.isFinite(v) ? v : fallback; +} + +function readNum(s: CSSStyleDeclaration, name: string, fallback: number): number { + const v = Number.parseFloat(s.getPropertyValue(name)); + return Number.isFinite(v) ? v : fallback; +} + +function calculateModalLayout(input: { + pageHeight: number; + mascotTopOffset: number; + mascotHeight: number; + modalHeight: number; + footerHeight: number; + anchorRatio: number; + minTop: number; + bottomGap: number; +}) { + const desiredTop = input.mascotTopOffset + input.mascotHeight * input.anchorRatio; + const maxTop = input.pageHeight - input.footerHeight - input.bottomGap - input.modalHeight; + const top = Math.max(input.minTop, Math.min(desiredTop, maxTop)); + const maxHeight = Math.max(0, input.pageHeight - input.footerHeight - input.bottomGap - top); + return { + top: Math.round(top), + maxHeight: Math.floor(maxHeight), + constrained: input.modalHeight > maxHeight, + }; +} + +const rootVars: React.CSSProperties = { + '--vojo-mascot-size': 'clamp(28rem, 57dvh, 48rem)', + '--vojo-mascot-top': 'clamp(1.5rem, 4dvh, 3rem)', + '--vojo-stack-pad': '1.5rem', + '--vojo-anchor-ratio': '0.71', + '--vojo-modal-min-top': 'clamp(12rem, 26dvh, 18rem)', + '--vojo-modal-gap': 'clamp(0.75rem, 2dvh, 1.5rem)', + '--vojo-footer-space': 'clamp(4.5rem, 9dvh, 6.5rem)', +} as React.CSSProperties; + export function AuthLayout() { + const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const { server: urlEncodedServer } = useParams(); const clientConfig = useClientConfig(); - const defaultServer = clientDefaultServer(clientConfig); let server: string = urlEncodedServer ? tryDecodeURIComponent(urlEncodedServer) : defaultServer; @@ -79,13 +187,12 @@ export function AuthLayout() { server = defaultServer; } + const [showServerDialog, setShowServerDialog] = useState(false); + const [discoveryState, discoverServer] = useAsyncCallback( useCallback(async (serverName: string) => { const response = await autoDiscovery(fetch, serverName); - return { - serverName, - response, - }; + return { serverName, response }; }, []) ); @@ -93,7 +200,6 @@ export function AuthLayout() { if (server) discoverServer(server); }, [discoverServer, server]); - // if server is mismatches with path server, update path useEffect(() => { if (!urlEncodedServer || tryDecodeURIComponent(urlEncodedServer) !== server) { navigate( @@ -113,7 +219,9 @@ export function AuthLayout() { return; } navigate( - generatePath(currentAuthPath(location.pathname), { server: encodeURIComponent(newServer) }) + generatePath(currentAuthPath(location.pathname), { + server: encodeURIComponent(newServer), + }) ); }, [navigate, location, discoveryState, server, discoverServer] @@ -122,88 +230,174 @@ export function AuthLayout() { const [autoDiscoveryError, autoDiscoveryInfo] = discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : []; + /* ── Page title based on route ── */ + const getPageTitle = () => { + if (matchPath(LOGIN_PATH, location.pathname)) return t('Auth.title_login'); + if (matchPath(REGISTER_PATH, location.pathname)) return t('Auth.title_register'); + return t('Auth.title_reset_password'); + }; + const pageTitle = getPageTitle(); + + /* ── Refs for JS-driven layout ── */ + const pageRef = useRef(null); + const mascotRef = useRef(null); + const modalRef = useRef(null); + const footerRef = useRef(null); + const cardContentRef = useRef(null); + + useLayoutEffect(() => { + const page = pageRef.current; + const mascot = mascotRef.current; + const modal = modalRef.current; + const footer = footerRef.current; + if (!page || !mascot || !modal || !footer) return undefined; + + let frameId = 0; + const update = (): void => { + cancelAnimationFrame(frameId); + frameId = requestAnimationFrame(() => { + const s = getComputedStyle(page); + const layout = calculateModalLayout({ + pageHeight: page.clientHeight, + mascotTopOffset: readPx(s, '--vojo-mascot-top'), + mascotHeight: mascot.offsetHeight, + modalHeight: modal.offsetHeight, + footerHeight: footer.offsetHeight || readPx(s, '--vojo-footer-space', footer.offsetHeight), + anchorRatio: readNum(s, '--vojo-anchor-ratio', 0.58), + minTop: readPx(s, '--vojo-modal-min-top'), + bottomGap: readPx(s, '--vojo-modal-gap'), + }); + page.style.setProperty('--vojo-modal-top', `${layout.top}px`); + page.style.setProperty('--vojo-modal-max-h', `${layout.maxHeight}px`); + if (cardContentRef.current) { + cardContentRef.current.classList.toggle(css.AuthCardContentConstrained, layout.constrained); + } + }); + }; + + update(); + const ro = new ResizeObserver(update); + ro.observe(page); + ro.observe(mascot); + ro.observe(modal); + ro.observe(footer); + window.addEventListener('resize', update); + + return () => { + cancelAnimationFrame(frameId); + ro.disconnect(); + window.removeEventListener('resize', update); + }; + }, []); + return ( - - - -
- - Cinny Logo - Cinny - -
- - - - Homeserver +
+
+ + +
+
+
+ {/* Title */} + + {pageTitle} - - - {discoveryState.status === AsyncStatus.Loading && ( - - )} - {discoveryState.status === AsyncStatus.Error && ( - - )} - {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && ( - - )} - {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && ( - - )} - {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && ( - - - ( - - )} - error={() => ( - - )} + + {/* Server info row with separator */} +
+ {t('Auth.homeserver')} + {server} + {clientConfig.allowCustomHomeservers && ( + + )} +
+ + {/* Loading / error / form */} + {discoveryState.status === AsyncStatus.Loading && ( + + )} + {discoveryState.status === AsyncStatus.Error && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && ( + + )} + {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && ( + + )} + {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && ( + + + ( + + )} + error={() => ( + + )} + > + {(specVersions) => ( + + ( + + )} + error={() => ( + + )} + > + {(authFlows) => ( + + + + )} + + + )} + + + + )} +
+
+
+
+ + + {/* Server edit dialog */} + {showServerDialog && ( + { + setShowServerDialog(false); + selectServer(newServer); + }} + onCancel={() => setShowServerDialog(false)} + /> + )} +
); } diff --git a/src/app/pages/auth/login/Login.tsx b/src/app/pages/auth/login/Login.tsx index 2f04a733..6dfcfd2a 100644 --- a/src/app/pages/auth/login/Login.tsx +++ b/src/app/pages/auth/login/Login.tsx @@ -1,98 +1,50 @@ import React, { useMemo } from 'react'; import { Box, Text, color } from 'folds'; import { Link, useSearchParams } from 'react-router-dom'; -import { SSOAction } from 'matrix-js-sdk'; +import { useTranslation } from 'react-i18next'; import { useAuthFlows } from '../../../hooks/useAuthFlows'; import { useAuthServer } from '../../../hooks/useAuthServer'; import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; import { PasswordLoginForm } from './PasswordLoginForm'; -import { SSOLogin } from '../SSOLogin'; -import { TokenLogin } from './TokenLogin'; -import { OrDivider } from '../OrDivider'; -import { getLoginPath, getRegisterPath, withSearchParam } from '../../pathUtils'; -import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin'; +import { getRegisterPath } from '../../pathUtils'; import { LoginPathSearchParams } from '../../paths'; -import { useClientConfig } from '../../../hooks/useClientConfig'; - -const getLoginTokenSearchParam = () => { - // when using hasRouter query params in existing route - // gets ignored by react-router, so we need to read it ourself - // we only need to read loginToken as it's the only param that - // is provided by external entity. example: SSO login - const parmas = new URLSearchParams(window.location.search); - const loginToken = parmas.get('loginToken'); - return loginToken ?? undefined; -}; const useLoginSearchParams = (searchParams: URLSearchParams): LoginPathSearchParams => useMemo( () => ({ username: searchParams.get('username') ?? undefined, email: searchParams.get('email') ?? undefined, - loginToken: searchParams.get('loginToken') ?? undefined, }), [searchParams] ); export function Login() { + const { t } = useTranslation(); const server = useAuthServer(); - const { hashRouter } = useClientConfig(); const { loginFlows } = useAuthFlows(); const [searchParams] = useSearchParams(); const loginSearchParams = useLoginSearchParams(searchParams); - const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server)); - const loginTokenForHashRouter = getLoginTokenSearchParam(); - const absoluteLoginPath = usePathWithOrigin(getLoginPath(server)); - - if (hashRouter?.enabled && loginTokenForHashRouter) { - window.location.replace( - withSearchParam(absoluteLoginPath, { - loginToken: loginTokenForHashRouter, - }) - ); - } const parsedFlows = useParsedLoginFlows(loginFlows.flows); return ( - - Login - - {parsedFlows.token && loginSearchParams.loginToken && ( - - )} {parsedFlows.password && ( - <> - - - {parsedFlows.sso && } - + )} - {parsedFlows.sso && ( - <> - - - + {!parsedFlows.password && ( + + {t('Auth.error_client_unsupported', { server })} + )} - {!parsedFlows.password && !parsedFlows.sso && ( - <> - - {`This client does not support login on "${server}" homeserver. Password and SSO based login method not found.`} - - - - )} - - Do not have an account? Register + + {t('Auth.new_here')}{' '} + + {t('Auth.create_account')} + ); diff --git a/src/app/pages/auth/login/PasswordLoginForm.tsx b/src/app/pages/auth/login/PasswordLoginForm.tsx index 62f46dd2..b75a2aae 100644 --- a/src/app/pages/auth/login/PasswordLoginForm.tsx +++ b/src/app/pages/auth/login/PasswordLoginForm.tsx @@ -1,24 +1,16 @@ -import React, { FormEventHandler, MouseEventHandler, useCallback, useState } from 'react'; +import React, { FormEventHandler, useCallback } from 'react'; import { Box, Button, - Header, - Icon, - IconButton, - Icons, Input, - Menu, Overlay, OverlayBackdrop, OverlayCenter, - PopOut, - RectCords, Spinner, Text, - config, } from 'folds'; -import FocusTrap from 'focus-trap-react'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { MatrixError } from 'matrix-js-sdk'; import { getMxIdLocalPart, getMxIdServer, isUserId } from '../../../utils/matrix'; import { EMAIL_REGEX } from '../../../utils/regex'; @@ -36,81 +28,14 @@ import { import { PasswordInput } from '../../../components/password-input'; import { FieldError } from '../FiledError'; import { getResetPasswordPath } from '../../pathUtils'; -import { stopPropagation } from '../../../utils/keyboard'; - -function UsernameHint({ server }: { server: string }) { - const [anchor, setAnchor] = useState(); - - const handleOpenMenu: MouseEventHandler = (evt) => { - setAnchor(evt.currentTarget.getBoundingClientRect()); - }; - return ( - setAnchor(undefined), - clickOutsideDeactivates: true, - escapeDeactivates: stopPropagation, - }} - > - -
- Hint -
- - - - Username: - {' '} - user123 - - - - Matrix ID: - - {` @user123:${server}`} - - - - Email: - - {` user123@${server}`} - - -
- - } - > - - - -
- ); -} type PasswordLoginFormProps = { defaultUsername?: string; defaultEmail?: string; }; + export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLoginFormProps) { + const { t } = useTranslation(); const server = useAuthServer(); const clientConfig = useClientConfig(); @@ -133,7 +58,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog user: username, }, password, - initial_device_display_name: 'Cinny Web', + initial_device_display_name: 'Vojo Web', }); }; @@ -151,9 +76,10 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog user: mxIdUsername, }, password, - initial_device_display_name: 'Cinny Web', + initial_device_display_name: 'Vojo Web', }); }; + const handleEmailLogin = (email: string, password: string) => { startLogin(baseUrl, { type: 'm.login.password', @@ -163,7 +89,7 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog address: email, }, password, - initial_device_display_name: 'Cinny Web', + initial_device_display_name: 'Vojo Web', }); }; @@ -199,65 +125,63 @@ export function PasswordLoginForm({ defaultUsername, defaultEmail }: PasswordLog return ( - - Username - } + placeholder={t('Auth.username_placeholder')} /> {loginState.status === AsyncStatus.Error && ( <> {loginState.error.errcode === LoginError.ServerNotAllowed && ( - + )} {loginState.error.errcode === LoginError.InvalidServer && ( - + )} )} - - Password - - - - {loginState.status === AsyncStatus.Error && ( - <> - {loginState.error.errcode === LoginError.Forbidden && ( - - )} - {loginState.error.errcode === LoginError.UserDeactivated && ( - - )} - {loginState.error.errcode === LoginError.InvalidRequest && ( - - )} - {loginState.error.errcode === LoginError.RateLimited && ( - - )} - {loginState.error.errcode === LoginError.Unknown && ( - - )} - - )} - - - Forget Password? - + + {loginState.status === AsyncStatus.Error && ( + + {loginState.error.errcode === LoginError.Forbidden && ( + + )} + {loginState.error.errcode === LoginError.UserDeactivated && ( + + )} + {loginState.error.errcode === LoginError.InvalidRequest && ( + + )} + {loginState.error.errcode === LoginError.RateLimited && ( + + )} + {loginState.error.errcode === LoginError.Unknown && ( + + )} - + )} + + + {t('Auth.forgot_password')} + + diff --git a/src/app/pages/auth/login/TokenLogin.tsx b/src/app/pages/auth/login/TokenLogin.tsx deleted file mode 100644 index 761d5dc5..00000000 --- a/src/app/pages/auth/login/TokenLogin.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { - Box, - Icon, - Icons, - Overlay, - OverlayBackdrop, - OverlayCenter, - Spinner, - Text, - color, - config, -} from 'folds'; -import React, { useCallback, useEffect } from 'react'; -import { MatrixError } from 'matrix-js-sdk'; -import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; -import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; -import { CustomLoginResponse, LoginError, login, useLoginComplete } from './loginUtil'; - -function LoginTokenError({ message }: { message: string }) { - return ( - - - - Token Login - - {message} - - - - ); -} - -type TokenLoginProps = { - token: string; -}; -export function TokenLogin({ token }: TokenLoginProps) { - const discovery = useAutoDiscoveryInfo(); - const baseUrl = discovery['m.homeserver'].base_url; - - const [loginState, startLogin] = useAsyncCallback< - CustomLoginResponse, - MatrixError, - Parameters - >(useCallback(login, [])); - - useEffect(() => { - startLogin(baseUrl, { - type: 'm.login.token', - token, - initial_device_display_name: 'Cinny Web', - }); - }, [baseUrl, token, startLogin]); - - useLoginComplete(loginState.status === AsyncStatus.Success ? loginState.data : undefined); - - return ( - <> - {loginState.status === AsyncStatus.Error && ( - <> - {loginState.error.errcode === LoginError.Forbidden && ( - - )} - {loginState.error.errcode === LoginError.UserDeactivated && ( - - )} - {loginState.error.errcode === LoginError.InvalidRequest && ( - - )} - {loginState.error.errcode === LoginError.RateLimited && ( - - )} - {loginState.error.errcode === LoginError.Unknown && ( - - )} - - )} - }> - - - - - - ); -} diff --git a/src/app/pages/auth/register/Register.tsx b/src/app/pages/auth/register/Register.tsx index 7176489b..7fdf6e31 100644 --- a/src/app/pages/auth/register/Register.tsx +++ b/src/app/pages/auth/register/Register.tsx @@ -1,16 +1,12 @@ import React, { useMemo } from 'react'; import { Box, Text, color } from 'folds'; import { Link, useSearchParams } from 'react-router-dom'; -import { SSOAction } from 'matrix-js-sdk'; +import { useTranslation } from 'react-i18next'; import { useAuthServer } from '../../../hooks/useAuthServer'; import { RegisterFlowStatus, useAuthFlows } from '../../../hooks/useAuthFlows'; -import { useParsedLoginFlows } from '../../../hooks/useParsedLoginFlows'; import { PasswordRegisterForm, SUPPORTED_REGISTER_STAGES } from '../register/PasswordRegisterForm'; -import { OrDivider } from '../OrDivider'; -import { SSOLogin } from '../SSOLogin'; import { SupportedUIAFlowsLoader } from '../../../components/SupportedUIAFlowsLoader'; import { getLoginPath } from '../../pathUtils'; -import { usePathWithOrigin } from '../../../hooks/usePathWithOrigin'; import { RegisterPathSearchParams } from '../../paths'; const useRegisterSearchParams = (searchParams: URLSearchParams): RegisterPathSearchParams => @@ -24,74 +20,56 @@ const useRegisterSearchParams = (searchParams: URLSearchParams): RegisterPathSea ); export function Register() { + const { t } = useTranslation(); const server = useAuthServer(); - const { loginFlows, registerFlows } = useAuthFlows(); + const { registerFlows } = useAuthFlows(); const [searchParams] = useSearchParams(); const registerSearchParams = useRegisterSearchParams(searchParams); - const { sso } = useParsedLoginFlows(loginFlows.flows); - - // redirect to /login because only that path handle m.login.token - const ssoRedirectUrl = usePathWithOrigin(getLoginPath(server)); return ( - - Register - - {registerFlows.status === RegisterFlowStatus.RegistrationDisabled && !sso && ( + {registerFlows.status === RegisterFlowStatus.RegistrationDisabled && ( - Registration has been disabled on this homeserver. + {t('Auth.register_disabled')} )} - {registerFlows.status === RegisterFlowStatus.RateLimited && !sso && ( + {registerFlows.status === RegisterFlowStatus.RateLimited && ( - You have been rate-limited! Please try after some time. + {t('Auth.register_rate_limited')} )} - {registerFlows.status === RegisterFlowStatus.InvalidRequest && !sso && ( + {registerFlows.status === RegisterFlowStatus.InvalidRequest && ( - Invalid Request! Failed to get any registration options. + {t('Auth.register_invalid_request')} )} {registerFlows.status === RegisterFlowStatus.FlowRequired && ( - <> - - {(supportedFlows) => - supportedFlows.length === 0 ? ( - - This application does not support registration on this homeserver. - - ) : ( - - ) - } - - - {sso && } - + + {(supportedFlows) => + supportedFlows.length === 0 ? ( + + {t('Auth.register_unsupported')} + + ) : ( + + ) + } + )} - {sso && ( - <> - - - - )} - - Already have an account? Login + + {t('Auth.already_have_account')}{' '} + + {t('Auth.title_login')} + ); diff --git a/src/app/pages/auth/reset-password/PasswordResetForm.tsx b/src/app/pages/auth/reset-password/PasswordResetForm.tsx index 392f45c0..058e7e9a 100644 --- a/src/app/pages/auth/reset-password/PasswordResetForm.tsx +++ b/src/app/pages/auth/reset-password/PasswordResetForm.tsx @@ -13,6 +13,7 @@ import { config, } from 'folds'; import { useNavigate } from 'react-router-dom'; +import { Trans, useTranslation } from 'react-i18next'; import FocusTrap from 'focus-trap-react'; import { AuthDict, AuthType, MatrixError, createClient } from 'matrix-js-sdk'; import { useAutoDiscoveryInfo } from '../../../hooks/useAutoDiscoveryInfo'; @@ -36,6 +37,7 @@ type FormData = { }; function ResetPasswordComplete({ email }: { email?: string }) { + const { t } = useTranslation(); const server = useAuthServer(); const navigate = useNavigate(); @@ -56,11 +58,11 @@ function ResetPasswordComplete({ email }: { email?: string }) { - Password has been reset successfully. Please login with your new password. + {t('Auth.reset_success_message')} @@ -75,6 +77,7 @@ type PasswordResetFormProps = { defaultEmail?: string; }; export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) { + const { t } = useTranslation(); const server = useAuthServer(); const serverDiscovery = useAutoDiscoveryInfo(); @@ -167,11 +170,11 @@ export function PasswordResetForm({ defaultEmail }: PasswordResetFormProps) { return ( - Homeserver {server} will send you an email to let you reset your password. + }} /> - Email + {t('Auth.reset_email_label')} - New Password + {t('Auth.reset_new_password')} - Confirm Password + {t('Auth.reset_confirm_password')} )} diff --git a/src/app/pages/auth/reset-password/ResetPassword.tsx b/src/app/pages/auth/reset-password/ResetPassword.tsx index c5e1d2ad..fa217b8a 100644 --- a/src/app/pages/auth/reset-password/ResetPassword.tsx +++ b/src/app/pages/auth/reset-password/ResetPassword.tsx @@ -1,6 +1,7 @@ import { Box, Text } from 'folds'; import React, { useMemo } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; import { getLoginPath } from '../../pathUtils'; import { useAuthServer } from '../../../hooks/useAuthServer'; import { PasswordResetForm } from './PasswordResetForm'; @@ -17,20 +18,21 @@ const useResetPasswordSearchParams = ( ); export function ResetPassword() { + const { t } = useTranslation(); const server = useAuthServer(); const [searchParams] = useSearchParams(); const resetPasswordSearchParams = useResetPasswordSearchParams(searchParams); return ( - - Reset Password - - - Remember your password? Login + + {t('Auth.remember_password')}{' '} + + {t('Auth.title_login')} + ); diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts index 5834ad84..d5adc579 100644 --- a/src/app/pages/auth/styles.css.ts +++ b/src/app/pages/auth/styles.css.ts @@ -1,53 +1,267 @@ -import { style } from '@vanilla-extract/css'; -import { DefaultReset, color, config, toRem } from 'folds'; +import { style, globalStyle } from '@vanilla-extract/css'; +import { DefaultReset, toRem, color } from 'folds'; +/* + * CSS custom properties --vojo-* are managed manually (not via createVar) + * because they're read/written from JS layout logic in AuthLayout.tsx. + */ + +/* ── Root page ── */ export const AuthLayout = style({ - minHeight: '100%', - backgroundColor: color.Background.Container, - color: color.Background.OnContainer, - padding: config.space.S400, - paddingRight: config.space.S200, - paddingBottom: 0, - position: 'relative', -}); - -export const AuthCard = style({ - marginTop: '1vh', - maxWidth: toRem(460), width: '100%', - backgroundColor: color.Surface.Container, - color: color.Surface.OnContainer, - borderRadius: config.radii.R400, - boxShadow: config.shadow.E100, - border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`, + height: '100dvh', + minHeight: '100dvh', + position: 'relative', + display: 'flex', + flexDirection: 'column', overflow: 'hidden', + background: + 'radial-gradient(ellipse 80% 60% at 50% 25%, rgba(92, 70, 170, 0.18) 0%, rgba(92, 70, 170, 0) 70%), radial-gradient(ellipse at center, #171a33 0%, #10132a 45%, #080a1a 100%)', + color: '#e8e4df', + + '@media': { + 'only screen and (max-width: 768px)': { + vars: { + '--vojo-mascot-size': 'min(23rem, 43dvh, 88vw)', + '--vojo-mascot-top': 'clamp(0.75rem, 2.5dvh, 1.5rem)', + '--vojo-stack-pad': '0.75rem', + '--vojo-anchor-ratio': '0.66', + '--vojo-modal-min-top': 'clamp(10.5rem, 24dvh, 16rem)', + '--vojo-modal-gap': '0.75rem', + '--vojo-footer-space': '4.4rem', + }, + }, + 'only screen and (max-height: 760px)': { + vars: { + '--vojo-mascot-size': 'min(21rem, 34dvh, 80vw)', + '--vojo-mascot-top': '0.75rem', + '--vojo-anchor-ratio': '0.6', + '--vojo-modal-min-top': '9rem', + '--vojo-modal-gap': '0.4rem', + '--vojo-footer-space': '4rem', + }, + }, + }, }); +/* ── Stack (mascot + modal zone) ── */ +export const AuthStack = style({ + flex: '1 1 auto', + minHeight: 0, + position: 'relative', + padding: '0 var(--vojo-stack-pad) var(--vojo-footer-space)', + boxSizing: 'border-box', +}); + +/* ── Mascot ── */ +export const AuthMascot = style({ + position: 'absolute', + insetInlineStart: '50%', + top: 'var(--vojo-mascot-top)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: 0, + transform: 'translateX(-50%)', +}); + +export const AuthMascotVideo = style({ + display: 'block', + width: 'var(--vojo-mascot-size)', + height: 'auto', + aspectRatio: '1', + maxWidth: 'calc(100vw - (2 * var(--vojo-stack-pad)))', + pointerEvents: 'none', + userSelect: 'none', + filter: 'drop-shadow(0 30px 70px rgba(91, 70, 170, 0.45))', +}); + +/* ── Modal zone (position driven by JS) ── */ +export const AuthModalZone = style({ + position: 'absolute', + insetInline: 'var(--vojo-stack-pad)', + top: 'var(--vojo-modal-top)', + display: 'flex', + justifyContent: 'center', + boxSizing: 'border-box', + zIndex: 1, +}); + +/* ── Auth card (glassmorphism) ── */ +export const AuthCard = style({ + display: 'flex', + borderRadius: toRem(24), + backgroundColor: 'transparent', + boxShadow: '0 24px 70px rgba(0, 0, 0, 0.5)', + maxWidth: '100%', +}); + +export const AuthCardContent = style({ + display: 'flex', + flexDirection: 'column', + borderRadius: 'inherit', + backgroundColor: 'rgba(16, 18, 40, 0.48)', + backdropFilter: 'blur(22px) saturate(150%)', + WebkitBackdropFilter: 'blur(22px) saturate(150%)', + border: '1px solid rgba(255, 255, 255, 0.08)', + width: `min(${toRem(492)}, calc(100vw - 3rem))`, + padding: `${toRem(42)} ${toRem(50)}`, + boxSizing: 'border-box', + gap: toRem(24), +}); + +export const AuthCardContentConstrained = style({ + maxHeight: 'var(--vojo-modal-max-h)', + overflowY: 'auto', + overscrollBehavior: 'contain', +}); + +/* ── Server info row ── */ +export const ServerRow = style({ + display: 'grid', + gridTemplateColumns: 'auto min-content', + gridTemplateRows: 'auto auto', + gap: '4px', + paddingBottom: toRem(16), + marginBottom: toRem(4), + borderBottom: '1px solid rgba(141, 151, 165, 0.2)', +}); + +export const ServerLabel = style({ + gridColumn: '1 / -1', + gridRow: '1', + fontSize: toRem(13), + color: 'rgba(232, 228, 223, 0.55)', +}); + +export const ServerName = style({ + gridColumn: '1', + gridRow: '2', + fontSize: toRem(15), + color: '#e8e4df', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + alignSelf: 'center', +}); + +export const ServerEdit = style({ + gridColumn: '2', + gridRow: '2', + alignSelf: 'center', + fontSize: toRem(13), + color: '#e8e4df', + background: 'none', + border: 'none', + cursor: 'pointer', + padding: 0, + textDecoration: 'underline', + textUnderlineOffset: '2px', + whiteSpace: 'nowrap', + selectors: { + '&:hover': { + color: '#ffffff', + }, + }, +}); + +/* ── Server edit dialog ── */ +export const ServerDialog = style({ + position: 'fixed', + inset: 0, + zIndex: 100, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +export const ServerDialogBackdrop = style({ + position: 'absolute', + inset: 0, + backgroundColor: 'rgba(0, 0, 0, 0.6)', +}); + +export const ServerDialogCard = style({ + position: 'relative', + backgroundColor: '#1a1d36', + border: '1px solid rgba(255, 255, 255, 0.12)', + borderRadius: toRem(16), + padding: toRem(32), + width: `min(${toRem(420)}, calc(100vw - 3rem))`, + display: 'flex', + flexDirection: 'column', + gap: toRem(20), + color: '#e8e4df', + boxShadow: '0 16px 48px rgba(0, 0, 0, 0.5)', +}); + +/* ── Logo ── */ export const AuthLogo = style([ DefaultReset, { width: toRem(26), height: toRem(26), - borderRadius: '50%', }, ]); -export const AuthHeader = style({ - padding: `0 ${config.space.S400}`, - borderBottomWidth: config.borderWidth.B300, -}); - -export const AuthCardContent = style({ - maxWidth: toRem(402), - width: '100%', - margin: 'auto', - padding: config.space.S400, - paddingTop: config.space.S700, - paddingBottom: toRem(44), - gap: toRem(44), -}); - +/* ── Footer ── */ export const AuthFooter = style({ - padding: config.space.S200, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '10px', + width: '100%', + padding: '20px 24px 24px', + fontSize: '13px', + color: 'rgba(232, 228, 223, 0.55)', + letterSpacing: '0.02em', +}); + +export const AuthFooterSeparator = style({ + color: 'rgba(232, 228, 223, 0.3)', +}); + +/* ── Theme overrides for folds components inside auth card ── */ +/* Extract CSS custom property name from folds color token reference */ +/* e.g. "var(--oq6d070)" → "--oq6d070" — tied to folds API, not hashed names */ +const v = (token: string) => token.replace(/^var\((.+)\)$/, '$1'); + +globalStyle(`${AuthCardContent}`, { + vars: { + /* Background variant: make inputs transparent dark */ + [v(color.Background.Container)]: 'rgba(255, 255, 255, 0.06)', + [v(color.Background.ContainerHover)]: 'rgba(255, 255, 255, 0.10)', + [v(color.Background.ContainerActive)]: 'rgba(255, 255, 255, 0.14)', + [v(color.Background.ContainerLine)]: 'rgba(255, 255, 255, 0.18)', + [v(color.Background.OnContainer)]: '#e8e4df', + + /* Surface variant */ + [v(color.Surface.Container)]: 'rgba(255, 255, 255, 0.04)', + [v(color.Surface.ContainerHover)]: 'rgba(255, 255, 255, 0.08)', + [v(color.Surface.ContainerActive)]: 'rgba(255, 255, 255, 0.12)', + [v(color.Surface.ContainerLine)]: 'rgba(255, 255, 255, 0.18)', + [v(color.Surface.OnContainer)]: '#e8e4df', + + /* Primary: white button with dark text */ + [v(color.Primary.Main)]: '#e8e4df', + [v(color.Primary.MainHover)]: '#ffffff', + [v(color.Primary.MainActive)]: '#d4d0cc', + [v(color.Primary.MainLine)]: 'rgba(255, 255, 255, 0.18)', + [v(color.Primary.OnMain)]: '#10132a', + }, +}); + +globalStyle(`${AuthCardContent} input::placeholder`, { + color: 'rgba(232, 228, 223, 0.35)', +}); + +globalStyle(`${AuthCardContent} a`, { + color: '#e8e4df', + textDecoration: 'underline', + textUnderlineOffset: '2px', +}); + +globalStyle(`${AuthCardContent} a:hover`, { + color: '#ffffff', }); diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 9cf4bfb9..c6a58ba2 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -3,7 +3,6 @@ export const ROOT_PATH = '/'; export type LoginPathSearchParams = { username?: string; email?: string; - loginToken?: string; }; export const LOGIN_PATH = '/login/:server?/';