1+ 'use client' ;
2+
3+ import { useFooterItems } from 'fumadocs-ui/utils/use-footer-items' ;
4+ import { usePathname } from 'fumadocs-core/framework' ;
5+ import Link from 'fumadocs-core/link' ;
6+ import type * as PageTree from 'fumadocs-core/page-tree' ;
7+ import { ChevronLeft , ChevronRight } from 'lucide-react' ;
8+ import { type ComponentProps , useMemo } from 'react' ;
9+ import { cn } from '@/lib/utils' ;
10+ import { useI18n } from 'fumadocs-ui/contexts/i18n' ;
11+ import { config } from '@/lib/config' ;
12+ import { Badge } from '@/components/badge' ;
13+
14+ type Item = Pick < PageTree . Item , 'name' | 'description' | 'url' > & {
15+ title ?: string | null ;
16+ version ?: string | null ;
17+ } ;
18+
19+ export interface FooterProps extends ComponentProps < 'div' > {
20+ /**
21+ * Items including information for the next and previous page
22+ */
23+ items ?: {
24+ previous ?: Item ;
25+ next ?: Item ;
26+ } ;
27+ }
28+
29+ export function Footer ( { items, children, className, ...props } : FooterProps ) {
30+ const footerList = getFooterItems ( ) ;
31+ const pathname = usePathname ( ) ;
32+ const { previous, next } = useMemo ( ( ) => {
33+ if ( items ) return items ;
34+
35+ const idx = footerList . findIndex ( ( item ) => isActive ( item . url , pathname ) ) ;
36+
37+ if ( idx === - 1 ) return { } ;
38+ return {
39+ previous : footerList [ idx - 1 ] ,
40+ next : footerList [ idx + 1 ] ,
41+ } ;
42+ } , [ footerList , items , pathname ] ) ;
43+
44+ return (
45+ < >
46+ < div
47+ className = { cn (
48+ '@container grid gap-4' ,
49+ previous && next ? 'grid-cols-2' : 'grid-cols-1' ,
50+ className ,
51+ ) }
52+ { ...props }
53+ >
54+ { previous && < FooterItem item = { previous } index = { 0 } /> }
55+ { next && < FooterItem item = { next } index = { 1 } /> }
56+ </ div >
57+ { children }
58+ </ >
59+ ) ;
60+ }
61+
62+ function FooterItem ( { item, index } : { item : Item ; index : 0 | 1 } ) {
63+ const { text } = useI18n ( ) ;
64+ const Icon = index === 0 ? ChevronLeft : ChevronRight ;
65+
66+ return (
67+ < Link
68+ href = { item . url }
69+ className = { cn (
70+ 'flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full' ,
71+ index === 1 && 'text-end' ,
72+ ) }
73+ >
74+ < div
75+ className = { cn (
76+ 'inline-flex items-center gap-1.5 font-medium' ,
77+ index === 1 && 'flex-row-reverse' ,
78+ ) }
79+ >
80+ < Icon className = "-mx-1 size-4 shrink-0 rtl:rotate-180" />
81+
82+ < p className = "truncate" >
83+ { item . title && (
84+ < span className = "font-bold" > { item . title } - </ span >
85+ ) }
86+ { item . name }
87+ { item . version && (
88+ < Badge size = "sm" color = 'gray' className = 'ml-1 rounded-md' > { item . version } </ Badge >
89+ ) }
90+ </ p >
91+
92+ </ div >
93+ < p className = "text-fd-muted-foreground truncate" >
94+ { item . description ?? ( index === 0 ? text . previousPage : text . nextPage ) }
95+ </ p >
96+ </ Link >
97+ ) ;
98+ }
99+
100+ function isActive ( url : string , pathname : string ) : unknown {
101+ // Exact match or pathname starts with url followed by a slash
102+ return url === pathname || pathname . startsWith ( url + '/' ) ;
103+ }
104+
105+ function getFooterItems ( ) {
106+ const items = useFooterItems ( ) ;
107+ const allPlugins = config . plugins ;
108+
109+ return items . map ( item => {
110+ const itemPluginId = item . url . split ( '/' ) [ 2 ] ;
111+ const itemVersionId = item . url . split ( '/' ) [ 3 ] ;
112+ const match = allPlugins . find ( p => p . id == itemPluginId ) ;
113+
114+ let version = null ;
115+ let pluginName = null ;
116+
117+ if ( match ) {
118+ pluginName = match . title ;
119+ const versionMatch = match . versions . find ( v => v . version == itemVersionId ) ;
120+ if ( versionMatch ) {
121+ version = versionMatch . version ;
122+ }
123+ }
124+
125+ return {
126+ ...item ,
127+ title : pluginName ,
128+ version : version ,
129+ } ;
130+ } ) ;
131+ }
0 commit comments