210 lines
8.1 KiB
TypeScript
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
|