diff --git a/client/src/hooks/usePageTitle.ts b/client/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..90a1c27 --- /dev/null +++ b/client/src/hooks/usePageTitle.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +const APP_NAME = 'Liqa'; + +/** + * Sets document.title to " | 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]); +} diff --git a/client/src/pages/credential/CreateCredentialPage.tsx b/client/src/pages/credential/CreateCredentialPage.tsx index d55fb78..d7e4cf0 100644 --- a/client/src/pages/credential/CreateCredentialPage.tsx +++ b/client/src/pages/credential/CreateCredentialPage.tsx @@ -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(); diff --git a/client/src/pages/credential/CredentialDetailPage.tsx b/client/src/pages/credential/CredentialDetailPage.tsx index 42b69ff..52e974c 100644 --- a/client/src/pages/credential/CredentialDetailPage.tsx +++ b/client/src/pages/credential/CredentialDetailPage.tsx @@ -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); diff --git a/client/src/pages/credential/CredentialsPage.tsx b/client/src/pages/credential/CredentialsPage.tsx index 63763c3..d74d996 100644 --- a/client/src/pages/credential/CredentialsPage.tsx +++ b/client/src/pages/credential/CredentialsPage.tsx @@ -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); diff --git a/client/src/pages/credential/EditCredentialPage.tsx b/client/src/pages/credential/EditCredentialPage.tsx index 47834fa..71d544e 100644 --- a/client/src/pages/credential/EditCredentialPage.tsx +++ b/client/src/pages/credential/EditCredentialPage.tsx @@ -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(''); diff --git a/client/src/pages/environment/CreateEnvironmentPage.tsx b/client/src/pages/environment/CreateEnvironmentPage.tsx index 601c700..dad804c 100644 --- a/client/src/pages/environment/CreateEnvironmentPage.tsx +++ b/client/src/pages/environment/CreateEnvironmentPage.tsx @@ -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(); diff --git a/client/src/pages/environment/EditEnvironmentPage.tsx b/client/src/pages/environment/EditEnvironmentPage.tsx index bfa598b..c4143a6 100644 --- a/client/src/pages/environment/EditEnvironmentPage.tsx +++ b/client/src/pages/environment/EditEnvironmentPage.tsx @@ -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(''); diff --git a/client/src/pages/environment/EnvironmentDetailPage.tsx b/client/src/pages/environment/EnvironmentDetailPage.tsx index 83b8f2b..0a83477 100644 --- a/client/src/pages/environment/EnvironmentDetailPage.tsx +++ b/client/src/pages/environment/EnvironmentDetailPage.tsx @@ -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); diff --git a/client/src/pages/environment/EnvironmentsPage.tsx b/client/src/pages/environment/EnvironmentsPage.tsx index 556e3ea..8076682 100644 --- a/client/src/pages/environment/EnvironmentsPage.tsx +++ b/client/src/pages/environment/EnvironmentsPage.tsx @@ -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); diff --git a/client/src/pages/run/AllRunsPage.tsx b/client/src/pages/run/AllRunsPage.tsx index 8ead479..d877bfb 100644 --- a/client/src/pages/run/AllRunsPage.tsx +++ b/client/src/pages/run/AllRunsPage.tsx @@ -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'); diff --git a/client/src/pages/run/RunDetailPage.tsx b/client/src/pages/run/RunDetailPage.tsx index c274821..a66d3db 100644 --- a/client/src/pages/run/RunDetailPage.tsx +++ b/client/src/pages/run/RunDetailPage.tsx @@ -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); diff --git a/client/src/pages/run/RunsPage.tsx b/client/src/pages/run/RunsPage.tsx index f765838..ba9f321 100644 --- a/client/src/pages/run/RunsPage.tsx +++ b/client/src/pages/run/RunsPage.tsx @@ -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); diff --git a/client/src/pages/scenario/CreateScenarioPage.tsx b/client/src/pages/scenario/CreateScenarioPage.tsx index 58cefb3..eaf04c1 100644 --- a/client/src/pages/scenario/CreateScenarioPage.tsx +++ b/client/src/pages/scenario/CreateScenarioPage.tsx @@ -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(); diff --git a/client/src/pages/scenario/CreateStepPage.tsx b/client/src/pages/scenario/CreateStepPage.tsx index 65a9621..fc01299 100644 --- a/client/src/pages/scenario/CreateStepPage.tsx +++ b/client/src/pages/scenario/CreateStepPage.tsx @@ -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>(''); diff --git a/client/src/pages/scenario/EditScenarioPage.tsx b/client/src/pages/scenario/EditScenarioPage.tsx index 695e9e9..800b485 100644 --- a/client/src/pages/scenario/EditScenarioPage.tsx +++ b/client/src/pages/scenario/EditScenarioPage.tsx @@ -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>(''); diff --git a/client/src/pages/scenario/EditStepPage.tsx b/client/src/pages/scenario/EditStepPage.tsx index 9f310ae..0714e59 100644 --- a/client/src/pages/scenario/EditStepPage.tsx +++ b/client/src/pages/scenario/EditStepPage.tsx @@ -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>(''); diff --git a/client/src/pages/scenario/ScenarioDetailPage.tsx b/client/src/pages/scenario/ScenarioDetailPage.tsx index 7cfebf8..24d2c3e 100644 --- a/client/src/pages/scenario/ScenarioDetailPage.tsx +++ b/client/src/pages/scenario/ScenarioDetailPage.tsx @@ -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[]>([]); diff --git a/client/src/pages/scenario/ScenariosPage.tsx b/client/src/pages/scenario/ScenariosPage.tsx index aba85ed..41697b0 100644 --- a/client/src/pages/scenario/ScenariosPage.tsx +++ b/client/src/pages/scenario/ScenariosPage.tsx @@ -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[]>([]); diff --git a/client/src/pages/session/SessionDetailPage.tsx b/client/src/pages/session/SessionDetailPage.tsx index 2a65db3..70d7789 100644 --- a/client/src/pages/session/SessionDetailPage.tsx +++ b/client/src/pages/session/SessionDetailPage.tsx @@ -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); diff --git a/client/src/pages/session/SessionsPage.tsx b/client/src/pages/session/SessionsPage.tsx index e5b179b..1c57321 100644 --- a/client/src/pages/session/SessionsPage.tsx +++ b/client/src/pages/session/SessionsPage.tsx @@ -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); diff --git a/client/src/pages/snippet/CreateSnippetPage.tsx b/client/src/pages/snippet/CreateSnippetPage.tsx index 14fcfc8..6101334 100644 --- a/client/src/pages/snippet/CreateSnippetPage.tsx +++ b/client/src/pages/snippet/CreateSnippetPage.tsx @@ -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(); diff --git a/client/src/pages/snippet/EditSnippetPage.tsx b/client/src/pages/snippet/EditSnippetPage.tsx index 109b90d..3f6c3e6 100644 --- a/client/src/pages/snippet/EditSnippetPage.tsx +++ b/client/src/pages/snippet/EditSnippetPage.tsx @@ -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(''); diff --git a/client/src/pages/snippet/SnippetDetailPage.tsx b/client/src/pages/snippet/SnippetDetailPage.tsx index 963e8e2..db931a7 100644 --- a/client/src/pages/snippet/SnippetDetailPage.tsx +++ b/client/src/pages/snippet/SnippetDetailPage.tsx @@ -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); diff --git a/client/src/pages/snippet/SnippetsPage.tsx b/client/src/pages/snippet/SnippetsPage.tsx index 876919c..1ef3730 100644 --- a/client/src/pages/snippet/SnippetsPage.tsx +++ b/client/src/pages/snippet/SnippetsPage.tsx @@ -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);