refactor: 调整首页统计与标签洞察展示
This commit is contained in:
@@ -56,10 +56,9 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
translations={{
|
translations={{
|
||||||
sectionTitle: t('insightsSection'),
|
sectionTitle: t('insightsSection'),
|
||||||
totalProjects: t('statsTotalProjects'),
|
totalProjects: t('statsTotalProjects'),
|
||||||
activeProjects: t('statsActiveProjects'),
|
newProjects30d: t('statsNewProjects30d'),
|
||||||
archivedProjects: t('statsArchivedProjects'),
|
|
||||||
totalTags: t('statsTotalTags'),
|
|
||||||
newProjects7d: t('statsNewProjects7d'),
|
newProjects7d: t('statsNewProjects7d'),
|
||||||
|
newProjects24h: t('statsNewProjects24h'),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -84,13 +83,8 @@ export default async function HomePage({ params }: HomePageProps) {
|
|||||||
<HomeTagInsights
|
<HomeTagInsights
|
||||||
locale={locale}
|
locale={locale}
|
||||||
topTags={homeData.tagInsights.topTags}
|
topTags={homeData.tagInsights.topTags}
|
||||||
categories={homeData.tagInsights.categoryDistribution}
|
|
||||||
translations={{
|
translations={{
|
||||||
sectionTitle: t('tagInsightsSection'),
|
sectionTitle: t('tagInsightsSection'),
|
||||||
topTagsTitle: t('topTagsTitle'),
|
|
||||||
categoryDistributionTitle: t('categoryDistributionTitle'),
|
|
||||||
tagsUnit: t('tagsUnit'),
|
|
||||||
projectsUnit: t('projectsUnit'),
|
|
||||||
noTagData: t('noTagData'),
|
noTagData: t('noTagData'),
|
||||||
viewAllProjects: t('viewAllProjects'),
|
viewAllProjects: t('viewAllProjects'),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -124,4 +124,59 @@
|
|||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
border: 1px solid rgba(255,255,255,0.6);
|
border: 1px solid rgba(255,255,255,0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-marquee::before,
|
||||||
|
.tag-marquee::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2rem;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-marquee::before {
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to right, rgba(245, 242, 235, 1), rgba(245, 242, 235, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-marquee::after {
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(to left, rgba(245, 242, 235, 1), rgba(245, 242, 235, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tag-marquee::before {
|
||||||
|
background: linear-gradient(to right, rgba(30, 30, 30, 1), rgba(30, 30, 30, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .tag-marquee::after {
|
||||||
|
background: linear-gradient(to left, rgba(30, 30, 30, 1), rgba(30, 30, 30, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-marquee-track {
|
||||||
|
animation-name: tag-marquee-x;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-marquee:hover .tag-marquee-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tag-marquee-x {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.tag-marquee-track {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ interface HomeOverviewStatsProps {
|
|||||||
locale: string
|
locale: string
|
||||||
stats: {
|
stats: {
|
||||||
totalProjects: number
|
totalProjects: number
|
||||||
activeProjects: number
|
newProjects30d: number
|
||||||
archivedProjects: number
|
|
||||||
totalTags: number
|
|
||||||
newProjects7d: number
|
newProjects7d: number
|
||||||
|
newProjects24h: number
|
||||||
}
|
}
|
||||||
translations: {
|
translations: {
|
||||||
sectionTitle: string
|
sectionTitle: string
|
||||||
totalProjects: string
|
totalProjects: string
|
||||||
activeProjects: string
|
newProjects30d: string
|
||||||
archivedProjects: string
|
|
||||||
totalTags: string
|
|
||||||
newProjects7d: string
|
newProjects7d: string
|
||||||
|
newProjects24h: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,10 +23,9 @@ function formatNumber(value: number, locale: string): string {
|
|||||||
export function HomeOverviewStats({ locale, stats, translations }: HomeOverviewStatsProps) {
|
export function HomeOverviewStats({ locale, stats, translations }: HomeOverviewStatsProps) {
|
||||||
const cards = [
|
const cards = [
|
||||||
{ label: translations.totalProjects, value: stats.totalProjects },
|
{ label: translations.totalProjects, value: stats.totalProjects },
|
||||||
{ label: translations.activeProjects, value: stats.activeProjects },
|
{ label: translations.newProjects30d, value: stats.newProjects30d },
|
||||||
{ label: translations.archivedProjects, value: stats.archivedProjects },
|
|
||||||
{ label: translations.totalTags, value: stats.totalTags },
|
|
||||||
{ label: translations.newProjects7d, value: stats.newProjects7d },
|
{ label: translations.newProjects7d, value: stats.newProjects7d },
|
||||||
|
{ label: translations.newProjects24h, value: stats.newProjects24h },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,7 +34,7 @@ export function HomeOverviewStats({ locale, stats, translations }: HomeOverviewS
|
|||||||
<h2 className="font-display text-3xl font-bold">{translations.sectionTitle}</h2>
|
<h2 className="font-display text-3xl font-bold">{translations.sectionTitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{cards.map((card) => (
|
{cards.map((card) => (
|
||||||
<article
|
<article
|
||||||
key={card.label}
|
key={card.label}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
import { Star } from 'lucide-react'
|
||||||
import type { HomeProjectSummary } from '@/hooks/useHome'
|
import type { HomeProjectSummary } from '@/hooks/useHome'
|
||||||
|
|
||||||
type RankingWindow = '24h' | '7d' | '30d'
|
type RankingWindow = '24h' | '7d' | '30d'
|
||||||
@@ -70,8 +71,12 @@ function ProjectRow({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showStars && (
|
{showStars && (
|
||||||
<span className="font-display text-xs font-bold shrink-0">
|
<span className="font-display text-xs font-bold shrink-0 inline-flex items-center gap-1.5">
|
||||||
★ {formatNumber(project.githubStars, locale)}
|
<Star
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-5 w-5 text-primary fill-current drop-shadow-[1px_1px_0_rgba(0,0,0,1)]"
|
||||||
|
/>
|
||||||
|
{formatNumber(project.githubStars, locale)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,13 +90,13 @@ export function HomeRankings({
|
|||||||
topStars,
|
topStars,
|
||||||
translations,
|
translations,
|
||||||
}: HomeRankingsProps) {
|
}: HomeRankingsProps) {
|
||||||
const [activeWindow, setActiveWindow] = useState<RankingWindow>('7d')
|
const [activeWindow, setActiveWindow] = useState<RankingWindow>('30d')
|
||||||
|
|
||||||
const windowOptions = useMemo(
|
const windowOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ key: '24h' as const, label: translations.window24h },
|
|
||||||
{ key: '7d' as const, label: translations.window7d },
|
|
||||||
{ key: '30d' as const, label: translations.window30d },
|
{ key: '30d' as const, label: translations.window30d },
|
||||||
|
{ key: '7d' as const, label: translations.window7d },
|
||||||
|
{ key: '24h' as const, label: translations.window24h },
|
||||||
],
|
],
|
||||||
[translations.window24h, translations.window7d, translations.window30d]
|
[translations.window24h, translations.window7d, translations.window30d]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { HomeCategoryDistribution } from '@/hooks/useHome'
|
|
||||||
|
|
||||||
interface HomeTagInsightsProps {
|
interface HomeTagInsightsProps {
|
||||||
locale: string
|
locale: string
|
||||||
@@ -10,13 +9,8 @@ interface HomeTagInsightsProps {
|
|||||||
slug: string
|
slug: string
|
||||||
projectCount: number
|
projectCount: number
|
||||||
}>
|
}>
|
||||||
categories: HomeCategoryDistribution[]
|
|
||||||
translations: {
|
translations: {
|
||||||
sectionTitle: string
|
sectionTitle: string
|
||||||
topTagsTitle: string
|
|
||||||
categoryDistributionTitle: string
|
|
||||||
tagsUnit: string
|
|
||||||
projectsUnit: string
|
|
||||||
noTagData: string
|
noTagData: string
|
||||||
viewAllProjects: string
|
viewAllProjects: string
|
||||||
}
|
}
|
||||||
@@ -30,10 +24,10 @@ function formatNumber(value: number, locale: string): string {
|
|||||||
export function HomeTagInsights({
|
export function HomeTagInsights({
|
||||||
locale,
|
locale,
|
||||||
topTags,
|
topTags,
|
||||||
categories,
|
|
||||||
translations,
|
translations,
|
||||||
}: HomeTagInsightsProps) {
|
}: HomeTagInsightsProps) {
|
||||||
const maxAssociations = Math.max(1, ...categories.map((category) => category.projectAssociationCount))
|
const marqueeItems = [...topTags, ...topTags]
|
||||||
|
const animationDurationSeconds = Math.max(18, topTags.length * 4)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
@@ -41,16 +35,18 @@ export function HomeTagInsights({
|
|||||||
<h2 className="font-display text-3xl font-bold">{translations.sectionTitle}</h2>
|
<h2 className="font-display text-3xl font-bold">{translations.sectionTitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
<article className="border-2 border-black dark:border-gray-600 bg-surface-light dark:bg-surface-dark p-5 shadow-neo">
|
||||||
<article className="border-2 border-black dark:border-gray-600 bg-surface-light dark:bg-surface-dark p-5 shadow-neo">
|
{topTags.length > 0 ? (
|
||||||
<h3 className="font-display text-xl font-bold mb-4">{translations.topTagsTitle}</h3>
|
<div className="tag-marquee relative overflow-hidden">
|
||||||
{topTags.length > 0 ? (
|
<div
|
||||||
<div className="flex flex-wrap gap-2">
|
className="tag-marquee-track flex w-max items-center gap-2 pr-2"
|
||||||
{topTags.map((tag) => (
|
style={{ animationDuration: `${animationDurationSeconds}s` }}
|
||||||
|
>
|
||||||
|
{marqueeItems.map((tag, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={tag.id}
|
key={`${tag.id}-${index}`}
|
||||||
href={`/${locale}/projects?tag=${tag.slug}`}
|
href={`/${locale}/projects?tag=${tag.slug}`}
|
||||||
className="px-3 py-2 border-2 border-black dark:border-gray-600 bg-white dark:bg-surface-dark text-xs font-display font-bold uppercase shadow-neo-sm hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all"
|
className="shrink-0 px-3 py-2 border-2 border-black dark:border-gray-600 bg-white dark:bg-surface-dark text-xs font-display font-bold uppercase shadow-neo-sm hover:shadow-none hover:translate-x-[2px] hover:translate-y-[2px] transition-all"
|
||||||
>
|
>
|
||||||
<span>{locale === 'en' && tag.nameEn ? tag.nameEn : tag.name}</span>
|
<span>{locale === 'en' && tag.nameEn ? tag.nameEn : tag.name}</span>
|
||||||
<span className="ml-2 text-gray-500 dark:text-gray-400">
|
<span className="ml-2 text-gray-500 dark:text-gray-400">
|
||||||
@@ -59,67 +55,20 @@ export function HomeTagInsights({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">{translations.noTagData}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/projects`}
|
|
||||||
className="inline-flex items-center font-display text-sm font-bold hover:underline"
|
|
||||||
>
|
|
||||||
{translations.viewAllProjects} <span className="ml-1">→</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{translations.noTagData}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<article className="border-2 border-black dark:border-gray-600 bg-surface-light dark:bg-surface-dark p-5 shadow-neo">
|
<div className="mt-4">
|
||||||
<h3 className="font-display text-xl font-bold mb-4">{translations.categoryDistributionTitle}</h3>
|
<Link
|
||||||
{categories.length > 0 ? (
|
href={`/${locale}/projects`}
|
||||||
<div className="space-y-4">
|
className="inline-flex items-center font-display text-sm font-bold hover:underline"
|
||||||
{categories.map((category) => {
|
>
|
||||||
const categoryName =
|
{translations.viewAllProjects} <span className="ml-1">→</span>
|
||||||
locale === 'en' ? category.nameEn : category.name
|
</Link>
|
||||||
const widthPercent = Math.max(
|
</div>
|
||||||
6,
|
</article>
|
||||||
Math.round((category.projectAssociationCount / maxAssociations) * 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={category.category} className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<p className="font-display text-sm font-bold uppercase">{categoryName}</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{formatNumber(category.tagCount, locale)} {translations.tagsUnit} ·{' '}
|
|
||||||
{formatNumber(category.projectAssociationCount, locale)} {translations.projectsUnit}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="h-3 border-2 border-black dark:border-gray-600 bg-white dark:bg-gray-800">
|
|
||||||
<div
|
|
||||||
className="h-full bg-primary"
|
|
||||||
style={{ width: `${widthPercent}%` }}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{category.topTag && (
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/projects?tag=${category.topTag.slug}`}
|
|
||||||
className="inline-flex text-xs font-display font-bold hover:underline"
|
|
||||||
>
|
|
||||||
{locale === 'en' && category.topTag.nameEn
|
|
||||||
? category.topTag.nameEn
|
|
||||||
: category.topTag.name}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">{translations.noTagData}</p>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-60
@@ -1,5 +1,5 @@
|
|||||||
import { prisma } from '@/lib/prisma'
|
import { prisma } from '@/lib/prisma'
|
||||||
import { getTagCategoryGroups, getTopTags } from '@/hooks/useProjects'
|
import { getTopTags } from '@/hooks/useProjects'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
||||||
@@ -24,28 +24,12 @@ export type HomeProjectSummary = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HomeCategoryDistribution = {
|
|
||||||
category: string
|
|
||||||
name: string
|
|
||||||
nameEn: string
|
|
||||||
tagCount: number
|
|
||||||
projectAssociationCount: number
|
|
||||||
topTag: {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
nameEn: string | null
|
|
||||||
slug: string
|
|
||||||
projectCount: number
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HomePageData = {
|
export type HomePageData = {
|
||||||
overview: {
|
overview: {
|
||||||
totalProjects: number
|
totalProjects: number
|
||||||
activeProjects: number
|
newProjects30d: number
|
||||||
archivedProjects: number
|
|
||||||
totalTags: number
|
|
||||||
newProjects7d: number
|
newProjects7d: number
|
||||||
|
newProjects24h: number
|
||||||
}
|
}
|
||||||
rankings: {
|
rankings: {
|
||||||
latestByWindow: {
|
latestByWindow: {
|
||||||
@@ -63,7 +47,6 @@ export type HomePageData = {
|
|||||||
slug: string
|
slug: string
|
||||||
projectCount: number
|
projectCount: number
|
||||||
}>
|
}>
|
||||||
categoryDistribution: HomeCategoryDistribution[]
|
|
||||||
}
|
}
|
||||||
timeline: HomeProjectSummary[]
|
timeline: HomeProjectSummary[]
|
||||||
}
|
}
|
||||||
@@ -162,29 +145,23 @@ export async function getHomePageData(): Promise<HomePageData> {
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
totalProjects,
|
totalProjects,
|
||||||
activeProjects,
|
newProjects30d,
|
||||||
archivedProjects,
|
|
||||||
totalTags,
|
|
||||||
newProjects7d,
|
newProjects7d,
|
||||||
|
newProjects24h,
|
||||||
latest24h,
|
latest24h,
|
||||||
latest7d,
|
latest7d,
|
||||||
latest30d,
|
latest30d,
|
||||||
topStars,
|
topStars,
|
||||||
timeline,
|
timeline,
|
||||||
topTags,
|
topTags,
|
||||||
tagCategoryGroups,
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
safeQuery('countTotalProjects', 0, () => prisma.project.count()),
|
safeQuery('countTotalProjects', 0, () => prisma.project.count()),
|
||||||
safeQuery('countActiveProjects', 0, () => prisma.project.count({ where: { status: 'ACTIVE' } })),
|
safeQuery('countNewProjects30d', 0, () =>
|
||||||
safeQuery('countArchivedProjects', 0, () => prisma.project.count({ where: { status: 'ARCHIVED' } })),
|
prisma.project.count({
|
||||||
safeQuery('countTotalTags', 0, () =>
|
|
||||||
prisma.tag.count({
|
|
||||||
where: {
|
where: {
|
||||||
category: {
|
status: 'ACTIVE',
|
||||||
not: 'FIXED_PROJECT_TYPE',
|
createdAt: {
|
||||||
},
|
gte: last30Days,
|
||||||
projects: {
|
|
||||||
some: {},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -199,44 +176,30 @@ export async function getHomePageData(): Promise<HomePageData> {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
safeQuery('countNewProjects24h', 0, () =>
|
||||||
|
prisma.project.count({
|
||||||
|
where: {
|
||||||
|
status: 'ACTIVE',
|
||||||
|
createdAt: {
|
||||||
|
gte: last24Hours,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
getLatestProjects(DEFAULT_RANKING_LIMIT, last24Hours),
|
getLatestProjects(DEFAULT_RANKING_LIMIT, last24Hours),
|
||||||
getLatestProjects(DEFAULT_RANKING_LIMIT, last7Days),
|
getLatestProjects(DEFAULT_RANKING_LIMIT, last7Days),
|
||||||
getLatestProjects(DEFAULT_RANKING_LIMIT, last30Days),
|
getLatestProjects(DEFAULT_RANKING_LIMIT, last30Days),
|
||||||
getTopStarsProjects(DEFAULT_RANKING_LIMIT),
|
getTopStarsProjects(DEFAULT_RANKING_LIMIT),
|
||||||
getLatestProjects(DEFAULT_TIMELINE_LIMIT),
|
getLatestProjects(DEFAULT_TIMELINE_LIMIT),
|
||||||
getTopTags(DEFAULT_TOP_TAG_LIMIT),
|
getTopTags(DEFAULT_TOP_TAG_LIMIT),
|
||||||
getTagCategoryGroups(),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const categoryDistribution: HomeCategoryDistribution[] = tagCategoryGroups.map((group) => {
|
|
||||||
const projectAssociationCount = group.tags.reduce((sum, tag) => sum + tag._count.projects, 0)
|
|
||||||
const leadingTag = group.tags[0]
|
|
||||||
|
|
||||||
return {
|
|
||||||
category: group.category,
|
|
||||||
name: group.name,
|
|
||||||
nameEn: group.nameEn,
|
|
||||||
tagCount: group.tags.length,
|
|
||||||
projectAssociationCount,
|
|
||||||
topTag: leadingTag
|
|
||||||
? {
|
|
||||||
id: leadingTag.id,
|
|
||||||
name: leadingTag.name,
|
|
||||||
nameEn: leadingTag.nameEn,
|
|
||||||
slug: leadingTag.slug,
|
|
||||||
projectCount: leadingTag._count.projects,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
overview: {
|
overview: {
|
||||||
totalProjects,
|
totalProjects,
|
||||||
activeProjects,
|
newProjects30d,
|
||||||
archivedProjects,
|
|
||||||
totalTags,
|
|
||||||
newProjects7d,
|
newProjects7d,
|
||||||
|
newProjects24h,
|
||||||
},
|
},
|
||||||
rankings: {
|
rankings: {
|
||||||
latestByWindow: {
|
latestByWindow: {
|
||||||
@@ -254,7 +217,6 @@ export async function getHomePageData(): Promise<HomePageData> {
|
|||||||
slug: tag.slug,
|
slug: tag.slug,
|
||||||
projectCount: tag._count.projects,
|
projectCount: tag._count.projects,
|
||||||
})),
|
})),
|
||||||
categoryDistribution,
|
|
||||||
},
|
},
|
||||||
timeline,
|
timeline,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
"projectCount": "projects"
|
"projectCount": "projects"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "AI Project Navigator",
|
"title": "Agent Project Navigator",
|
||||||
"subtitle": "Discover and explore AI projects",
|
"subtitle": "Discover and explore AI projects",
|
||||||
"featuredProjects": "Featured Projects",
|
"featuredProjects": "Featured Projects",
|
||||||
"browseByTag": "Browse by Tag",
|
"browseByTag": "Browse by Tag",
|
||||||
"searchPlaceholder": "Search AI projects...",
|
"searchPlaceholder": "Search AI projects...",
|
||||||
"aiSearchPlaceholder": "Describe what you're looking for, e.g.: AI tools that can generate videos...",
|
"aiSearchPlaceholder": "Describe what you're looking for, e.g.: AI tools that can generate videos...",
|
||||||
"metaTitle": "Agent Park - AI Project Navigator",
|
"metaTitle": "Agent Park - Agent Project Navigator",
|
||||||
"metaDescription": "Discover and explore quality AI projects from across the web",
|
"metaDescription": "Discover and explore quality AI projects from across the web",
|
||||||
"heroTitle": "AI PROJECT",
|
"heroTitle": "AGENT PROJECT",
|
||||||
"heroTitle2": "NAVIGATOR",
|
"heroTitle2": "NAVIGATOR",
|
||||||
"heroDescription": "Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.",
|
"heroDescription": "Discover and explore high-quality AI projects from across the web. Curated for developers and enthusiasts.",
|
||||||
"browseByTagsSection": "BROWSE BY TAGS",
|
"browseByTagsSection": "BROWSE BY TAGS",
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
"statsTotalProjects": "Total Projects",
|
"statsTotalProjects": "Total Projects",
|
||||||
"statsActiveProjects": "Active Projects",
|
"statsActiveProjects": "Active Projects",
|
||||||
"statsArchivedProjects": "Archived Projects",
|
"statsArchivedProjects": "Archived Projects",
|
||||||
|
"statsNewProjects30d": "New in 30 Days",
|
||||||
|
"statsNewProjects24h": "New in 24h",
|
||||||
"statsTotalTags": "Total Tags",
|
"statsTotalTags": "Total Tags",
|
||||||
"statsNewProjects7d": "New in 7 Days",
|
"statsNewProjects7d": "New in 7 Days",
|
||||||
"rankingsSection": "Project Rankings",
|
"rankingsSection": "Project Rankings",
|
||||||
@@ -53,8 +55,8 @@
|
|||||||
"noStarsProjects": "No stars data available",
|
"noStarsProjects": "No stars data available",
|
||||||
"viewAllLatest": "View all latest projects",
|
"viewAllLatest": "View all latest projects",
|
||||||
"viewAllByStars": "View all by stars",
|
"viewAllByStars": "View all by stars",
|
||||||
"tagInsightsSection": "Tag Popularity & Distribution",
|
"tagInsightsSection": "Top Tags",
|
||||||
"topTagsTitle": "Top Tags",
|
"topTagsTitle": "Top Tags Carousel",
|
||||||
"categoryDistributionTitle": "Category Distribution",
|
"categoryDistributionTitle": "Category Distribution",
|
||||||
"tagsUnit": "tags",
|
"tagsUnit": "tags",
|
||||||
"projectsUnit": "projects",
|
"projectsUnit": "projects",
|
||||||
|
|||||||
@@ -22,15 +22,15 @@
|
|||||||
"projectCount": "个项目"
|
"projectCount": "个项目"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "AI 项目导航",
|
"title": "Agent 项目导航",
|
||||||
"subtitle": "发现和探索全网优质 AI 项目",
|
"subtitle": "发现和探索全网优质 AI 项目",
|
||||||
"featuredProjects": "精选项目",
|
"featuredProjects": "精选项目",
|
||||||
"browseByTag": "按标签浏览",
|
"browseByTag": "按标签浏览",
|
||||||
"searchPlaceholder": "搜索 AI 项目...",
|
"searchPlaceholder": "搜索 AI 项目...",
|
||||||
"aiSearchPlaceholder": "描述你想要的项目,如:能生成视频的 AI 工具...",
|
"aiSearchPlaceholder": "描述你想要的项目,如:能生成视频的 AI 工具...",
|
||||||
"metaTitle": "Agent Park - AI 项目导航",
|
"metaTitle": "Agent Park - Agent 项目导航",
|
||||||
"metaDescription": "发现和探索全网优质 AI 项目",
|
"metaDescription": "发现和探索全网优质 AI 项目",
|
||||||
"heroTitle": "AI 项目",
|
"heroTitle": "Agent 项目",
|
||||||
"heroTitle2": "导航",
|
"heroTitle2": "导航",
|
||||||
"heroDescription": "发现和探索来自全网的高质量 AI 项目。为开发者和爱好者精心策划。",
|
"heroDescription": "发现和探索来自全网的高质量 AI 项目。为开发者和爱好者精心策划。",
|
||||||
"browseByTagsSection": "按标签浏览",
|
"browseByTagsSection": "按标签浏览",
|
||||||
@@ -41,6 +41,8 @@
|
|||||||
"statsTotalProjects": "项目总数",
|
"statsTotalProjects": "项目总数",
|
||||||
"statsActiveProjects": "活跃项目",
|
"statsActiveProjects": "活跃项目",
|
||||||
"statsArchivedProjects": "归档项目",
|
"statsArchivedProjects": "归档项目",
|
||||||
|
"statsNewProjects30d": "近30天新增",
|
||||||
|
"statsNewProjects24h": "近24小时新增",
|
||||||
"statsTotalTags": "标签总数",
|
"statsTotalTags": "标签总数",
|
||||||
"statsNewProjects7d": "近7天新增",
|
"statsNewProjects7d": "近7天新增",
|
||||||
"rankingsSection": "项目榜单",
|
"rankingsSection": "项目榜单",
|
||||||
@@ -53,8 +55,8 @@
|
|||||||
"noStarsProjects": "暂无可展示的 Stars 数据",
|
"noStarsProjects": "暂无可展示的 Stars 数据",
|
||||||
"viewAllLatest": "查看全部最新项目",
|
"viewAllLatest": "查看全部最新项目",
|
||||||
"viewAllByStars": "查看全部 Stars 排序",
|
"viewAllByStars": "查看全部 Stars 排序",
|
||||||
"tagInsightsSection": "标签热度与分布",
|
"tagInsightsSection": "热门标签",
|
||||||
"topTagsTitle": "热门标签",
|
"topTagsTitle": "热门标签轮播",
|
||||||
"categoryDistributionTitle": "分类分布",
|
"categoryDistributionTitle": "分类分布",
|
||||||
"tagsUnit": "个标签",
|
"tagsUnit": "个标签",
|
||||||
"projectsUnit": "个项目",
|
"projectsUnit": "个项目",
|
||||||
|
|||||||
Reference in New Issue
Block a user