Set dynamic document titles per page
This commit is contained in:
+2
-2
@@ -6,7 +6,7 @@
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Portal</title>
|
||||
<title>Stalwart WebUI</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -14,4 +14,4 @@
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const APP_NAME = 'Stalwart WebUI';
|
||||
|
||||
// Keeps the tab title in sync with the current page instead of the static
|
||||
// index.html title; falls back to the bare app name when no page title is given.
|
||||
export function useDocumentTitle(title?: string | null) {
|
||||
useEffect(() => {
|
||||
document.title = title ? `${title} · ${APP_NAME}` : APP_NAME;
|
||||
}, [title]);
|
||||
}
|
||||
@@ -76,6 +76,7 @@
|
||||
"preset30d": "Last 30 days",
|
||||
"preset7d": "Last 7 days",
|
||||
"preset90d": "Last 90 days",
|
||||
"title": "Dashboard",
|
||||
"to": "To"
|
||||
},
|
||||
"deliveryTrace": {
|
||||
@@ -308,6 +309,7 @@
|
||||
"continue": "Continue",
|
||||
"error": "An unexpected error occurred",
|
||||
"prompt": "Enter your account name to continue",
|
||||
"title": "Sign in",
|
||||
"usernamePlaceholder": "user@example.com"
|
||||
},
|
||||
"logo": {
|
||||
@@ -326,6 +328,7 @@
|
||||
"missingParams": "Missing authorization code or state parameter",
|
||||
"processing": "Completing sign in...",
|
||||
"stateMismatch": "State parameter mismatch. Please try logging in again.",
|
||||
"title": "Signing in",
|
||||
"tokenExchangeFailed": "Token exchange failed: {{status}} {{statusText}}"
|
||||
},
|
||||
"otp": {
|
||||
|
||||
@@ -19,6 +19,8 @@ import { MainContent } from '@/components/layout/MainContent';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { ErrorBoundary } from '@/components/layout/ErrorBoundary';
|
||||
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { friendlyName } from '@/hooks/useGlobalSearch';
|
||||
import {
|
||||
findFirstAccessibleLinkInLayout,
|
||||
findFirstVisibleLinkInLayout,
|
||||
@@ -85,6 +87,23 @@ export default function AdminPanel() {
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const [initializing, setInitializing] = useState(!isSchemaLoaded);
|
||||
|
||||
const searchIndex = useSchemaStore((s) => s.searchIndex);
|
||||
|
||||
// Tab title mirrors the sidebar label (search index) so it always matches
|
||||
// what the user sees in the navigation, e.g. "MTA-STS · Settings".
|
||||
const pageTitle = useMemo(() => {
|
||||
if (!section) return t('dashboard.title', 'Dashboard');
|
||||
if (!viewName) return section;
|
||||
const entry =
|
||||
searchIndex.find((e) => e.type === 'link' && e.viewName === viewName && e.section === section) ??
|
||||
searchIndex.find((e) => e.type === 'link' && e.viewName === viewName);
|
||||
const base = entry?.text ?? friendlyName(viewName);
|
||||
if (id === 'new') return `${t('form.createTitle', 'Create {{name}}', { name: base })} · ${section}`;
|
||||
return `${base} · ${section}`;
|
||||
}, [section, viewName, id, searchIndex, t]);
|
||||
|
||||
useDocumentTitle(pageTitle);
|
||||
|
||||
useEffect(() => {
|
||||
const bypassToken = import.meta.env.VITE_ACCESS_TOKEN;
|
||||
if (bypassToken && !accessToken) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ArrowRight, Loader2 } from 'lucide-react';
|
||||
|
||||
import Logo from '@/components/common/Logo';
|
||||
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
@@ -24,6 +25,8 @@ export default function LoginPage() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useDocumentTitle(t('login.title', 'Sign in'));
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
const trimmed = username.trim();
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
|
||||
export default function NotFound() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useDocumentTitle(t('errors.notFound', 'The requested resource was not found.'));
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Loader2 } from 'lucide-react';
|
||||
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { getBasePath } from '@/lib/basePath';
|
||||
import { exchangeCode, getStoredOAuthData, clearStoredOAuthData, getOAuthRedirectUri } from '@/services/auth/oauth';
|
||||
|
||||
@@ -20,6 +21,8 @@ export default function OAuthCallback() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useDocumentTitle(t('oauth.title', 'Signing in'));
|
||||
|
||||
useEffect(() => {
|
||||
async function handleCallback() {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user