Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
87 changes: 66 additions & 21 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,11 +16,34 @@ 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 (
Expand All @@ -39,7 +61,7 @@ export function Hero() {
<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,33 +76,56 @@ 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 the 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">
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>
</div>
Expand Down
54 changes: 35 additions & 19 deletions src/app/(public)/repos/[language]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,7 @@ async function getRepos(
? `stars:<${endStars}`
: '';

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 language:${language} ${searchQuery} ${starsQuery}`
);

const languages = language.split(',').map(l => l.trim()); // split multi-language
const headers: HeadersInit = {
Accept: 'application/vnd.github.mercy-preview+json'
};
Expand All @@ -136,25 +127,50 @@ async function getRepos(
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();
let allRepos: RepoItem[] = [];

// fetch github repos for each language and merge
for (const lang of languages) {
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 language:${lang} ${searchQuery} ${starsQuery}`
);

const res = await fetch(apiUrl, { headers });
if (!res.ok) continue;

const reposData: RepoData = await res.json();
const filteredItems = reposData.items.filter(
(repo: RepoItem) => !repo.archived && !reports.find(r => r.repoId === repo.id)
);

allRepos = allRepos.concat(filteredItems);
}

repos.items = repos.items.filter((repo: RepoItem) => {
return !repo.archived && !reports.find(report => report.repoId === repo.id);
});
if (allRepos.length < 1) return undefined;

if (!Array.isArray(repos.items) || repos.items?.length < 1) return undefined;
// sort merged repos by updated date descending
allRepos.sort(
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);

return {
page: +page.toString(),
languageName: language,
repos
repos: {
...{ total_count: allRepos.length, incomplete_results: false },
items: allRepos
}
};
}


async function getReportedRepos() {
const reports = await db
.select()
Expand Down
Loading