257 lines
5.9 KiB
TypeScript
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()
|
|
}
|