Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
118 changes: 93 additions & 25 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,54 @@ 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 chosenLanguages = selected;

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

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

const params = new URLSearchParams();
chosenLanguages.forEach(lang => params.append('l', lang.toLowerCase()));

router.push(`/repos?${params.toString()}`);
}

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>
<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 @@ -54,35 +78,79 @@ export function Hero() {
</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
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 => (
<LanguageButton key={language} language={language} />
))}
{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">
<Button tabIndex={0} className="umami--click--otherlangs-button text-sm sm:text-base">

<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-60 border border-gray-200/50 z-[9999]"
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]"
>
{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>
))}
{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>
<form className="mt-6 flex justify-center" onSubmit={handleSearch}>
<button
type="submit"
className="bg-transparent rounded-tl-none rounded-bl-none group btn btn-square text-hacktoberfest-light border-hacktoberfest-light hover:!border-hacktoberfest-light hover:bg-primary-btn-hover-gradient flex-shrink-0"
>
<Search size={16} className="sm:w-5 sm:h-5" />
</button>
</form>

</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
137 changes: 137 additions & 0 deletions src/app/(public)/repos/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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: string[] = Array.isArray(sp.l) ? sp.l : sp.l ? [String(sp.l)] : [];

const langs = raw
.flatMap(r => r.split(',')) // handles comma-separated and multi params
.map(s => 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