Skip to content

Commit 89a0a1d

Browse files
committed
feat: add docs url for each page in page header
1 parent 42005e8 commit 89a0a1d

File tree

10 files changed

+106
-22
lines changed

10 files changed

+106
-22
lines changed

dashboard/public/statics/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"hosts": "Hosts",
1515
"success": "Success",
1616
"error": "Error",
17+
"tutorial": "View tutorial",
1718
"username": "Username",
1819
"modifying": "Modifying...",
1920
"removing": "Removing...",

dashboard/public/statics/locales/fa.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,7 @@
12531253
},
12541254
"success": "موفق",
12551255
"error": "خطا",
1256+
"tutorial": "مشاهده آموزش",
12561257
"theme": {
12571258
"title": "تم",
12581259
"description": "سفارشی‌سازی تم برنامه شما",

dashboard/public/statics/locales/ru.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"hosts": "Хосты",
4242
"success": "Успешно",
4343
"error": "Ошибка",
44+
"tutorial": "Просмотр руководства",
4445
"modifying": "Изменение...",
4546
"removing": "Удаление...",
4647
"creating": "Создание...",

dashboard/public/statics/locales/zh.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"hosts": "主机",
1313
"success": "成功",
1414
"error": "错误",
15+
"tutorial": "查看教程",
1516
"modifying": "修改中...",
1617
"removing": "删除中...",
1718
"creating": "正在创建...",

dashboard/src/components/page-header.tsx

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Button } from '@/components/ui/button'
22
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
33
import useDirDetection from '@/hooks/use-dir-detection'
4-
import { LucideIcon, Plus } from 'lucide-react'
4+
import { getDocsUrl } from '@/utils/docs-url'
5+
import { LucideIcon, Plus, HelpCircle } from 'lucide-react'
56
import { useTranslation } from 'react-i18next'
7+
import { useLocation } from 'react-router'
68

79
interface PageHeaderProps {
810
title: string
@@ -11,15 +13,41 @@ interface PageHeaderProps {
1113
onButtonClick?: () => void
1214
buttonIcon?: LucideIcon
1315
buttonTooltip?: string
16+
tutorialUrl?: string
1417
}
1518

16-
export default function PageHeader({ title, description, buttonText, onButtonClick, buttonIcon: Icon = Plus, buttonTooltip }: PageHeaderProps) {
19+
export default function PageHeader({ title, description, buttonText, onButtonClick, buttonIcon: Icon = Plus, buttonTooltip, tutorialUrl }: PageHeaderProps) {
1720
const { t } = useTranslation()
1821
const dir = useDirDetection()
22+
const location = useLocation()
23+
24+
// Generate tutorial URL if not provided
25+
const docsUrl = tutorialUrl || getDocsUrl(location.pathname)
26+
1927
return (
2028
<div dir={dir} className="mx-auto flex w-full flex-row items-start justify-between gap-4 px-4 py-4 md:pt-6">
2129
<div className="flex flex-col gap-y-1">
22-
<h1 className="text-lg font-medium sm:text-xl">{t(title)}</h1>
30+
<div className="flex items-center gap-2.5">
31+
<h1 className="text-lg font-medium sm:text-xl">{t(title)}</h1>
32+
<TooltipProvider>
33+
<Tooltip>
34+
<TooltipTrigger asChild>
35+
<a
36+
href={docsUrl}
37+
target="_blank"
38+
rel="noopener noreferrer"
39+
className="inline-flex h-7 w-7 items-center justify-center rounded-md border-0 text-primary transition-colors hover:border-2 hover:border-primary/40 hover:bg-primary/5 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
40+
aria-label={t('tutorial', { defaultValue: 'View tutorial' })}
41+
>
42+
<HelpCircle className="h-4 w-4" />
43+
</a>
44+
</TooltipTrigger>
45+
<TooltipContent>
46+
<p>{t('tutorial', { defaultValue: 'View tutorial' })}</p>
47+
</TooltipContent>
48+
</Tooltip>
49+
</TooltipProvider>
50+
</div>
2351
{description && <span className="whitespace-normal text-xs text-muted-foreground sm:text-sm">{t(description)}</span>}
2452
</div>
2553
{buttonText && onButtonClick && (

dashboard/src/pages/_dashboard._index.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react'
2727
import { useForm } from 'react-hook-form'
2828
import { useTranslation } from 'react-i18next'
2929
import { toast } from 'sonner'
30+
import PageHeader from '@/components/page-header'
3031
import { getDefaultUserForm, UseEditFormValues, UseFormValues } from './_dashboard.users'
3132
// Lazy load CoreConfigModal to prevent Monaco Editor from loading until needed
3233
const CoreConfigModal = lazy(() => import('@/components/dialogs/CoreConfigModal'))
@@ -348,21 +349,13 @@ const Dashboard = () => {
348349
return (
349350
<div className="flex w-full flex-col items-start gap-2">
350351
<div className="w-full transform-gpu animate-fade-in" style={{ animationDuration: '400ms' }}>
351-
<div className="mx-auto flex w-full flex-row items-start justify-between gap-2 px-3 py-3 sm:gap-4 sm:px-4 md:py-4 lg:pt-6">
352-
<div className="flex min-w-0 flex-1 flex-col gap-y-1 pr-2 sm:pr-0">
353-
<h1 className="truncate text-base font-medium sm:text-lg lg:text-xl">{t('dashboard')}</h1>
354-
<span className="whitespace-normal text-xs leading-relaxed text-muted-foreground sm:text-sm">{t('dashboardDescription')}</span>
355-
</div>
356-
<div className="flex flex-shrink-0 gap-1 sm:gap-2">
357-
<Button onClick={handleOpenQuickActions} size="sm" variant="outline" className="hidden text-xs sm:flex sm:text-sm">
358-
<Bookmark className="h-3 w-3 sm:h-4 sm:w-4" />
359-
<span className="hidden lg:inline">{t('quickActions.title')}</span>
360-
</Button>
361-
<Button onClick={handleOpenQuickActions} size="sm" variant="outline" className="p-2 sm:hidden">
362-
<Bookmark className="h-3 w-3" />
363-
</Button>
364-
</div>
365-
</div>
352+
<PageHeader
353+
title="dashboard"
354+
description="dashboardDescription"
355+
buttonIcon={Bookmark}
356+
buttonText="quickActions.title"
357+
onButtonClick={handleOpenQuickActions}
358+
/>
366359
<Separator />
367360
</div>
368361

dashboard/src/pages/_dashboard.bulk.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PageHeader from '@/components/page-header'
22
import PageTransition from '@/components/PageTransition'
3+
import { getDocsUrl } from '@/utils/docs-url'
34
import { ArrowUpDown, Calendar, Lock, Users2 } from 'lucide-react'
45
import { useEffect, useState } from 'react'
56
import { useTranslation } from 'react-i18next'
@@ -65,7 +66,7 @@ const BulkPage = () => {
6566
return (
6667
<div className="flex w-full flex-col items-start gap-0">
6768
<PageTransition isContentTransition={true}>
68-
<PageHeader {...getPageHeaderProps()} />
69+
<PageHeader {...getPageHeaderProps()} tutorialUrl={getDocsUrl(location.pathname)} />
6970
</PageTransition>
7071
<div className="w-full">
7172
<div className="scrollbar-hide flex overflow-x-auto border-b px-4 lg:flex-wrap">

dashboard/src/pages/_dashboard.nodes.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PageHeader from '@/components/page-header'
22
import PageTransition from '@/components/PageTransition'
3+
import { getDocsUrl } from '@/utils/docs-url'
34
import { Cpu, LucideIcon, Share2, Plus, FileText } from 'lucide-react'
45
import { useEffect, useState } from 'react'
56
import { useTranslation } from 'react-i18next'
@@ -70,7 +71,7 @@ const Settings = () => {
7071
return (
7172
<div className="flex w-full flex-col items-start gap-0">
7273
<PageTransition isContentTransition={true}>
73-
<PageHeader {...getPageHeaderProps()} />
74+
<PageHeader {...getPageHeaderProps()} tutorialUrl={getDocsUrl(location.pathname)} />
7475
</PageTransition>
7576
<div className="w-full">
7677
<div className="flex border-b px-4">

dashboard/src/pages/_dashboard.settings.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const sudoTabs: Tab[] = [
5151
const nonSudoTabs: Tab[] = [{ id: 'theme', label: 'theme.title', icon: Palette, url: '/settings/theme' }]
5252

5353
export default function Settings() {
54-
const { t } = useTranslation()
54+
const { t, i18n } = useTranslation()
5555
const location = useLocation()
5656
const navigate = useNavigate()
5757
const { admin } = useAdmin()
@@ -202,10 +202,16 @@ export default function Settings() {
202202
isSaving: is_sudo ? isSaving : false,
203203
}
204204

205+
// Generate tutorial URL for the current settings tab
206+
const getTutorialUrl = () => {
207+
const locale = i18n.language || 'en'
208+
return `https://docs.pasarguard.org/${locale}/panel/settings`
209+
}
210+
205211
return (
206212
<SettingsContext.Provider value={settingsContextValue}>
207213
<div className="flex w-full flex-col items-start gap-0">
208-
<PageHeader title={t(`settings.${activeTab}.title`)} description="manageSettings" />
214+
<PageHeader title={t(`settings.${activeTab}.title`)} description="manageSettings" tutorialUrl={getTutorialUrl()} />
209215

210216
<div className="relative w-full">
211217
<div className="flex border-b">

dashboard/src/utils/docs-url.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { DOCUMENTATION } from '@/constants/Project'
2+
import i18n from '@/locales/i18n'
3+
4+
/**
5+
* Generates a documentation URL for a given page path
6+
* Format: docs.pasarguard.org/{locale}/panel/{page}
7+
*
8+
* @param pagePath - The page path (e.g., '/settings', '/users', '/nodes/cores')
9+
* @returns The full documentation URL
10+
*/
11+
export function getDocsUrl(pagePath: string): string {
12+
const locale = i18n.language || 'en'
13+
// Normalize locale (e.g., 'en-US' -> 'en')
14+
const normalizedLocale = locale.split('-')[0]
15+
16+
// Map route paths to documentation paths
17+
const pathMap: Record<string, string> = {
18+
'/': 'dashboard',
19+
'/users': 'users',
20+
'/statistics': 'statistics',
21+
'/hosts': 'hosts',
22+
'/nodes': 'nodes',
23+
'/groups': 'groups',
24+
'/templates': 'templates',
25+
'/admins': 'admins',
26+
'/settings': 'settings',
27+
'/bulk': 'bulk',
28+
}
29+
30+
// Handle nested routes - find the longest matching route
31+
let mappedPath = ''
32+
let longestMatch = ''
33+
34+
for (const [route, docPath] of Object.entries(pathMap)) {
35+
if (pagePath.startsWith(route) && route.length > longestMatch.length) {
36+
longestMatch = route
37+
mappedPath = docPath
38+
}
39+
}
40+
41+
// If we found a match, always use the base path (no sub-routes in docs)
42+
if (mappedPath) {
43+
return `${DOCUMENTATION}/${normalizedLocale}/panel/${mappedPath}`
44+
}
45+
46+
// If no mapping found, use the last segment of the path
47+
const segments = pagePath.split('/').filter(Boolean)
48+
const fallbackPath = segments[segments.length - 1] || 'dashboard'
49+
return `${DOCUMENTATION}/${normalizedLocale}/panel/${fallbackPath}`
50+
}
51+

0 commit comments

Comments
 (0)