import { getTranslations, setRequestLocale } from 'next-intl/server';
import { Header } from '@/components/Header';
import { Footer } from '@/components/Footer';
import { SkillCard } from '@/components/SkillCard';
import { Pagination } from '@/components/BrowseFilters';
import { createDb, skillQueries } from '@skillhub/db';
import { formatCompactNumber, toPersianNumber } from '@/lib/format-number';
import { ExternalLink, Download, Package, Eye, GitFork, ArrowUpDown, FolderGit2 } from 'lucide-react';
import Link from 'next/link';
export const dynamic = 'force-dynamic';
const ITEMS_PER_PAGE = 24;
type SortOption = 'popularity' | 'downloads' | 'stars';
interface OwnerPageProps {
params: Promise<{ locale: string; username: string }>;
searchParams: Promise<{ page?: string; sort?: string; repo?: string }>;
}
export default async function OwnerPage({ params, searchParams }: OwnerPageProps) {
const { locale, username: rawUsername } = await params;
const searchParamsResolved = await searchParams;
setRequestLocale(locale);
const t = await getTranslations('owner');
const tBrowse = await getTranslations('browse');
const username = decodeURIComponent(rawUsername);
const page = Math.max(1, parseInt(searchParamsResolved.page || '1'));
const sort = (['popularity', 'downloads', 'stars'].includes(searchParamsResolved.sort || '')
? searchParamsResolved.sort
: 'popularity') as SortOption;
const activeRepo = searchParamsResolved.repo || '';
const db = createDb();
// Fetch stats, count, and repo list in parallel
const [stats, totalSkills, ownerRepos] = await Promise.all([
skillQueries.getOwnerStats(db, username),
skillQueries.countByOwner(db, username, activeRepo || undefined),
skillQueries.getOwnerRepos(db, username),
]);
if (stats.totalSkills === 0) {
return (
{t('notFound')}
{t('notFoundDescription', { username })}
{t('browseAll')}
);
}
const offset = (page - 1) * ITEMS_PER_PAGE;
const totalPages = Math.ceil(totalSkills / ITEMS_PER_PAGE);
const skills = await skillQueries.getByOwner(db, username, {
limit: ITEMS_PER_PAGE,
offset,
sortBy: sort,
repo: activeRepo || undefined,
});
// Group fetched skills by repo for display
const repoMap = new Map();
for (const skill of skills) {
const repo = skill.githubRepo;
if (!repoMap.has(repo)) {
repoMap.set(repo, {
name: repo,
stars: skill.githubStars ?? 0,
skills: [],
});
}
repoMap.get(repo)!.skills.push(skill);
}
const repos = Array.from(repoMap.values());
const formatNum = (n: number) =>
locale === 'fa' ? toPersianNumber(formatCompactNumber(n, locale)) : formatCompactNumber(n, locale);
const startItem = offset + 1;
const endItem = Math.min(offset + ITEMS_PER_PAGE, totalSkills);
const paginationTranslations = {
previous: tBrowse('pagination.previous'),
next: tBrowse('pagination.next'),
page: tBrowse('pagination.page'),
of: tBrowse('pagination.of'),
};
const sortOptions: { value: SortOption; label: string }[] = [
{ value: 'popularity', label: t('sort.popularity') },
{ value: 'downloads', label: t('sort.downloads') },
{ value: 'stars', label: t('sort.stars') },
];
// Build URL helper preserving sort/repo params
const buildUrl = (overrides: { sort?: string; repo?: string; page?: number }) => {
const params = new URLSearchParams();
const s = overrides.sort ?? sort;
const r = overrides.repo ?? activeRepo;
const p = overrides.page ?? 1;
if (s && s !== 'popularity') params.set('sort', s);
if (r) params.set('repo', r);
if (p > 1) params.set('page', String(p));
const qs = params.toString();
return `/${locale}/owner/${rawUsername}${qs ? `?${qs}` : ''}`;
};
return (
{/* Owner Header */}
{t('title', { username })}
{/* Stats */}
{t('stats.skills', { count: stats.totalSkills })}
{formatNum(stats.totalDownloads)} {t('stats.downloads')}
{formatNum(stats.totalViews)} {t('stats.views')}
{t('stats.repos', { count: stats.totalRepos })}
{/* Repo filter chips (only if > 1 repo) */}
{ownerRepos.length > 1 && (
{t('repo.filter')}:
{t('repo.all')} ({locale === 'fa' ? toPersianNumber(stats.totalSkills) : stats.totalSkills})
{ownerRepos.map((r) => (
{r.repo} ({locale === 'fa' ? toPersianNumber(r.skillCount) : r.skillCount})
))}
)}
{/* Controls: Sort + Results count */}
{/* Results count */}
{t('pagination.showing', {
start: locale === 'fa' ? toPersianNumber(startItem) : startItem,
end: locale === 'fa' ? toPersianNumber(endItem) : endItem,
total: locale === 'fa' ? toPersianNumber(totalSkills) : totalSkills,
})}
{/* Sort selector */}
{t('sort.label')}:
{sortOptions.map((opt) => (
{opt.label}
))}
{/* Claim CTA - subtle inline hint */}
{t('claimCta')}
{t('claimButton')}
{/* Repos + Skills */}
{repos.map((repo) => (
{repo.skills.map((skill) => (
))}
))}
{/* Pagination */}
{/* Browse CTA - link to owner page and CLI for owners with long enough usernames */}
{t('installCta')}
{skills.length > 0 && (
npx skillhub install {skills[0].id}
)}
);
}