- Search your language
+ Search your language(s)
- Or select the programming language you would like to find
+ Or select one or more programming languages you would like to find
repositories for.
+
- {mainLanguages.map(language => (
-
- ))}
+ {mainLanguages.map(language => {
+ const id = `lang-${language}`;
+ const checked = selected.includes(language);
+ return (
+
+ );
+ })}
-
diff --git a/src/app/(public)/_components/search-form.tsx b/src/app/(public)/_components/search-form.tsx
index 9ceacc4..cb52e32 100644
--- a/src/app/(public)/_components/search-form.tsx
+++ b/src/app/(public)/_components/search-form.tsx
@@ -26,20 +26,20 @@ export function SearchForm() {
}
function onSubmit({ searchQuery }: FormValues) {
+ if (!pathname.startsWith('/repos')) return;
+
+ const reposPathname = pathname as `/repos/${string}`;
const trimmedQuery = searchQuery.trim();
if (trimmedQuery !== '') {
const sp = new URLSearchParams(searchParams);
sp.set('q', trimmedQuery);
- router.push(`${pathname}?${sp.toString()}`);
+ router.push(`${reposPathname}?${sp.toString()}`);
}
}
return (
-
);
-}
\ No newline at end of file
+}
diff --git a/src/app/(public)/repos/loading.tsx b/src/app/(public)/repos/loading.tsx
new file mode 100644
index 0000000..7206bf3
--- /dev/null
+++ b/src/app/(public)/repos/loading.tsx
@@ -0,0 +1,109 @@
+export default function Loading() {
+ return (
+ <>
+ {/* Header Skeleton */}
+
+
+
+
+ {/* Title Section Skeleton */}
+
+
+ {/* Sorter Skeleton */}
+
+
+ {/* Stars Filter Skeleton */}
+
+
+ {/* Repository Cards Grid Skeleton */}
+
+ {Array.from({ length: 21 }).map((_, index) => (
+
+ {/* Repository Title */}
+
+
+ {/* Language and Stats */}
+
+
+ {/* Topics */}
+
+
+ {/* Action Button */}
+
+
+ ))}
+
+
+
+ {/* Pagination Skeleton */}
+
+
+
+ {Array.from({ length: 5 }).map((_, index) => (
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/(public)/repos/page.tsx b/src/app/(public)/repos/page.tsx
new file mode 100644
index 0000000..3b03988
--- /dev/null
+++ b/src/app/(public)/repos/page.tsx
@@ -0,0 +1,157 @@
+import { env } from '@/env.mjs';
+import { notFound } from 'next/navigation';
+import { Header } from '@/app/(public)/_components/header';
+import { ScrollToTop } from './_components/scroll-to-top';
+import { RepoCard } from './_components/repo-card';
+import { Sorter } from './_components/sorter';
+import { StarsFilter } from './_components/stars-filter';
+import { Pagination } from './_components/pagination';
+import { auth } from '@/auth';
+import { db } from '@/lib/db/connection';
+import { accountsTable, reportsTable } from '@/lib/db/migrations/schema';
+import { eq } from 'drizzle-orm';
+import type { RepoResponse, RepoData, RepoItem, SearchParams } from '@/types';
+import { capitalize } from '@/lib/utils';
+
+export default async function ReposPage({
+ searchParams
+}: {
+ searchParams: Promise
;
+}) {
+ const sp = await searchParams;
+ const langs: string[] = Array.isArray(sp.l)
+ ? sp.l
+ : sp.l
+ ? [String(sp.l)]
+ : [];
+
+ const reposRes = await getRepos(langs, sp);
+ if (!reposRes) notFound();
+
+ const { repos, page } = reposRes;
+ const languagesList = langs
+ .map(lang => capitalize(decodeURIComponent(lang)))
+ .join(', ');
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {Intl.NumberFormat().format(repos.total_count)}
+ {' '}
+ repositories for{' '}
+
+ {sp.q ? sp.q + ' in ' + languagesList : languagesList}
+
+
+
+
+
+
+
+ {repos.items.map(repo => (
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
+
+async function getRepos(
+ languages: string[],
+ searchParams: SearchParams
+): Promise {
+ const session = await auth();
+ const {
+ p: page = '1',
+ s: sort = '',
+ o: order = 'desc',
+ q: searchQuery = '',
+ startStars = '1',
+ endStars = ''
+ } = searchParams;
+
+ const starsQuery =
+ startStars && endStars
+ ? `stars:${startStars}..${endStars}`
+ : startStars && !endStars
+ ? `stars:>${startStars}`
+ : !startStars && endStars
+ ? `stars:<${endStars}`
+ : '';
+
+ const combinedLangs = languages.map(l => `language:${l}`).join(' ');
+
+ const apiUrl = new URL('https://api.github.com/search/repositories');
+ apiUrl.searchParams.set('page', page.toString());
+ apiUrl.searchParams.set('per_page', '21');
+ apiUrl.searchParams.set('sort', sort.toString());
+ apiUrl.searchParams.set('order', order.toString());
+ apiUrl.searchParams.set(
+ 'q',
+ `topic:hacktoberfest ${combinedLangs} ${searchQuery} ${starsQuery}`
+ );
+
+ const headers: HeadersInit = {
+ Accept: 'application/vnd.github.mercy-preview+json'
+ };
+
+ const userId = session?.user?.id;
+
+ if (userId) {
+ const [account] = await db
+ .select()
+ .from(accountsTable)
+ .where(eq(accountsTable.userId, userId))
+ .limit(1);
+
+ if (account && account.access_token) {
+ headers.Authorization = `Bearer ${account.access_token}`;
+ } else if (env.AUTH_GITHUB_TOKEN) {
+ headers.Authorization = `Bearer ${env.AUTH_GITHUB_TOKEN}`;
+ }
+ } else if (env.AUTH_GITHUB_TOKEN) {
+ headers.Authorization = `Bearer ${env.AUTH_GITHUB_TOKEN}`;
+ }
+
+ const res = await fetch(apiUrl, { headers });
+ if (!res.ok) return undefined;
+
+ const repos = (await res.json()) as RepoData;
+ const reports = await getReportedRepos();
+
+ repos.items = repos.items.filter((repo: RepoItem) => {
+ return !repo.archived && !reports.find(report => report.repoId === repo.id);
+ });
+
+ return {
+ page: +page.toString(),
+ languageName: languages.join(', '),
+ repos
+ };
+}
+
+async function getReportedRepos() {
+ const reports = await db
+ .select()
+ .from(reportsTable)
+ .where(eq(reportsTable.valid, false))
+ .limit(100);
+
+ return reports;
+}