Files
agent-park/src/app/[locale]/projects/page.tsx
T

210 lines
8.1 KiB
TypeScript

import { Suspense } from 'react'
import { getTranslations } from 'next-intl/server'
import {
getFixedProjectTypeFilters,
getProjects,
getTagCategoryGroups,
normalizeProjectSort,
} from '@/hooks/useProjects'
import { TagFilterPanel } from '@/components/project/TagFilterPanel'
import { ProjectsPageClient } from './ProjectsPageClient'
import { ProjectsResultsClient } from './ProjectsResultsClient'
import { isFixedProjectTypeSlug } from '@/lib/tag-taxonomy'
interface ProjectsPageProps {
params: Promise<{ locale: string }>
searchParams: Promise<{
search?: string
tag?: string
tags?: string
domain?: string
domains?: string
productForm?: string
productForms?: string
projectType?: string
sort?: string
page?: string
limit?: string
ai?: string
}>
}
const PAGE_LIMIT_OPTIONS = [10, 20, 50] as const
function normalizePageLimit(value?: string): (typeof PAGE_LIMIT_OPTIONS)[number] {
const parsedValue = Number(value)
if (PAGE_LIMIT_OPTIONS.includes(parsedValue as (typeof PAGE_LIMIT_OPTIONS)[number])) {
return parsedValue as (typeof PAGE_LIMIT_OPTIONS)[number]
}
return 10
}
export default async function ProjectsPage({
params,
searchParams,
}: ProjectsPageProps) {
const t = await getTranslations('home')
const tCommon = await getTranslations('common')
const tProject = await getTranslations('project')
const [resolvedParams, resolvedSearchParams] = await Promise.all([params, searchParams])
const { locale } = resolvedParams
const search = resolvedSearchParams.search || ''
const selectedTags = Array.from(
new Set(
[resolvedSearchParams.tag || '', ...(resolvedSearchParams.tags || '').split(',')]
.map((item) => item.trim())
.filter((item) => item.length > 0)
)
)
const selectedDomains = Array.from(
new Set(
[resolvedSearchParams.domain || '', ...(resolvedSearchParams.domains || '').split(',')]
.map((item) => item.trim())
.filter((item) => item.length > 0)
)
)
const selectedProductForms = Array.from(
new Set(
[resolvedSearchParams.productForm || '', ...(resolvedSearchParams.productForms || '').split(',')]
.map((item) => item.trim())
.filter((item) => item.length > 0)
)
)
const projectTypeParam = resolvedSearchParams.projectType || ''
const activeProjectType = isFixedProjectTypeSlug(projectTypeParam)
? projectTypeParam
: ''
const isAIMode = resolvedSearchParams.ai === '1'
const activeSort = normalizeProjectSort(resolvedSearchParams.sort)
const page = Number(resolvedSearchParams.page) || 1
const limit = normalizePageLimit(resolvedSearchParams.limit)
const [projectsData, projectTypeOptions, tagCategoryGroups] = await Promise.all([
getProjects({
search,
tags: selectedTags,
domains: selectedDomains,
productForms: selectedProductForms,
projectType: activeProjectType,
sort: activeSort,
page,
limit,
}),
getFixedProjectTypeFilters(),
getTagCategoryGroups(),
])
const domainCategoryGroup =
tagCategoryGroups.find((group) => group.category === 'DOMAIN_SCENARIO') || null
const productFormCategoryGroup =
tagCategoryGroups.find((group) => group.category === 'PRODUCT_FORM') || null
const normalTagCategoryGroups = tagCategoryGroups.filter(
(group) => group.category !== 'DOMAIN_SCENARIO' && group.category !== 'PRODUCT_FORM'
)
const activeProjectTypeMeta = projectTypeOptions.find((type) => type.slug === activeProjectType)
const activeProjectTypeLabel = activeProjectType
? locale === 'en'
? activeProjectTypeMeta?.nameEn || activeProjectType
: activeProjectTypeMeta?.name || activeProjectType
: tProject('allProjects')
return (
<main className="container mx-auto px-4 py-12 md:py-16 max-w-7xl">
{/* Search and Filter Section */}
<div className="bg-surface-light dark:bg-surface-dark border-2 border-black dark:border-white/20 p-6 md:p-8 mb-12 shadow-neo dark:shadow-none">
<Suspense fallback={<div className="h-20"></div>}>
<ProjectsPageClient
locale={locale}
isAI={isAIMode}
selectedTags={selectedTags}
selectedDomains={selectedDomains}
selectedProductForms={selectedProductForms}
projectType={activeProjectType}
sort={activeSort}
limit={limit}
searchPlaceholder={t('searchPlaceholder')}
searchLabel={tCommon('search')}
aiPlaceholder={t('aiSearchPlaceholder')}
aiLabel={tCommon('aiSearch')}
toggleToAI={tCommon('toggleToAI')}
toggleToTraditional={tCommon('toggleToTraditional')}
searching={tCommon('searching')}
showFilterPanel={tProject('showFilterPanel')}
hideFilterPanel={tProject('hideFilterPanel')}
>
{/* Filters - rendered inside client component for non-AI mode */}
<div className="border-t-2 border-gray-100 dark:border-gray-800 pt-6 mt-8">
<h3 className="font-display font-bold uppercase text-sm mb-4 border-b-2 border-black inline-block dark:border-primary pb-1">
{tProject('browseByFilters')}
</h3>
<TagFilterPanel
locale={locale}
search={search}
activeProjectType={activeProjectType}
selectedTags={selectedTags}
selectedDomains={selectedDomains}
selectedProductForms={selectedProductForms}
sort={activeSort}
projectTypeOptions={projectTypeOptions}
domainTags={domainCategoryGroup?.tags || []}
productFormTags={productFormCategoryGroup?.tags || []}
tagCategoryGroups={normalTagCategoryGroups}
translations={{
fixedProjectType: tProject('fixedProjectType'),
allTypes: tProject('allTypes'),
filterByDomain: tProject('filterByDomain'),
allDomains: tProject('allDomains'),
clearDomainFilters: tProject('clearDomainFilters'),
filterByProductForm: tProject('filterByProductForm'),
allProductForms: tProject('allProductForms'),
clearProductFormFilters: tProject('clearProductFormFilters'),
filterByTagCategory: tProject('filterByTagCategory'),
searchTagsPlaceholder: tProject('searchTagsPlaceholder'),
clearTagFilters: tProject('clearTagFilters'),
showMoreTags: tProject('showMoreTags'),
showLessTags: tProject('showLessTags'),
noMatchingTags: tProject('noMatchingTags'),
}}
/>
</div>
</ProjectsPageClient>
</Suspense>
</div>
<ProjectsResultsClient
locale={locale}
search={search}
isAI={isAIMode}
selectedTags={selectedTags}
selectedDomains={selectedDomains}
selectedProductForms={selectedProductForms}
projectType={activeProjectType}
sort={activeSort}
page={page}
activeProjectTypeLabel={activeProjectTypeLabel}
projects={projectsData.projects}
pagination={projectsData.pagination}
translations={{
allProjects: tProject('allProjects'),
sortLatest: tProject('sortLatest'),
sortStarsDesc: tProject('sortStarsDesc'),
sortStarsAsc: tProject('sortStarsAsc'),
itemsPerPage: tProject('itemsPerPage'),
paginationFirst: tProject('paginationFirst'),
paginationPrev: tProject('paginationPrev'),
paginationNext: tProject('paginationNext'),
paginationLast: tProject('paginationLast'),
viewDetails: tCommon('viewDetails'),
noProjects: tCommon('noProjects'),
noResults: tCommon('noResults'),
searching: tCommon('searching'),
aiSearchUnavailableFallback: tProject('aiSearchUnavailableFallback'),
aiSearchEmptyFallback: tProject('aiSearchEmptyFallback'),
}}
/>
</main>
)
}
export const revalidate = 300 // ISR: revalidate every 5 minutes