feat(client): set dynamic page title based on content

- add usePageTitle hook that writes '<title> | Liqa' to document.title
- list pages use section name; detail pages use entity name once loaded
- create/edit pages use static action title, edit includes entity name
This commit is contained in:
2026-04-20 16:32:16 +03:00
parent ba46789226
commit aa3bae9020
24 changed files with 63 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
import { useEffect } from 'react';
const APP_NAME = 'Liqa';
/**
* Sets document.title to "<title> | Liqa" for the current page.
* Pass undefined or empty string to fall back to just "Liqa".
*/
export function usePageTitle(title: string | null | undefined): void {
useEffect(() => {
const prev = document.title;
document.title = title ? `${title} | ${APP_NAME}` : APP_NAME;
return () => {
document.title = prev;
};
}, [title]);
}
@@ -1,6 +1,7 @@
import { useState, SubmitEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { KeyRound, X, Save } from 'lucide-react';
import { credentials } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
export function CreateCredentialPage() {
const { t } = useTranslation();
usePageTitle(t('credentials.create_title'));
const navigate = useNavigate();
const toast = useToast();
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Upload, Pencil, Trash2, KeyRound } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
@@ -16,6 +17,7 @@ export function CredentialDetailPage() {
const navigate = useNavigate();
const [credential, setCredential] = useState<Credential | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(credential?.name ?? null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Settings, Pencil, Trash2, Plus, Download, KeyRound } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { credentials } from '../../api';
@@ -100,6 +101,7 @@ function AddCredentialCard() {
export function CredentialsPage() {
const { t } = useTranslation();
usePageTitle(t('credentials.title'));
const navigate = useNavigate();
const [items, setItems] = useState<Credential[]>([]);
const [loading, setLoading] = useState(true);
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { KeyRound, X, Save } from 'lucide-react';
import { credentials } from '../../api';
import type { Credential } from '../../api';
@@ -15,6 +16,7 @@ export function EditCredentialPage() {
const [credential, setCredential] = useState<Credential | null>(null);
const [name, setName] = useState('');
usePageTitle(credential ? `${t('credentials.edit_title')}${credential.name}` : t('credentials.edit_title'));
const [data, setData] = useState('');
const [nameError, setNameError] = useState('');
const [dataError, setDataError] = useState('');
@@ -1,6 +1,7 @@
import { useState, SubmitEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
export function CreateEnvironmentPage() {
const { t } = useTranslation();
usePageTitle(t('environments.create_title'));
const navigate = useNavigate();
const toast = useToast();
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Globe, Save } from 'lucide-react';
import { environments } from '../../api';
import type { Environment } from '../../api';
@@ -15,6 +16,7 @@ export function EditEnvironmentPage() {
const [env, setEnv] = useState<Environment | null>(null);
const [name, setName] = useState('');
usePageTitle(env ? `${t('environments.edit_title')}${env.name}` : t('environments.edit_title'));
const [description, setDescription] = useState('');
const [dataJson, setDataJson] = useState('{}');
const [nameError, setNameError] = useState('');
@@ -1,6 +1,7 @@
import { Globe, Pencil, Trash2, Upload } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { useNavigate, useParams } from 'react-router-dom';
import { stringify as yamlStringify } from 'yaml';
import type { Environment } from '../../api';
@@ -25,6 +26,7 @@ export function EnvironmentDetailPage() {
const navigate = useNavigate();
const [env, setEnv] = useState<Environment | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(env?.name ?? null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const dataEntries = Object.entries(env?.data ?? {}).filter(([, v]) => v);
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Settings, ExternalLink, Pencil, Trash2, Plus, Globe, Download } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { stringify as yamlStringify } from 'yaml';
@@ -117,6 +118,7 @@ function AddEnvironmentCard() {
export function EnvironmentsPage() {
const { t } = useTranslation();
usePageTitle(t('environments.title'));
const navigate = useNavigate();
const [items, setItems] = useState<Environment[]>([]);
const [loading, setLoading] = useState(true);
+2
View File
@@ -2,6 +2,7 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Activity } from 'lucide-react';
import { runs } from '../../api';
import type { ScenarioRun, ScenarioRunStatus } from '../../api';
@@ -38,6 +39,7 @@ type AllRunRow = ScenarioRun & {
export function AllRunsPage() {
const { t } = useTranslation();
usePageTitle(t('runs.title'));
const navigate = useNavigate();
const [sortBy, setSortBy] = useState('createdAt');
+2
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { runs } from '../../api';
import type {
Scenario,
@@ -69,6 +70,7 @@ export function RunDetailPage() {
const [scenario, setScenario] = useState<Scenario | null>(null);
const [run, setRun] = useState<ScenarioRunDetail | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(scenario ? `${t('runs.title')}${scenario.name}` : t('runs.title'));
const [error, setError] = useState<string | null>(null);
const [polling, setPolling] = useState(false);
const [pulseKey, setPulseKey] = useState(0);
+2
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Play, ClipboardList, Activity } from 'lucide-react';
import { environments, scenarios, runs } from '../../api';
import type {
@@ -50,6 +51,7 @@ export function RunsPage() {
const [scenario, setScenario] = useState<Scenario | null>(null);
const [items, setItems] = useState<RunRow[]>([]);
usePageTitle(scenario ? `${t('runs.title')}${scenario.name}` : t('runs.title'));
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pulseKey, setPulseKey] = useState(0);
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, environments } from '../../api';
import type { Environment } from '../../api';
@@ -9,6 +10,7 @@ import styles from '../Page.module.css';
export function CreateScenarioPage() {
const { t } = useTranslation();
usePageTitle(t('scenarios.create_title'));
const navigate = useNavigate();
const toast = useToast();
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, steps } from '../../api';
import type { Scenario } from '../../api';
@@ -14,6 +15,7 @@ export function CreateStepPage() {
const toast = useToast();
const [scenario, setScenario] = useState<Scenario | null>(null);
usePageTitle(t('steps.create_title'));
const [title, setTitle] = useState('');
const [execCode, setExecCode] = useState('');
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, environments } from '../../api';
import type { Scenario, Environment } from '../../api';
@@ -15,6 +16,7 @@ export function EditScenarioPage() {
const [scenario, setScenario] = useState<Scenario | null>(null);
const [name, setName] = useState('');
usePageTitle(scenario ? `${t('scenarios.edit_title')}${scenario.name}` : t('scenarios.edit_title'));
const [description, setDescription] = useState('');
const [environmentId, setEnvironmentId] = useState<string>('');
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, ClipboardList } from 'lucide-react';
import { scenarios, steps } from '../../api';
import type { Scenario, ScenarioStep } from '../../api';
@@ -15,6 +16,7 @@ export function EditStepPage() {
const [scenario, setScenario] = useState<Scenario | null>(null);
const [step, setStep] = useState<ScenarioStep | null>(null);
usePageTitle(step ? t('steps.edit_title', { order: step.order }) : t('steps.edit_title', { order: '' }));
const [title, setTitle] = useState('');
const [execCode, setExecCode] = useState('');
const [timeoutSeconds, setTimeoutSeconds] = useState<string>('');
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import {
Play,
Pencil,
@@ -51,6 +52,7 @@ export function ScenarioDetailPage() {
const toast = useToast();
const [scenario, setScenario] = useState<(Scenario & { steps: ScenarioStep[] }) | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(scenario?.name ?? null);
// Credentials state
const [scenarioCreds, setScenarioCreds] = useState<ScenarioCredential[]>([]);
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Play, Plus, Trash2, Download, ClipboardList } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { environments, scenarios } from '../../api';
@@ -38,6 +39,7 @@ const LAST_RUN_LABEL: Record<ScenarioRunStatus, string> = {
export function ScenariosPage() {
const { t } = useTranslation();
usePageTitle(t('scenarios.title'));
const navigate = useNavigate();
const toast = useToast();
const [items, setItems] = useState<Scenario[]>([]);
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Trash2, Monitor } from 'lucide-react';
import { sessions } from '../../api';
import type { Session } from '../../api';
@@ -14,6 +15,7 @@ export function SessionDetailPage() {
const navigate = useNavigate();
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(session?.sessionName ?? null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Trash2, Monitor } from 'lucide-react';
import { sessions } from '../../api';
import type { Session } from '../../api';
@@ -19,6 +20,7 @@ import styles from '../Page.module.css';
export function SessionsPage() {
const { t } = useTranslation();
usePageTitle(t('sessions.title'));
const navigate = useNavigate();
const [items, setItems] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
@@ -1,6 +1,7 @@
import { useState, SubmitEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, Braces } from 'lucide-react';
import { snippets } from '../../api';
import { Breadcrumbs, Button, Card, Input, CodeEditor, useToast } from '../../ui';
@@ -8,6 +9,7 @@ import styles from '../Page.module.css';
export function CreateSnippetPage() {
const { t } = useTranslation();
usePageTitle(t('snippets.create_title'));
const navigate = useNavigate();
const toast = useToast();
@@ -1,6 +1,7 @@
import { useEffect, useState, SubmitEvent } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { X, Save, Braces } from 'lucide-react';
import { snippets } from '../../api';
import type { Snippet } from '../../api';
@@ -15,6 +16,7 @@ export function EditSnippetPage() {
const [snippet, setSnippet] = useState<Snippet | null>(null);
const [alias, setAlias] = useState('');
usePageTitle(snippet ? `${t('snippets.edit_title')}${snippet.title}` : t('snippets.edit_title'));
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [code, setCode] = useState('');
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Upload, Pencil, Trash2, Braces } from 'lucide-react';
import { CodeBlock } from '../../ui';
import { stringify as yamlStringify } from 'yaml';
@@ -24,6 +25,7 @@ export function SnippetDetailPage() {
const navigate = useNavigate();
const [snippet, setSnippet] = useState<Snippet | null>(null);
const [loading, setLoading] = useState(true);
usePageTitle(snippet?.title ?? null);
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTitle } from '../../hooks/usePageTitle';
import { Settings, Pencil, Trash2, Plus, Download, Braces } from 'lucide-react';
import { parse as yamlParse } from 'yaml';
import { snippets } from '../../api';
@@ -114,6 +115,7 @@ function AddSnippetCard() {
export function SnippetsPage() {
const { t } = useTranslation();
usePageTitle(t('snippets.title'));
const navigate = useNavigate();
const [items, setItems] = useState<Snippet[]>([]);
const [loading, setLoading] = useState(true);