Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions apps/frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { Github } from 'lucide-react'
import { LocaleSwitcher } from '@/components/LocaleSwitcher'
import { ThemeToggle } from '@/components/ThemeToggle'
import { getTranslations } from '@/i18n'
import { localizePath } from '@/lib/routes'

const translations = {
en: { submit: 'Submit' },
zh: { submit: '投稿' },
} as const

export function Header({ locale }: { locale: 'en' | 'zh' }) {
const t = translations[locale]
const t = getTranslations(locale)

return (
<header className="relative z-10">
Expand All @@ -19,6 +15,12 @@ export function Header({ locale }: { locale: 'en' | 'zh' }) {
</a>

<div className="flex items-center gap-2 text-sm">
<a
href={localizePath(locale, '/tags')}
className="inline-flex h-8 items-center justify-center rounded-lg px-2.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
{t.card.tags}
</a>
<a
href="https://github.com/bkhq/website"
target="_blank"
Expand All @@ -34,7 +36,7 @@ export function Header({ locale }: { locale: 'en' | 'zh' }) {
href={localizePath(locale, '/sys/submit')}
className="inline-flex h-8 items-center justify-center gap-2 whitespace-nowrap rounded-lg border border-input bg-background px-3 text-sm font-medium shadow-xs transition-colors hover:bg-accent hover:text-accent-foreground"
>
{t.submit}
{t.nav.submit}
</a>
</div>
</div>
Expand Down
15 changes: 12 additions & 3 deletions apps/frontend/src/components/MarkdownArticlePage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ interface Props {

const { locale, title, description, content } = Astro.props
const htmlContent = await renderMarkdown(content, locale)
const dividerLabel = locale === 'zh' ? '内容' : 'Content'
---

<Base title={title} description={description} locale={locale}>
<section class="relative z-10">
<div class="mx-auto w-full max-w-5xl px-4 pb-14 pt-8 sm:px-6 sm:pt-10 lg:px-8">
<div class="pb-8 text-center">
<div class="mx-auto w-full max-w-5xl px-4 pb-8 pt-8 sm:px-6 sm:pt-10 lg:px-8">
<div class="pb-5 text-center">
<h1 class="text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
{title}
</h1>
Expand All @@ -31,8 +32,16 @@ const htmlContent = await renderMarkdown(content, locale)
</div>
</div>

<div class="mx-auto mb-5 flex w-full max-w-[75ch] items-center gap-3 px-1">
<span class="h-px flex-1 bg-gradient-to-r from-transparent via-border to-border/70"></span>
<span class="rounded-full border border-border/50 bg-background/80 px-2.5 py-0.5 text-[10px] font-medium uppercase tracking-[0.18em] text-muted-foreground/75">
{dividerLabel}
</span>
<span class="h-px flex-1 bg-gradient-to-l from-transparent via-border to-border/70"></span>
</div>

<div class="flex items-start justify-center">
<article class="prose-article rounded-xl bg-white p-6 shadow-sm dark:bg-white/[0.03] sm:p-8" set:html={htmlContent} />
<article class="prose-article prose-article-narrow rounded-xl bg-white p-6 shadow-sm dark:bg-white/[0.03] sm:p-8" set:html={htmlContent} />
</div>
</div>
</section>
Expand Down
108 changes: 45 additions & 63 deletions apps/frontend/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,62 @@
import { Check, Moon, Sun } from 'lucide-react'
import { useCallback, useEffect, useState } from 'react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Moon, Sun, SunMoon } from 'lucide-react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { getTranslations } from '@/i18n'

type Theme = 'light' | 'dark'
type ThemeMode = 'light' | 'dark' | 'system'

const STORAGE_KEY = 'bkio-theme'

function getSystemTheme(): Theme {
return window.matchMedia('(prefers-color-scheme: dark)').matches ?
'dark' :
'light'
}

const themeOptions: {
value: Theme
icon: typeof Sun
en: string
zh: string
}[] = [
{ value: 'light', icon: Sun, en: 'Light', zh: '浅色' },
{ value: 'dark', icon: Moon, en: 'Dark', zh: '深色' },
]
const themeOrder: ThemeMode[] = ['dark', 'light', 'system']

export function ThemeToggle({ locale }: { locale: 'en' | 'zh' }) {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined')
return 'light'
const t = getTranslations(locale)
const [theme, setThemeState] = useState<ThemeMode>('system')

useEffect(() => {
const saved = localStorage.getItem(STORAGE_KEY)
if (saved === 'light' || saved === 'dark')
return saved
return getSystemTheme()
})
if (saved === 'light' || saved === 'dark' || saved === 'system')
setThemeState(saved)
}, [])

useEffect(() => {
document.documentElement.classList.toggle('dark', theme === 'dark')
const media = window.matchMedia('(prefers-color-scheme: dark)')
const applyTheme = () => {
const resolvedTheme = theme === 'system' ? (media.matches ? 'dark' : 'light') : theme
document.documentElement.classList.toggle('dark', resolvedTheme === 'dark')
}

applyTheme()
localStorage.setItem(STORAGE_KEY, theme)
media.addEventListener('change', applyTheme)
return () => media.removeEventListener('change', applyTheme)
}, [theme])

const nextTheme = useMemo(() => {
const index = themeOrder.indexOf(theme)
return themeOrder[(index + 1) % themeOrder.length]
}, [theme])

const setTheme = useCallback((mode: Theme) => {
setThemeState(mode)
const currentLabel = t.theme[theme]
const nextLabel = t.theme[nextTheme]

const setTheme = useCallback(() => {
setThemeState((current) => {
const index = themeOrder.indexOf(current)
return themeOrder[(index + 1) % themeOrder.length]
})
}, [])

const Icon = theme === 'dark' ? Moon : theme === 'light' ? Sun : SunMoon

return (
<DropdownMenu>
<DropdownMenuTrigger
className="inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg outline-none hover:bg-muted hover:text-foreground size-7"
aria-label="Toggle color theme"
>
{theme === 'dark' ?
(
<Moon className="h-4 w-4" />
) :
(
<Sun className="h-4 w-4" />
)}
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{themeOptions.map((opt) => {
const Icon = opt.icon
return (
<DropdownMenuItem
key={opt.value}
onClick={() => setTheme(opt.value)}
>
<Icon className="h-4 w-4" />
{opt[locale]}
{theme === opt.value && <Check className="ml-auto h-4 w-4" />}
</DropdownMenuItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
onClick={setTheme}
className="inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-lg outline-none transition-colors hover:bg-muted hover:text-foreground"
aria-label={`${currentLabel} theme. Click to switch to ${nextLabel}.`}
title={`${currentLabel} -> ${nextLabel}`}
>
<Icon className="h-4 w-4" />
</button>
)
}
14 changes: 7 additions & 7 deletions apps/frontend/src/components/ToolDetailPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ return null

<Base title={`${txt(meta.title, locale)} - ${pageTitle}`} description={txt(meta.description, locale)} locale={locale}>
<section class="relative z-10">
<div class="mx-auto w-full max-w-5xl px-4 pb-14 pt-8 sm:px-6 sm:pt-10 lg:px-8">
<div class="pb-8 text-center">
<div class="mx-auto w-full max-w-5xl px-4 pb-8 pt-8 sm:px-6 sm:pt-10 lg:px-8">
<div class="pb-4 text-center">
<h1 class="text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
{txt(meta.title, locale)}
</h1>
Expand Down Expand Up @@ -149,16 +149,16 @@ return null
)}
</div>

<div class="flex items-center justify-center gap-1.5 py-5">
<div class="flex items-center justify-center gap-1.5 py-3">
<span class="h-1 w-1 rounded-full bg-border"></span>
<span class="h-1 w-1 rounded-full bg-border"></span>
<span class="h-1 w-1 rounded-full bg-border"></span>
</div>

<div class="mt-6 flex items-start justify-center">
<div class="mt-2 flex items-start justify-center">
{sidebarDocs.length > 1 && (
<aside class="hidden w-[140px] flex-none self-stretch lg:block">
<nav class="sticky top-28 space-y-0.5 border-r border-border/40 pr-4 text-sm">
<nav class="sticky top-28 space-y-0.5 pr-4 text-sm">
{sidebarDocs.map(doc => (
<a
href={doc.href}
Expand All @@ -174,7 +174,7 @@ return null
</aside>
)}

<div class:list={[sidebarDocs.length > 1 && 'min-w-0 lg:pl-5']}>
<div class:list={['w-full min-w-0', sidebarDocs.length > 1 && 'lg:flex-1 lg:pl-5']}>
{sidebarDocs.length > 1 && (
<nav class="mb-8 flex flex-wrap gap-1.5 lg:hidden">
{sidebarDocs.map(doc => (
Expand Down Expand Up @@ -211,7 +211,7 @@ return null
</div>
<div class="min-w-0">
<div class="text-sm font-semibold text-foreground">{item.title}</div>
<p class="mt-1 text-sm leading-6 text-muted-foreground">{item.description}</p>
<p class="mt-1 line-clamp-2 text-[13px] leading-relaxed text-muted-foreground">{item.description}</p>
</div>
</div>
</a>
Expand Down
25 changes: 25 additions & 0 deletions apps/frontend/src/components/ToolDirectoryPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface Props {
metaTitle?: string
metaDescription?: string
initialTag?: string
allPath?: string
maxVisibleTags?: number
detectLocale?: boolean
}

Expand All @@ -24,6 +26,8 @@ const {
metaTitle,
metaDescription,
initialTag,
allPath = '/',
maxVisibleTags,
detectLocale = false,
} = Astro.props

Expand Down Expand Up @@ -52,6 +56,25 @@ const allTags = toolsData.tags.map(tag => ({
key: tag.key,
label: txt(tag, locale),
}))

const tagCounts = new Map<string, number>()
for (const item of toolItems) {
for (const tag of item.tags ?? []) {
tagCounts.set(tag.key, (tagCounts.get(tag.key) ?? 0) + 1)
}
}

const visibleTags = maxVisibleTags
? allTags
.filter(tag => (tagCounts.get(tag.key) ?? 0) > 0)
.sort((a, b) => {
const diff = (tagCounts.get(b.key) ?? 0) - (tagCounts.get(a.key) ?? 0)
if (diff !== 0)
return diff
return allTags.findIndex(item => item.key === a.key) - allTags.findIndex(item => item.key === b.key)
})
.slice(0, maxVisibleTags)
: allTags
---

<Base
Expand All @@ -75,7 +98,9 @@ const allTags = toolsData.tags.map(tag => ({
locale={locale}
tools={toolItems}
allTags={allTags}
visibleTags={visibleTags}
initialTag={initialTag}
allPath={allPath}
translations={{
searchPlaceholder: t.hero.searchPlaceholder,
all: t.filters.all,
Expand Down
38 changes: 17 additions & 21 deletions apps/frontend/src/components/ToolSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
Tag,
} from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import type { Locale } from '@/i18n'
Expand Down Expand Up @@ -50,7 +49,9 @@ interface Props {
locale: Locale
tools: ToolItem[]
allTags: ToolTag[]
visibleTags?: ToolTag[]
initialTag?: string
allPath?: string
translations: {
searchPlaceholder: string
all: string
Expand All @@ -66,7 +67,7 @@ interface Props {
}
}

export function ToolSearch({ locale, tools, allTags, initialTag, translations }: Props) {
export function ToolSearch({ locale, tools, allTags, visibleTags, initialTag, allPath = '/', translations }: Props) {
const [query, setQuery] = useState('')
const [page, setPage] = useState(1)
const [activeTag, setActiveTag] = useState(initialTag ?? '')
Expand Down Expand Up @@ -112,7 +113,7 @@ export function ToolSearch({ locale, tools, allTags, initialTag, translations }:
if (newTag) {
window.history.replaceState(null, '', localizePath(locale, `/tags/${newTag}`))
} else {
window.history.replaceState(null, '', localizePath(locale, '/'))
window.history.replaceState(null, '', localizePath(locale, allPath))
}
}

Expand All @@ -131,26 +132,21 @@ export function ToolSearch({ locale, tools, allTags, initialTag, translations }:

{allTags.length > 0 && (
<div className="mt-8 flex flex-wrap items-center justify-center gap-1.5 text-sm">
<Button
size="default"
variant={activeTag === '' ? 'default' : 'outline'}
className="rounded-full px-3"
<Tag className="h-3.5 w-3.5 text-muted-foreground/50" />
<button
type="button"
onClick={() => handleTagChange('')}
className={cn(
'inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs transition-colors',
activeTag === '' ?
'border-foreground/20 bg-foreground/10 text-foreground' :
'border-border/40 text-muted-foreground hover:border-border hover:text-foreground',
)}
>
<span className="text-[13px]">{translations.all}</span>
<Badge
variant={activeTag === '' ? 'secondary' : 'outline'}
className={cn(
'min-w-5 justify-center px-1.5 py-0 text-[10px]',
activeTag === '' &&
'border-transparent bg-white/15 text-white dark:bg-white/10',
)}
>
{tools.length}
</Badge>
</Button>
<Tag className="h-3.5 w-3.5 text-muted-foreground/50" />
{allTags.map((tag) => {
{translations.all}
<span className="text-[10px] opacity-60">{tools.length}</span>
</button>
{(visibleTags ?? allTags).map((tag) => {
const count = tagCounts.get(tag.key) ?? 0
if (count === 0)
return null
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const en: Translations = {
theme: {
light: 'Light',
dark: 'Dark',
system: 'System',
},
toolDetail: {
configuration: 'Configuration',
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface Translations {
theme: {
light: string
dark: string
system: string
}
toolDetail: {
configuration: string
Expand Down
Loading