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
72 changes: 32 additions & 40 deletions apps/frontend/src/components/ToolDetailPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { Locale } from '@/i18n'
import { getTranslations } from '@/i18n'
import Base from '@/layouts/Base.astro'
import {
getToolBadge,
getToolDoc,
getToolDocs,
getToolMeta,
Expand All @@ -24,7 +23,6 @@ type RelatedToolCard = {
href: string
title: string
description: string
badge: string
logo: ReturnType<typeof resolveToolLogo>
}

Expand All @@ -36,13 +34,12 @@ const docSlug = parts[1] || 'index'
const t = getTranslations(locale)
const toolsData = getToolsJson()
const toolEntry = toolsData.list.find(item => item.slug === toolSlug)
const categoryLabel = toolEntry ? getToolBadge(toolsData, toolEntry.category, locale) : ''
const toolTags = toolEntry ? getToolTags(toolsData, toolEntry.tags ?? [], locale) : []

const meta = getToolMeta(toolSlug)
const { data: frontmatter, html: htmlContent } = await getToolDoc(toolSlug, docSlug, locale)
const meta = getToolMeta(toolSlug, toolsData)
const { data: frontmatter, html: htmlContent } = await getToolDoc(toolSlug, docSlug, locale, toolsData)

const allDocs = await getToolDocs(toolSlug, locale)
const allDocs = await getToolDocs(toolSlug, locale, toolsData)
const sidebarDocs = allDocs
.map(doc => ({
slug: doc.slug,
Expand Down Expand Up @@ -80,14 +77,13 @@ const linkedTools: RelatedToolCard[] = (meta.link ?? [])
if (!relatedEntry)
return null

const relatedMeta = getToolMeta(relatedSlug)
const relatedMeta = getToolMeta(relatedSlug, toolsData)
return {
slug: relatedSlug,
href: localizePath(locale, `/${relatedSlug}`),
title: txt(relatedMeta.title, locale),
description: txt(relatedMeta.description, locale),
badge: getToolBadge(toolsData, relatedEntry.category, locale),
logo: resolveToolLogo(relatedSlug, relatedEntry.logo),
logo: resolveToolLogo(relatedSlug, relatedEntry.logo, toolsData),
}
})
.filter((item): item is RelatedToolCard => item !== null)
Expand All @@ -101,9 +97,6 @@ return null
{txt(meta.title, locale)}
</h1>
<div class="mt-3 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{categoryLabel && toolEntry && (
<a href={localizePath(locale, `/category/${toolEntry.category}`)} class="rounded-full bg-foreground/5 px-2.5 py-0.5 font-medium text-foreground/70 transition-colors hover:bg-foreground/10 hover:text-foreground">{categoryLabel}</a>
)}
{toolTags.map(tag => (
<a href={localizePath(locale, `/tags/${tag.key}`)} class="rounded-full border border-border/40 px-2.5 py-0.5 font-medium text-foreground/50 transition-colors hover:border-border hover:text-foreground">{tag.label}</a>
))}
Expand Down Expand Up @@ -156,34 +149,6 @@ return null
)}
</div>

{linkedTools.length > 0 && (
<div class="mb-8 rounded-2xl border border-border/50 bg-white/80 p-4 shadow-sm dark:bg-white/[0.03] sm:p-5">
<div class="mb-3 text-sm font-semibold text-foreground">{t.toolDetail.link}</div>
<div class="grid gap-3 sm:grid-cols-2">
{linkedTools.map(item => (
<a href={item.href} class="rounded-xl border border-border/50 bg-background/60 p-4 transition-colors hover:border-border hover:bg-background">
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 flex-none items-center justify-center rounded-xl bg-foreground/5 text-sm font-semibold text-foreground/70">
{item.logo.type === 'svg' ?
(
<span class="h-6 w-6" set:html={item.logo.content} />
) :
(
<span>{item.title.charAt(0).toUpperCase()}</span>
)}
</div>
<div class="min-w-0">
<div class="inline-flex rounded-full bg-foreground/5 px-2 py-0.5 text-xs font-medium text-foreground/70">{item.badge}</div>
<div class="mt-2 text-sm font-semibold text-foreground">{item.title}</div>
<p class="mt-1 text-sm leading-6 text-muted-foreground">{item.description}</p>
</div>
</div>
</a>
))}
</div>
</div>
)}

<div class="flex items-center justify-center gap-1.5 py-5">
<span class="h-1 w-1 rounded-full bg-border"></span>
<span class="h-1 w-1 rounded-full bg-border"></span>
Expand Down Expand Up @@ -227,6 +192,33 @@ return null
)}

<article class="prose-article rounded-xl bg-white p-6 shadow-sm dark:bg-white/[0.03] sm:p-8" set:html={htmlContent} />

{linkedTools.length > 0 && (
<div class="mt-8 rounded-2xl border border-border/50 bg-white/80 p-4 shadow-sm dark:bg-white/[0.03] sm:p-5">
<div class="mb-3 text-sm font-semibold text-foreground">{t.toolDetail.link}</div>
<div class="grid gap-3 sm:grid-cols-2">
{linkedTools.map(item => (
<a href={item.href} class="rounded-xl border border-border/50 bg-background/60 p-4 transition-colors hover:border-border hover:bg-background">
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 flex-none items-center justify-center rounded-xl bg-foreground/5 text-sm font-semibold text-foreground/70">
{item.logo.type === 'svg' ?
(
<span class="h-6 w-6" set:html={item.logo.content} />
) :
(
<span>{item.title.charAt(0).toUpperCase()}</span>
)}
</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>
</div>
</div>
</a>
))}
</div>
</div>
)}
</div>
</div>
</div>
Expand Down
18 changes: 3 additions & 15 deletions apps/frontend/src/components/ToolDirectoryPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type { Locale } from '@/i18n'
import { getTranslations } from '@/i18n'
import Base from '@/layouts/Base.astro'
import {
getToolBadge,
getToolMeta,
getToolTags,
getToolsJson,
Expand All @@ -16,7 +15,6 @@ interface Props {
locale: Locale
metaTitle?: string
metaDescription?: string
initialCategory?: string
initialTag?: string
detectLocale?: boolean
}
Expand All @@ -25,7 +23,6 @@ const {
locale,
metaTitle,
metaDescription,
initialCategory,
initialTag,
detectLocale = false,
} = Astro.props
Expand All @@ -36,14 +33,12 @@ const toolsData = getToolsJson()
const toolItems = toolsData.list
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
.map((item) => {
const meta = getToolMeta(item.slug)
const logo = resolveToolLogo(item.slug, item.logo)
const meta = getToolMeta(item.slug, toolsData)
const logo = resolveToolLogo(item.slug, item.logo, toolsData)
const title = txt(meta.title, locale)

return {
slug: item.slug,
category: item.category,
badge: getToolBadge(toolsData, item.category, locale),
logo: logo.type === 'icon' ? item.logo! : logo.type === 'svg' ? 'custom' : '',
logoSvg: logo.type === 'svg' ? logo.content : undefined,
order: item.order,
Expand All @@ -53,11 +48,6 @@ const toolItems = toolsData.list
}
})

const categories = toolsData.categories.map(cat => ({
key: cat.key,
label: txt(cat, locale),
}))

const allTags = toolsData.tags.map(tag => ({
key: tag.key,
label: txt(tag, locale),
Expand All @@ -84,13 +74,11 @@ const allTags = toolsData.tags.map(tag => ({
client:load
locale={locale}
tools={toolItems}
categories={categories}
allTags={allTags}
initialCategory={initialCategory}
initialTag={initialTag}
translations={{
searchPlaceholder: t.hero.searchPlaceholder,
all: t.categories.all,
all: t.filters.all,
view: t.card.view,
noResults: t.card.noResults,
tags: t.card.tags,
Expand Down
74 changes: 14 additions & 60 deletions apps/frontend/src/components/ToolSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ interface ToolTag {

interface ToolItem {
slug: string
category: string
badge: string
logo: string
logoSvg?: string
order: number
Expand All @@ -37,11 +35,6 @@ interface ToolItem {
tags?: ToolTag[]
}

interface Category {
key: string
label: string
}

const iconMap: Record<string, LucideIcon> = {
'image': ImageIcon,
'braces': Braces,
Expand All @@ -56,9 +49,7 @@ const PAGE_SIZE = 12
interface Props {
locale: Locale
tools: ToolItem[]
categories: Category[]
allTags: ToolTag[]
initialCategory?: string
initialTag?: string
translations: {
searchPlaceholder: string
Expand All @@ -75,28 +66,13 @@ interface Props {
}
}

export function ToolSearch({ locale, tools, categories, allTags, initialCategory, initialTag, translations }: Props) {
export function ToolSearch({ locale, tools, allTags, initialTag, translations }: Props) {
const [query, setQuery] = useState('')
const [page, setPage] = useState(1)
const [activeCategory, setActiveCategory] = useState(initialCategory ?? 'all')
const [activeTag, setActiveTag] = useState(initialTag ?? '')

const normalizedQuery = query.trim().toLowerCase()

const allCategories: Category[] = [
{ key: 'all', label: translations.all },
...categories,
]

const categoryCounts = useMemo(() => {
const counts = new Map<string, number>()
counts.set('all', tools.length)
for (const cat of categories) {
counts.set(cat.key, tools.filter(t => t.category === cat.key).length)
}
return counts
}, [tools, categories])

const tagCounts = useMemo(() => {
const counts = new Map<string, number>()
for (const tag of allTags) {
Expand All @@ -107,17 +83,15 @@ export function ToolSearch({ locale, tools, categories, allTags, initialCategory

const filteredTools = useMemo(() => {
return tools.filter((tool) => {
const matchesCategory =
activeCategory === 'all' || tool.category === activeCategory
const matchesTag =
activeTag === '' || tool.tags?.some(t => t.key === activeTag)
const searchText =
`${tool.title} ${tool.description} ${tool.badge} ${(tool.tags ?? []).map(t => t.label).join(' ')}`.toLowerCase()
`${tool.title} ${tool.description} ${(tool.tags ?? []).map(t => t.label).join(' ')}`.toLowerCase()
const matchesQuery =
normalizedQuery.length === 0 || searchText.includes(normalizedQuery)
return matchesCategory && matchesTag && matchesQuery
return matchesTag && matchesQuery
})
}, [tools, activeCategory, activeTag, normalizedQuery])
}, [tools, activeTag, normalizedQuery])

const totalPages = Math.max(1, Math.ceil(filteredTools.length / PAGE_SIZE))
const currentPage = Math.min(page, totalPages)
Expand All @@ -131,25 +105,14 @@ export function ToolSearch({ locale, tools, categories, allTags, initialCategory
setPage(1)
}, [normalizedQuery])

function handleCategoryChange(key: string) {
setActiveCategory(key)
setActiveTag('')
setPage(1)
const url = key === 'all' ? localizePath(locale, '/') : localizePath(locale, `/category/${key}`)
window.history.replaceState(null, '', url)
}

function handleTagChange(key: string) {
const newTag = activeTag === key ? '' : key
setActiveTag(newTag)
setPage(1)
if (newTag) {
window.history.replaceState(null, '', localizePath(locale, `/tags/${newTag}`))
} else {
const url = activeCategory === 'all' ?
localizePath(locale, '/') :
localizePath(locale, `/category/${activeCategory}`)
window.history.replaceState(null, '', url)
window.history.replaceState(null, '', localizePath(locale, '/'))
}
}

Expand All @@ -166,32 +129,26 @@ export function ToolSearch({ locale, tools, categories, allTags, initialCategory
/>
</div>

<div className="mt-8 flex flex-wrap items-center justify-center gap-2 text-sm">
{allCategories.map(cat => (
{allTags.length > 0 && (
<div className="mt-8 flex flex-wrap items-center justify-center gap-1.5 text-sm">
<Button
key={cat.key}
size="default"
variant={activeCategory === cat.key ? 'default' : 'outline'}
variant={activeTag === '' ? 'default' : 'outline'}
className="rounded-full px-3"
onClick={() => handleCategoryChange(cat.key)}
onClick={() => handleTagChange('')}
>
<span className="text-[13px]">{cat.label}</span>
<span className="text-[13px]">{translations.all}</span>
<Badge
variant={activeCategory === cat.key ? 'secondary' : 'outline'}
variant={activeTag === '' ? 'secondary' : 'outline'}
className={cn(
'min-w-5 justify-center px-1.5 py-0 text-[10px]',
activeCategory === cat.key &&
activeTag === '' &&
'border-transparent bg-white/15 text-white dark:bg-white/10',
)}
>
{categoryCounts.get(cat.key)}
{tools.length}
</Badge>
</Button>
))}
</div>

{allTags.length > 0 && (
<div className="mt-3 flex flex-wrap items-center justify-center gap-1.5 text-sm">
<Tag className="h-3.5 w-3.5 text-muted-foreground/50" />
{allTags.map((tag) => {
const count = tagCounts.get(tag.key) ?? 0
Expand Down Expand Up @@ -264,10 +221,7 @@ export function ToolSearch({ locale, tools, categories, allTags, initialCategory
</div>
<div className="mt-auto flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-1">
<span className="text-xs text-muted-foreground/70">
{tool.badge}
</span>
{tool.tags?.slice(0, 3).map(tag => (
{tool.tags?.slice(0, 4).map(tag => (
<span
key={tag.key}
className="rounded-full bg-foreground/5 px-1.5 py-px text-[10px] text-muted-foreground/60"
Expand Down
8 changes: 1 addition & 7 deletions apps/frontend/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,8 @@ export const en: Translations = {
subtitle: 'Discover tools for modern builders. Ship faster.',
searchPlaceholder: 'Search...',
},
categories: {
filters: {
all: 'All',
images: 'Images',
textCode: 'Text & Code',
colors: 'Colors',
converters: 'Converters',
devTools: 'Dev Tools',
containers: 'Containers',
},
card: {
view: 'View',
Expand Down
8 changes: 1 addition & 7 deletions apps/frontend/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,8 @@ export interface Translations {
subtitle: string
searchPlaceholder: string
}
categories: {
filters: {
all: string
images: string
textCode: string
colors: string
converters: string
devTools: string
containers: string
}
card: {
view: string
Expand Down
Loading