Files
agent-park/src/hooks/useHome.ts
T

257 lines
5.9 KiB
TypeScript

import { prisma } from '@/lib/prisma'
import { getTopTags } from '@/hooks/useProjects'
import { Prisma } from '@prisma/client'
import { unstable_cache } from 'next/cache'
const ONE_DAY_MS = 24 * 60 * 60 * 1000
const DEFAULT_RANKING_LIMIT = 6
const DEFAULT_TIMELINE_LIMIT = 8
const DEFAULT_TOP_TAG_LIMIT = 12
const HOME_PAGE_REVALIDATE_SECONDS = 300
export type HomeProjectSummary = {
id: string
slug: string
name: string
nameEn: string | null
description: string
descriptionEn: string | null
githubStars: number
createdAt: string
tags: Array<{
id: string
name: string
nameEn: string | null
slug: string
}>
}
export type HomePageData = {
overview: {
totalProjects: number
newProjects30d: number
newProjects7d: number
newProjects24h: number
}
rankings: {
latestByWindow: {
'24h': HomeProjectSummary[]
'7d': HomeProjectSummary[]
'30d': HomeProjectSummary[]
}
topStars: HomeProjectSummary[]
}
tagInsights: {
topTags: Array<{
id: string
name: string
nameEn: string | null
slug: string
projectCount: number
}>
}
timeline: HomeProjectSummary[]
}
type ProjectWithRelations = Prisma.ProjectGetPayload<{
include: {
tags: {
include: {
tag: true
}
}
}
}>
async function safeQuery<T>(operationName: string, fallback: T, task: () => Promise<T>): Promise<T> {
try {
return await task()
} catch (error) {
console.error(
`[db] ${operationName} degraded to fallback:`,
error instanceof Error ? error.message : String(error)
)
return fallback
}
}
function mapProjectSummary(project: ProjectWithRelations): HomeProjectSummary {
return {
id: project.id,
slug: project.slug,
name: project.name,
nameEn: project.nameEn,
description: project.description,
descriptionEn: project.descriptionEn,
githubStars: project.githubStars,
createdAt: project.createdAt.toISOString(),
tags: project.tags.map((projectTag) => ({
id: projectTag.tag.id,
name: projectTag.tag.name,
nameEn: projectTag.tag.nameEn,
slug: projectTag.tag.slug,
})),
}
}
async function getLatestProjects(limit: number, createdAfter?: Date): Promise<HomeProjectSummary[]> {
const projects = await safeQuery('getLatestProjects', [] as ProjectWithRelations[], () =>
prisma.project.findMany({
where: {
status: 'ACTIVE',
...(createdAfter ? { createdAt: { gte: createdAfter } } : {}),
},
include: {
tags: {
include: {
tag: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
take: limit,
})
)
return projects.map(mapProjectSummary)
}
async function getTopStarsProjects(limit: number): Promise<HomeProjectSummary[]> {
const projects = await safeQuery('getTopStarsProjects', [] as ProjectWithRelations[], () =>
prisma.project.findMany({
where: {
status: 'ACTIVE',
},
include: {
tags: {
include: {
tag: true,
},
},
},
orderBy: [{ githubStars: 'desc' }, { createdAt: 'desc' }],
take: limit,
})
)
return projects.map(mapProjectSummary)
}
function getLatestProjectsByWindow(
projects: HomeProjectSummary[],
createdAfter: Date,
limit: number
): HomeProjectSummary[] {
return projects
.filter((project) => new Date(project.createdAt) >= createdAfter)
.slice(0, limit)
}
async function buildHomePageData(): Promise<HomePageData> {
const now = Date.now()
const last24Hours = new Date(now - ONE_DAY_MS)
const last7Days = new Date(now - ONE_DAY_MS * 7)
const last30Days = new Date(now - ONE_DAY_MS * 30)
const latestProjectsLimit = Math.max(DEFAULT_RANKING_LIMIT, DEFAULT_TIMELINE_LIMIT)
const [
totalProjects,
newProjects30d,
newProjects7d,
newProjects24h,
latestProjects,
topStars,
topTags,
] = await Promise.all([
safeQuery('countTotalProjects', 0, () => prisma.project.count()),
safeQuery('countNewProjects30d', 0, () =>
prisma.project.count({
where: {
status: 'ACTIVE',
createdAt: {
gte: last30Days,
},
},
})
),
safeQuery('countNewProjects7d', 0, () =>
prisma.project.count({
where: {
status: 'ACTIVE',
createdAt: {
gte: last7Days,
},
},
})
),
safeQuery('countNewProjects24h', 0, () =>
prisma.project.count({
where: {
status: 'ACTIVE',
createdAt: {
gte: last24Hours,
},
},
})
),
getLatestProjects(latestProjectsLimit),
getTopStarsProjects(DEFAULT_RANKING_LIMIT),
getTopTags(DEFAULT_TOP_TAG_LIMIT),
])
const latest24h = getLatestProjectsByWindow(
latestProjects,
last24Hours,
DEFAULT_RANKING_LIMIT
)
const latest7d = getLatestProjectsByWindow(
latestProjects,
last7Days,
DEFAULT_RANKING_LIMIT
)
const latest30d = getLatestProjectsByWindow(
latestProjects,
last30Days,
DEFAULT_RANKING_LIMIT
)
const timeline = latestProjects.slice(0, DEFAULT_TIMELINE_LIMIT)
return {
overview: {
totalProjects,
newProjects30d,
newProjects7d,
newProjects24h,
},
rankings: {
latestByWindow: {
'24h': latest24h,
'7d': latest7d,
'30d': latest30d,
},
topStars,
},
tagInsights: {
topTags: topTags.map((tag) => ({
id: tag.id,
name: tag.name,
nameEn: tag.nameEn,
slug: tag.slug,
projectCount: tag._count.projects,
})),
},
timeline,
}
}
const getCachedHomePageData = unstable_cache(buildHomePageData, ['home-page-data:v1'], {
revalidate: HOME_PAGE_REVALIDATE_SECONDS,
tags: ['home-page-data'],
})
export async function getHomePageData(): Promise<HomePageData> {
return getCachedHomePageData()
}