@hutusi/amytis 1.5.6 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/CLAUDE.md +3 -2
- package/GEMINI.md +13 -6
- package/README.md +1 -1
- package/TODO.md +21 -76
- package/bun.lock +18 -3
- package/content/about.mdx +1 -0
- package/content/about.zh.mdx +21 -0
- package/content/flows/2026/02/20.md +16 -0
- package/content/links.mdx +42 -0
- package/content/links.zh.mdx +41 -0
- package/content/posts/2026-02-20-i18n-routing-considerations.mdx +150 -0
- package/content/posts/multimedia-showcase/index.mdx +261 -0
- package/content/privacy.mdx +32 -0
- package/content/privacy.zh.mdx +32 -0
- package/docs/ARCHITECTURE.md +11 -2
- package/docs/CONTRIBUTING.md +4 -2
- package/docs/deployment.md +9 -1
- package/eslint.config.mjs +2 -0
- package/package.json +5 -4
- package/public/next-image-export-optimizer-hashes.json +0 -3
- package/scripts/copy-assets.ts +1 -1
- package/site.config.ts +126 -44
- package/src/app/[slug]/page.tsx +0 -10
- package/src/app/archive/page.tsx +38 -10
- package/src/app/books/[slug]/page.tsx +18 -0
- package/src/app/flows/[year]/[month]/[day]/page.tsx +21 -4
- package/src/app/layout.tsx +48 -21
- package/src/app/page.tsx +135 -72
- package/src/app/posts/[slug]/page.tsx +6 -12
- package/src/app/search.json/route.ts +4 -0
- package/src/app/series/[slug]/page.tsx +18 -0
- package/src/app/subscribe/page.tsx +17 -0
- package/src/app/tags/[tag]/page.tsx +9 -26
- package/src/app/tags/page.tsx +3 -8
- package/src/components/AuthorCard.tsx +43 -0
- package/src/components/Comments.tsx +20 -4
- package/src/components/ExternalLinks.tsx +6 -2
- package/src/components/Footer.tsx +35 -26
- package/src/components/LanguageProvider.tsx +0 -5
- package/src/components/LanguageSwitch.tsx +117 -6
- package/src/components/LocaleSwitch.tsx +33 -0
- package/src/components/Navbar.tsx +31 -8
- package/src/components/PostNavigation.tsx +55 -0
- package/src/components/PostSidebar.tsx +172 -126
- package/src/components/ReadingProgressBar.tsx +6 -21
- package/src/components/RelatedPosts.tsx +1 -1
- package/src/components/Search.tsx +420 -70
- package/src/components/SelectedBooksSection.tsx +12 -6
- package/src/components/ShareBar.tsx +115 -0
- package/src/components/SimpleLayoutHeader.tsx +5 -14
- package/src/components/SubscribePage.tsx +298 -0
- package/src/components/TagContentTabs.tsx +103 -0
- package/src/components/TagPageHeader.tsx +7 -13
- package/src/components/TagSidebar.tsx +142 -0
- package/src/components/TagsIndexClient.tsx +156 -0
- package/src/hooks/useScrollY.ts +41 -0
- package/src/i18n/translations.ts +110 -2
- package/src/layouts/PostLayout.tsx +34 -7
- package/src/layouts/SimpleLayout.tsx +53 -15
- package/src/lib/markdown.ts +71 -15
- package/src/lib/search-utils.test.ts +163 -0
- package/src/lib/search-utils.ts +39 -0
- package/src/types/pagefind.d.ts +42 -0
- package/src/components/TableOfContents.tsx +0 -158
|
@@ -27,11 +27,6 @@ export function LanguageProvider({ children }: { children: React.ReactNode }) {
|
|
|
27
27
|
const rafId = requestAnimationFrame(() => {
|
|
28
28
|
if (savedLang && translations[savedLang]) {
|
|
29
29
|
setLanguageState(savedLang);
|
|
30
|
-
} else {
|
|
31
|
-
const browserLang = navigator.language.split('-')[0] as Language;
|
|
32
|
-
if (translations[browserLang]) {
|
|
33
|
-
setLanguageState(browserLang);
|
|
34
|
-
}
|
|
35
30
|
}
|
|
36
31
|
setIsHydrated(true);
|
|
37
32
|
});
|
|
@@ -1,17 +1,128 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useLanguage } from './LanguageProvider';
|
|
4
|
+
import { Language } from '@/i18n/translations';
|
|
5
|
+
import { siteConfig } from '../../site.config';
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
const LOCALE_LABELS: Record<string, string> = {
|
|
8
|
+
en: 'EN',
|
|
9
|
+
zh: '中文',
|
|
10
|
+
};
|
|
7
11
|
|
|
12
|
+
// Short labels for the compact navbar pill
|
|
13
|
+
const LOCALE_LABELS_SHORT: Record<string, string> = {
|
|
14
|
+
en: 'EN',
|
|
15
|
+
zh: '中',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface LanguageSwitchProps {
|
|
19
|
+
/** pill — compact segmented pill for the navbar (default)
|
|
20
|
+
* text — lightweight typographic links for the footer */
|
|
21
|
+
variant?: 'pill' | 'text';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function LanguageSwitch({ variant = 'pill' }: LanguageSwitchProps) {
|
|
25
|
+
const { language, setLanguage, isHydrated } = useLanguage();
|
|
26
|
+
const locales = siteConfig.i18n.locales;
|
|
27
|
+
|
|
28
|
+
if (locales.length < 2) return null;
|
|
29
|
+
|
|
30
|
+
// SSR placeholder — reserve space to avoid layout shift
|
|
31
|
+
if (!isHydrated) {
|
|
32
|
+
return <div className={variant === 'pill' ? 'w-[52px] h-8' : 'w-16 h-4'} aria-hidden="true" />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const currentIndex = locales.indexOf(language);
|
|
36
|
+
const nextLocale = locales[(currentIndex + 1) % locales.length] as Language;
|
|
37
|
+
|
|
38
|
+
// ── Text variant: quiet typographic links for the footer ──────────────────
|
|
39
|
+
if (variant === 'text') {
|
|
40
|
+
if (locales.length === 2) {
|
|
41
|
+
const [a, b] = locales as Language[];
|
|
42
|
+
return (
|
|
43
|
+
<span className="flex items-center gap-1.5" role="group" aria-label="Language">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={() => setLanguage(a)}
|
|
47
|
+
aria-pressed={language === a}
|
|
48
|
+
className={`text-xs font-sans tracking-wide transition-colors duration-150 ${
|
|
49
|
+
language === a
|
|
50
|
+
? 'text-foreground/80 font-medium cursor-default'
|
|
51
|
+
: 'text-muted/50 hover:text-foreground/70 cursor-pointer'
|
|
52
|
+
}`}
|
|
53
|
+
>
|
|
54
|
+
{LOCALE_LABELS[a] ?? a.toUpperCase()}
|
|
55
|
+
</button>
|
|
56
|
+
<span className="text-muted/25 select-none" aria-hidden="true">·</span>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
onClick={() => setLanguage(b)}
|
|
60
|
+
aria-pressed={language === b}
|
|
61
|
+
className={`text-xs font-sans tracking-wide transition-colors duration-150 ${
|
|
62
|
+
language === b
|
|
63
|
+
? 'text-foreground/80 font-medium cursor-default'
|
|
64
|
+
: 'text-muted/50 hover:text-foreground/70 cursor-pointer'
|
|
65
|
+
}`}
|
|
66
|
+
>
|
|
67
|
+
{LOCALE_LABELS[b] ?? b.toUpperCase()}
|
|
68
|
+
</button>
|
|
69
|
+
</span>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
// 3+ locales text fallback: cycle on click
|
|
73
|
+
return (
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
onClick={() => setLanguage(nextLocale)}
|
|
77
|
+
className="text-xs font-sans text-muted/60 hover:text-foreground/80 transition-colors duration-150"
|
|
78
|
+
aria-label={`Switch to ${LOCALE_LABELS[nextLocale] ?? nextLocale}`}
|
|
79
|
+
>
|
|
80
|
+
{LOCALE_LABELS[language] ?? language.toUpperCase()}
|
|
81
|
+
</button>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Pill variant: compact segmented pill for the navbar ───────────────────
|
|
86
|
+
if (locales.length === 2) {
|
|
87
|
+
const [a, b] = locales as Language[];
|
|
88
|
+
return (
|
|
89
|
+
<button
|
|
90
|
+
onClick={() => setLanguage(nextLocale)}
|
|
91
|
+
className="group flex items-center rounded-full border border-muted/20 bg-transparent hover:border-accent/40 transition-all duration-200"
|
|
92
|
+
aria-label={`Switch language to ${LOCALE_LABELS[nextLocale] ?? nextLocale}`}
|
|
93
|
+
title={`Switch to ${LOCALE_LABELS[nextLocale] ?? nextLocale}`}
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
className={`px-2 py-1 rounded-full text-[11px] font-sans font-bold tracking-wider transition-all duration-200 ${
|
|
97
|
+
language === a
|
|
98
|
+
? 'text-accent bg-accent/10'
|
|
99
|
+
: 'text-muted/50 group-hover:text-foreground/60'
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
102
|
+
{LOCALE_LABELS_SHORT[a] ?? a.toUpperCase()}
|
|
103
|
+
</span>
|
|
104
|
+
<span className="text-muted/25 text-[10px] select-none -mx-0.5" aria-hidden="true">·</span>
|
|
105
|
+
<span
|
|
106
|
+
className={`px-2 py-1 rounded-full text-[11px] font-sans font-bold tracking-wider transition-all duration-200 ${
|
|
107
|
+
language === b
|
|
108
|
+
? 'text-accent bg-accent/10'
|
|
109
|
+
: 'text-muted/50 group-hover:text-foreground/60'
|
|
110
|
+
}`}
|
|
111
|
+
>
|
|
112
|
+
{LOCALE_LABELS_SHORT[b] ?? b.toUpperCase()}
|
|
113
|
+
</span>
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 3+ locales pill fallback: show current, click cycles
|
|
8
119
|
return (
|
|
9
120
|
<button
|
|
10
|
-
onClick={() => setLanguage(
|
|
11
|
-
className="
|
|
12
|
-
aria-label=
|
|
121
|
+
onClick={() => setLanguage(nextLocale)}
|
|
122
|
+
className="w-8 h-8 flex items-center justify-center text-foreground/80 hover:text-accent transition-colors duration-200 text-[11px] font-sans font-bold tracking-wider"
|
|
123
|
+
aria-label={`Language: ${LOCALE_LABELS_SHORT[language] ?? language}. Click to switch to ${LOCALE_LABELS_SHORT[nextLocale] ?? nextLocale}`}
|
|
13
124
|
>
|
|
14
|
-
{language
|
|
125
|
+
{LOCALE_LABELS_SHORT[language] ?? language.toUpperCase()}
|
|
15
126
|
</button>
|
|
16
127
|
);
|
|
17
128
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useLanguage } from './LanguageProvider';
|
|
4
|
+
import { useEffect, useRef, ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shows the [data-locale] child matching the active language, hiding all others.
|
|
8
|
+
* Keeps MarkdownRenderer fully server-rendered — no fs/Node modules in client bundle.
|
|
9
|
+
*/
|
|
10
|
+
export default function LocaleSwitch({
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}) {
|
|
15
|
+
const { language } = useLanguage();
|
|
16
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const container = ref.current;
|
|
20
|
+
if (!container) return;
|
|
21
|
+
const elements = container.querySelectorAll<HTMLElement>('[data-locale]');
|
|
22
|
+
let hasMatch = false;
|
|
23
|
+
elements.forEach((el) => {
|
|
24
|
+
if (el.dataset.locale === language) hasMatch = true;
|
|
25
|
+
});
|
|
26
|
+
const effectiveLang = hasMatch ? language : elements[0]?.dataset.locale;
|
|
27
|
+
elements.forEach((el) => {
|
|
28
|
+
el.style.display = el.dataset.locale === effectiveLang ? '' : 'none';
|
|
29
|
+
});
|
|
30
|
+
}, [language]);
|
|
31
|
+
|
|
32
|
+
return <div ref={ref}>{children}</div>;
|
|
33
|
+
}
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { siteConfig } from '../../site.config';
|
|
6
6
|
import ThemeToggle from './ThemeToggle';
|
|
7
|
+
import LanguageSwitch from './LanguageSwitch';
|
|
7
8
|
import Search from '@/components/Search';
|
|
8
9
|
import { useLanguage } from '@/components/LanguageProvider';
|
|
9
10
|
import { resolveLocaleValue } from '@/lib/i18n';
|
|
@@ -19,12 +20,30 @@ interface NavbarProps {
|
|
|
19
20
|
booksList?: NavItem[];
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Map from nav URL to feature key
|
|
24
|
+
const FEATURE_URLS: Partial<Record<string, keyof typeof siteConfig.features>> = {
|
|
25
|
+
'/posts': 'posts',
|
|
26
|
+
'/flows': 'flows',
|
|
27
|
+
'/series': 'series',
|
|
28
|
+
'/books': 'books',
|
|
29
|
+
};
|
|
30
|
+
|
|
22
31
|
export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps) {
|
|
23
32
|
const { t, language } = useLanguage();
|
|
24
33
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
25
|
-
const navItems = [...siteConfig.nav]
|
|
34
|
+
const navItems = [...siteConfig.nav]
|
|
35
|
+
.filter(item => {
|
|
36
|
+
const featureKey = FEATURE_URLS[item.url];
|
|
37
|
+
if (!featureKey) return true; // not a feature-gated item, always show
|
|
38
|
+
return siteConfig.features?.[featureKey]?.enabled !== false;
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => a.weight - b.weight);
|
|
26
41
|
|
|
27
|
-
const getLabel = (name: string): string => {
|
|
42
|
+
const getLabel = (name: string, url: string): string => {
|
|
43
|
+
const featureKey = FEATURE_URLS[url];
|
|
44
|
+
if (featureKey && siteConfig.features?.[featureKey]?.name) {
|
|
45
|
+
return resolveLocaleValue(siteConfig.features[featureKey].name, language);
|
|
46
|
+
}
|
|
28
47
|
const key = name.toLowerCase() as TranslationKey;
|
|
29
48
|
const translated = t(key);
|
|
30
49
|
return translated !== key ? translated : name;
|
|
@@ -74,14 +93,14 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
74
93
|
const Component = isExternal ? 'a' : Link;
|
|
75
94
|
const props = isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {};
|
|
76
95
|
|
|
77
|
-
if (item.
|
|
96
|
+
if (item.url === '/books' && booksList.length > 0) {
|
|
78
97
|
return (
|
|
79
98
|
<div key={item.url} className="relative group">
|
|
80
99
|
<Link
|
|
81
100
|
href={item.url}
|
|
82
101
|
className="text-sm font-sans font-medium text-foreground/80 hover:text-heading no-underline transition-colors duration-200 flex items-center gap-1 py-4"
|
|
83
102
|
>
|
|
84
|
-
{getLabel(item.name)}
|
|
103
|
+
{getLabel(item.name, item.url)}
|
|
85
104
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="opacity-50 group-hover:rotate-180 transition-transform">
|
|
86
105
|
<path d="M6 9l6 6 6-6"/>
|
|
87
106
|
</svg>
|
|
@@ -110,14 +129,14 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
110
129
|
);
|
|
111
130
|
}
|
|
112
131
|
|
|
113
|
-
if (item.
|
|
132
|
+
if (item.url === '/series' && seriesList.length > 0) {
|
|
114
133
|
return (
|
|
115
134
|
<div key={item.url} className="relative group">
|
|
116
135
|
<Link
|
|
117
136
|
href={item.url}
|
|
118
137
|
className="text-sm font-sans font-medium text-foreground/80 hover:text-heading no-underline transition-colors duration-200 flex items-center gap-1 py-4"
|
|
119
138
|
>
|
|
120
|
-
{getLabel(item.name)}
|
|
139
|
+
{getLabel(item.name, item.url)}
|
|
121
140
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="opacity-50 group-hover:rotate-180 transition-transform">
|
|
122
141
|
<path d="M6 9l6 6 6-6"/>
|
|
123
142
|
</svg>
|
|
@@ -153,7 +172,7 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
153
172
|
{...props}
|
|
154
173
|
className="text-sm font-sans font-medium text-foreground/80 hover:text-heading no-underline transition-colors duration-200 flex items-center gap-1"
|
|
155
174
|
>
|
|
156
|
-
{getLabel(item.name)}
|
|
175
|
+
{getLabel(item.name, item.url)}
|
|
157
176
|
{isExternal && (
|
|
158
177
|
<svg
|
|
159
178
|
width="12"
|
|
@@ -197,6 +216,7 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
197
216
|
</svg>
|
|
198
217
|
</button>
|
|
199
218
|
<Search />
|
|
219
|
+
<LanguageSwitch />
|
|
200
220
|
<ThemeToggle />
|
|
201
221
|
</div>
|
|
202
222
|
</div>
|
|
@@ -225,7 +245,7 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
225
245
|
className="flex items-center gap-2 px-3 py-3 text-base font-sans font-medium text-foreground/80 hover:text-accent hover:bg-muted/5 rounded-lg no-underline transition-colors"
|
|
226
246
|
onClick={() => setIsMenuOpen(false)}
|
|
227
247
|
>
|
|
228
|
-
{getLabel(item.name)}
|
|
248
|
+
{getLabel(item.name, item.url)}
|
|
229
249
|
{isExternal && (
|
|
230
250
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="opacity-70">
|
|
231
251
|
<path d="M7 17l9.2-9.2M17 17V7H7" />
|
|
@@ -234,6 +254,9 @@ export default function Navbar({ seriesList = [], booksList = [] }: NavbarProps)
|
|
|
234
254
|
</Component>
|
|
235
255
|
);
|
|
236
256
|
})}
|
|
257
|
+
<div className="mt-2 pt-3 border-t border-muted/10 px-3">
|
|
258
|
+
<LanguageSwitch />
|
|
259
|
+
</div>
|
|
237
260
|
</div>
|
|
238
261
|
</div>
|
|
239
262
|
</>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
import { PostData } from '@/lib/markdown';
|
|
3
|
+
import { t } from '@/lib/i18n';
|
|
4
|
+
|
|
5
|
+
interface PostNavigationProps {
|
|
6
|
+
prev: PostData | null;
|
|
7
|
+
next: PostData | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function PostNavigation({ prev, next }: PostNavigationProps) {
|
|
11
|
+
if (!prev && !next) return null;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<nav
|
|
15
|
+
className="mt-12 pt-12 border-t border-muted/20 grid grid-cols-1 sm:grid-cols-2 gap-3"
|
|
16
|
+
aria-label={t('post_navigation')}
|
|
17
|
+
>
|
|
18
|
+
{prev ? (
|
|
19
|
+
<Link
|
|
20
|
+
href={`/posts/${prev.slug}`}
|
|
21
|
+
className="group flex flex-col gap-1.5 p-4 rounded-xl border border-muted/15 hover:border-accent/30 hover:bg-accent/5 transition-all no-underline"
|
|
22
|
+
>
|
|
23
|
+
<span className="text-[10px] font-sans font-bold uppercase tracking-widest text-muted flex items-center gap-1.5">
|
|
24
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
25
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
|
26
|
+
</svg>
|
|
27
|
+
{t('prev')}
|
|
28
|
+
</span>
|
|
29
|
+
<span className="text-sm font-serif font-semibold text-heading group-hover:text-accent transition-colors line-clamp-2 leading-snug">
|
|
30
|
+
{prev.title}
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-xs font-mono text-muted/60">{prev.date}</span>
|
|
33
|
+
</Link>
|
|
34
|
+
) : <div />}
|
|
35
|
+
|
|
36
|
+
{next ? (
|
|
37
|
+
<Link
|
|
38
|
+
href={`/posts/${next.slug}`}
|
|
39
|
+
className="group flex flex-col gap-1.5 p-4 rounded-xl border border-muted/15 hover:border-accent/30 hover:bg-accent/5 transition-all no-underline sm:items-end sm:text-right"
|
|
40
|
+
>
|
|
41
|
+
<span className="text-[10px] font-sans font-bold uppercase tracking-widest text-muted flex items-center gap-1.5">
|
|
42
|
+
{t('next')}
|
|
43
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
44
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
45
|
+
</svg>
|
|
46
|
+
</span>
|
|
47
|
+
<span className="text-sm font-serif font-semibold text-heading group-hover:text-accent transition-colors line-clamp-2 leading-snug">
|
|
48
|
+
{next.title}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-xs font-mono text-muted/60">{next.date}</span>
|
|
51
|
+
</Link>
|
|
52
|
+
) : <div />}
|
|
53
|
+
</nav>
|
|
54
|
+
);
|
|
55
|
+
}
|