import { getTranslations, setRequestLocale } from 'next-intl/server'; import { Header } from '@/components/Header'; import { Footer } from '@/components/Footer'; import { createDb, skillQueries, categoryQueries } from '@skillhub/db'; import { BrowseFilters, SearchBar, Pagination, ActiveFilters, EmptyState } from '@/components/BrowseFilters'; import { SkillCard } from '@/components/SkillCard'; import { toPersianNumber } from '@/lib/format-number'; import { getPageAlternates } from '@/lib/seo'; import { getOrSetCache, cacheKeys, cacheTTL, hashSearchParams } from '@/lib/cache'; // Force dynamic rendering to fetch fresh data from database export const dynamic = 'force-dynamic'; interface BrowsePageProps { params: Promise<{ locale: string }>; searchParams: Promise<{ q?: string; category?: string; platform?: string; format?: string; sort?: string; page?: string; }>; } // Get skills directly from database with filters - all filtering at database level // Results are cached in Redis for 30 minutes, keyed by a hash of all filter parameters async function getSkills(params: { q?: string; platform?: string; format?: string; sort?: string; page?: string; category?: string; }) { try { const limit = 20; const page = parseInt(params.page || '1'); const hash = hashSearchParams({ q: params.q, category: params.category, platform: params.platform && params.platform !== 'all' ? params.platform : undefined, format: params.format || 'skill.md', sort: params.sort || 'lastDownloaded', page, }); return await getOrSetCache( cacheKeys.searchSkills(hash), cacheTTL.search, async () => { const db = createDb(); const offset = (page - 1) * limit; const sortMap: Record = { 'stars': 'stars', 'downloads': 'downloads', 'recent': 'updated', 'rating': 'rating', 'lastDownloaded': 'lastDownloaded', }; const filterOptions = { query: params.q, category: params.category, platform: params.platform && params.platform !== 'all' ? params.platform : undefined, sourceFormat: params.format || 'skill.md', sortBy: sortMap[params.sort || 'lastDownloaded'] || 'lastDownloaded', sortOrder: 'desc' as const, limit, offset, }; const [skills, total] = await Promise.all([ skillQueries.search(db, filterOptions), skillQueries.count(db, { query: params.q, category: params.category, platform: params.platform && params.platform !== 'all' ? params.platform : undefined, sourceFormat: params.format || 'skill.md', }), ]); const totalPages = Math.ceil(total / limit); return { skills, pagination: { total, page, totalPages }, }; } ); } catch (error) { console.error('Error fetching skills:', error); return { skills: [], pagination: { total: 0, page: 1, totalPages: 1 } }; } } export async function generateMetadata({ params, }: { params: Promise<{ locale: string }>; }) { const { locale } = await params; return { alternates: getPageAlternates(locale, '/browse'), }; } export default async function BrowsePage({ params, searchParams }: BrowsePageProps) { const { locale } = await params; setRequestLocale(locale); const searchParamsResolved = await searchParams; const t = await getTranslations('browse'); const tCommon = await getTranslations('common'); const sortOptions = [ { id: 'lastDownloaded', name: t('filters.sortOptions.lastDownloaded') }, { id: 'downloads', name: t('filters.sortOptions.downloads') }, { id: 'stars', name: t('filters.sortOptions.stars') }, { id: 'recent', name: t('filters.sortOptions.recent') }, { id: 'rating', name: t('filters.sortOptions.rating') }, ]; // Fetch categories hierarchically for filter dropdown with translations (cached 12h) const tCategories = await getTranslations('categories'); type HierarchicalCategory = { id: string; name: string; slug: string; skillCount: number; children?: { id: string; name: string; slug: string; skillCount: number }[]; }; let categories: HierarchicalCategory[] = []; try { const rawCategories = await getOrSetCache( cacheKeys.categoriesHierarchical(), cacheTTL.categories, async () => { const db = createDb(); return await categoryQueries.getHierarchical(db); } ); categories = rawCategories.map(parent => ({ id: parent.id, name: tCategories(`parents.${parent.slug}`) || parent.name, slug: parent.slug, skillCount: parent.skillCount ?? 0, children: parent.children?.map(cat => ({ id: cat.id, name: tCategories(`names.${cat.slug}`) || cat.name, slug: cat.slug, skillCount: cat.skillCount ?? 0, })), })); } catch (error) { console.error('Error fetching categories:', error); } const filterTranslations = { category: t('filters.category') || 'Category', allCategories: t('filters.allCategories') || 'All Categories', sort: t('filters.sort'), format: t('filters.format') || 'Format', allFormats: t('filters.allFormats') || 'All Formats', agentSkills: t('filters.agentSkills') || 'Agent Skills (SKILL.md)', searching: t('searching') || 'Searching...', viewFeatured: t('filters.viewFeatured') || 'View Featured Skills', }; const paginationTranslations = { previous: t('pagination.previous') || 'Previous', next: t('pagination.next') || 'Next', page: t('pagination.page') || 'Page', of: t('pagination.of') || 'of', }; const activeFiltersTranslations = { search: t('activeFilters.search') || 'Search', category: t('activeFilters.category') || 'Category', sortBy: t('activeFilters.sortBy') || 'Sorted by', clearAll: t('activeFilters.clearAll') || 'Clear all', }; const emptyStateTranslations = { noResults: t('noResults') || 'No skills found', noResultsWithQuery: t('noResultsWithQuery', { query: searchParamsResolved.q || '' }), tryDifferent: t('emptyState.tryDifferent') || 'Try different search terms or adjust your filters', clearFilters: t('emptyState.clearFilters') || 'Clear filters', browseAll: t('emptyState.browseAll') || 'Browse Featured Skills', }; const searchPlaceholder = tCommon('search'); // Fetch skills from API with all filters const { skills, pagination } = await getSkills(searchParamsResolved); const limit = 20; const startItem = (pagination.page - 1) * limit + 1; const endItem = Math.min(pagination.page * limit, pagination.total); // Get category name for active filters display const currentCategory = searchParamsResolved.category; const currentSort = searchParamsResolved.sort || 'lastDownloaded'; const currentFormat = searchParamsResolved.format || ''; const hasActiveFilters = !!(searchParamsResolved.q || currentCategory || (currentSort && currentSort !== 'lastDownloaded') || currentFormat); // Find category name from hierarchical categories let categoryName = ''; if (currentCategory) { for (const parent of categories) { if (parent.id === currentCategory) { categoryName = parent.name; break; } const child = parent.children?.find(c => c.id === currentCategory); if (child) { categoryName = child.name; break; } } } // Find sort option name const sortName = sortOptions.find(o => o.id === currentSort)?.name || ''; return (
{/* Page Header */}

{t('title')}

{t('subtitle')}

{/* Filters Sidebar - Client Component */} {/* Skills Grid */}
{/* Search Bar - Client Component */} {/* Active Filters - Shows applied filters as removable chips */} {/* Results count with range */} {pagination.total > 0 && (

{t('resultsRange', { start: locale === 'fa' ? toPersianNumber(startItem) : startItem, end: locale === 'fa' ? toPersianNumber(endItem) : endItem, total: locale === 'fa' ? toPersianNumber(pagination.total) : pagination.total }) || `Showing ${startItem}-${endItem} of ${pagination.total} skills`}

)} {/* Skills Grid or Empty State */} {skills.length > 0 ? (
{skills.map((skill) => ( ))}
) : ( )} {/* Pagination */} {skills.length > 0 && ( )}
); }