Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 85 additions & 38 deletions src/app/(public)/_components/hero.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use client';

import { useRouter } from 'next/navigation';
import { useMemo, useState } from 'react';

import { Search } from 'lucide-react';
import { LanguageButton } from './language-button';
import { Button } from './button';
import Link from 'next/link';

import { sortByName } from '@/lib/utils';
import languages from '@/assets/languages.json';
Expand All @@ -17,29 +16,107 @@ const { main: mainLanguages, others: otherLanguages } = languages;
export function Hero() {
const router = useRouter();

// Track selected languages as a string array
const [selected, setSelected] = useState<string[]>([]);

const toggleLanguage = (language: string) => {
setSelected(prev =>
prev.includes(language) ? prev.filter(l => l !== language) : [...prev, language]
);
};

const sortedOthers = useMemo(() => [...otherLanguages].sort(sortByName), []);

function handleSearch(e: React.FormEvent) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const lang = formData.get('search') as string;
if (lang.trim() === '') return;
router.push(`/repos/${lang}`);
let chosen = selected;

// Fallback: if no checkbox selected, use the single input value
if (chosen.length === 0) {
const typed = String(formData.get('search') || '').trim();
if (typed) {
chosen = [typed];
}
}

if (chosen.length === 0) return; // nothing to search

const csv = chosen.map(l => l.toLowerCase()).join(',');
router.push(`/repos?l=${encodeURIComponent(csv)}`);
}

return (
<div className="relative bg-hero-gradient ">
<div className="z-50 flex flex-col space-y-8 justify-center items-center text-center min-h-screen pt-28 sm:pt-24">
<div className="max-w-md space-y-5 px-4">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-medium uppercase heading-text">
Search your language
Search your language(s)
</h1>
<p className="font-medium uppercase text-hacktoberfest-light text-sm sm:text-base">
Or select one or more programming languages you would like to find
repositories for.
</p>

<div className="flex flex-wrap gap-4 sm:gap-6 items-center justify-center">
{mainLanguages.map(language => {
const id = `lang-${language}`;
const checked = selected.includes(language);
return (
<label key={language} htmlFor={id} className="flex items-center gap-2 cursor-pointer select-none">
<input
id={id}
type="checkbox"
className="checkbox checkbox-primary"
checked={checked}
onChange={() => toggleLanguage(language)}
/>
<span className="text-hacktoberfest-light text-sm sm:text-base">{language}</span>
</label>
);
})}
</div>

<div className="dropdown dropdown-top mt-4">
<Button tabIndex={0} className="umami--click--otherlangs-button text-sm sm:text-base">
Other languages
</Button>

<ul
tabIndex={0}
className="h-64 p-2 overflow-y-auto shadow-lg menu dropdown-content bg-white/95 backdrop-blur-sm rounded-xl w-72 border border-gray-200/50 z-[9999]"
>
{sortedOthers.map(language => {
const id = `other-${language}`;
const checked = selected.includes(language);
return (
<li key={language} className="px-1">
<label htmlFor={id} className="flex items-center gap-3 rounded-lg px-3 py-2 hover:bg-hacktoberfest-blue/80 hover:text-white cursor-pointer">
<input
id={id}
type="checkbox"
className="checkbox checkbox-primary"
checked={checked}
onChange={() => toggleLanguage(language)}
/>
<span className="text-sm text-gray-800">{language}</span>
</label>
</li>
);
})}
</ul>
</div>

{/* Search form moved below language selection */}
<form
className="items-center w-full max-w-xs mx-auto form-control outline-none"
className="items-center w-full max-w-xs mx-auto form-control outline-none mt-8"
onSubmit={handleSearch}
>
<div className="flex w-full">
<div className="relative flex w-full">
<input
type="text"
placeholder="Search for your language"
placeholder="Type a language (optional)"
className="w-full max-w-xs bg-transparent rounded-tr-none rounded-br-none input input-bordered text-hacktoberfest-light border-hacktoberfest-light
focus:border-hacktoberfest-light focus:!outline-none focus-visible:!outline-none placeholder:text-hacktoberfest-light text-sm sm:text-base"
name="search"
Expand All @@ -53,36 +130,6 @@ export function Hero() {
</button>
</div>
</form>
<p className="font-medium uppercase text-hacktoberfest-light text-sm sm:text-base">
Or select the programming language you would like to find
repositories for.
</p>
<div className="flex flex-wrap gap-4 sm:gap-6 items-center justify-center">
{mainLanguages.map(language => (
<LanguageButton key={language} language={language} />
))}
</div>
<div className="dropdown dropdown-top">
<Button tabIndex={0} className="umami--click--otherlangs-button text-sm sm:text-base">
Other languages
</Button>

<ul
tabIndex={0}
className="h-64 p-2 overflow-y-auto shadow-lg menu dropdown-content bg-white/95 backdrop-blur-sm rounded-xl w-60 border border-gray-200/50 z-[9999]"
>
{otherLanguages.sort(sortByName).map(language => (
<li key={language}>
<Link
href={`/repos/${language.toLowerCase()}`}
className="text-gray-700 hover:text-white hover:bg-hacktoberfest-blue rounded-lg transition-colors duration-200 px-3 py-2 text-sm"
>
{language}
</Link>
</li>
))}
</ul>
</div>
</div>
<MarqueTextAnimation />
</div>
Expand Down
10 changes: 5 additions & 5 deletions src/app/(public)/repos/[language]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { env } from '@/env.mjs';
import { notFound } from 'next/navigation';
import { capitalize } from '@/lib/utils';
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 { 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 type { RepoData, RepoItem, RepoResponse, SearchParams } from '@/types';
import type { Metadata } from 'next';
import { auth } from '@/auth';
Expand Down
135 changes: 135 additions & 0 deletions src/app/(public)/repos/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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';

export default async function ReposPage({ searchParams }: { searchParams: Promise<SearchParams> }) {
const sp = await searchParams;
const raw = (sp as any).l ?? (sp as any).langs ?? '';
const langs = (Array.isArray(raw) ? raw : raw.toString().split(','))
.map((s: string) => decodeURIComponent(s.trim()))
.filter(Boolean);

const reposRes = await getRepos(langs, sp);
if (!reposRes) notFound();

const { repos, page } = reposRes;

return (
<>
<Header />
<ScrollToTop />
<div className="w-full overflow-x-hidden">
<div className="container mx-auto px-4 pt-32 sm:pt-36 md:pt-40 pb-8">
<div className="min-h-screen">
<Sorter />
<StarsFilter />
<div className="grid grid-cols-1 gap-6 px-2 sm:px-4 sm:grid-cols-2 lg:grid-cols-3">
{repos.items.map(repo => (
<RepoCard key={repo.id} repo={repo} />
))}
</div>
</div>
<Pagination
page={page}
totalCount={repos.total_count}
searchParams={sp}
/>
</div>
</div>
</>
);
}

async function getRepos(
languages: string[],
searchParams: SearchParams
): Promise<RepoResponse | undefined> {
const session = await auth();
const {
p: page = '1',
s: sort = 'updated',
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;
}
Loading