Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"next-cloudinary": "^6.16.0",
"next-seo": "^6.8.0",
"nextjs-progressbar": "^0.0.16",
"nuqs": "^2.7.2",
"postcss": "^8.5.6",
"postgres": "^3.4.7",
"prettier": "^3.6.2",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 15 additions & 11 deletions src/app/(public)/_components/search-form.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
'use client';

import { usePathname, useSearchParams, useRouter } from 'next/navigation';
import { usePathname } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { GoX } from 'react-icons/go';
import { useQueryState } from 'nuqs';

interface FormValues {
searchQuery: string;
}

export function SearchForm() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [searchQuery, setSearchQuery] = useQueryState('q', {
defaultValue: '',
parse: (value: string) => value,
serialize: (value: string) => value || ''
});

const { register, handleSubmit, reset, watch } = useForm<FormValues>({
defaultValues: {
searchQuery: searchParams.get('q') as string
searchQuery: searchQuery
}
});

const searchQuery = watch('searchQuery');
const queryValue = watch('searchQuery');

if (!pathname.startsWith('/repos')) {
return null;
Expand All @@ -28,12 +32,9 @@ 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(`${reposPathname}?${sp.toString()}`);
void setSearchQuery(trimmedQuery);
}
}

Expand All @@ -47,11 +48,14 @@ export function SearchForm() {
type="text"
{...register('searchQuery', { required: true })}
/>
{searchQuery && searchQuery.trim() !== '' && (
{queryValue && queryValue.trim() !== '' && (
<button
className="absolute top-0 right-0 rounded-l-none btn btn-ghost btn-sm"
type="button"
onClick={() => reset()}
onClick={() => {
reset();
void setSearchQuery(null);
}}
>
<GoX color="white" />
</button>
Expand Down
1 change: 0 additions & 1 deletion src/app/(public)/repos/[language]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ export default async function ReposPage({
<Pagination
page={page}
totalCount={repos.total_count}
searchParams={sp}
/>
</div>
</div>
Expand Down
21 changes: 9 additions & 12 deletions src/app/(public)/repos/_components/pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,30 @@

import { Button } from '@/app/(public)/_components/button';
import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
import type { SearchParams } from '@/types';
import { useQueryState } from 'nuqs';

const MAX_PER_PAGE = 21;
interface PaginationProps {
page: number;
totalCount: number;
searchParams: SearchParams;
}

export function Pagination({
page,
totalCount,
searchParams
totalCount
}: PaginationProps) {
const router = useRouter();
const [, setPageParam] = useQueryState('p', {
defaultValue: '1',
parse: (value: string) => value,
serialize: (value: string) => value
});
const [isPending, startTransition] = useTransition();

function changePage(delta: number) {
const params = new URLSearchParams(
Object.entries(searchParams).map(([k, v]) => [k, String(v)])
);
params.set('p', String(page + delta));

const newPage = page + delta;
startTransition(() => {
router.push(`?${params.toString()}`);
void setPageParam(String(newPage));
});
}

Expand Down
99 changes: 47 additions & 52 deletions src/app/(public)/repos/_components/sorter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { Button } from '@/app/(public)/_components/button';
import { ArrowUpAZ, Code } from 'lucide-react';
import Link from 'next/link';
import languages from '@/assets/languages.json';
import { usePathname, useSearchParams } from 'next/navigation';
import { usePathname } from 'next/navigation';
import { sortByName } from '@/lib/utils';
import { useQueryStates, useQueryState } from 'nuqs';

const { mainLanguages } = languages;

Expand All @@ -23,88 +24,74 @@ enum SortTypes {
type Pathname = '/repos' | `/repos/${string}`;

export function Sorter() {
const searchParams = useSearchParams();
const pathname = usePathname() as Pathname;
const [sortField] = useQueryState('s', { defaultValue: '' });
const [sortOrder] = useQueryState('o', { defaultValue: 'desc' });
const [languages_] = useQueryState('l', { parse: (value: string) => value.split(',').filter(Boolean), serialize: (value: string[]) => value.join(',') });

const navigationItems = [
{
name: 'Best match',
onSelect(sp: URLSearchParams) {
sp.delete('o');
sp.delete('s');
return sp;
onSelect(): { s: string | null; o: string | null } {
return { s: null, o: null };
}
},
{
name: 'Most stars',
onSelect(sp: URLSearchParams) {
sp.set('s', 'stars');
sp.set('o', 'desc');
return sp;
onSelect() {
return { s: 'stars', o: 'desc' };
}
},
{
name: 'Fewest stars',
onSelect(sp: URLSearchParams) {
sp.set('s', 'stars');
sp.set('o', 'asc');
return sp;
onSelect() {
return { s: 'stars', o: 'asc' };
}
},
{
name: 'Most forks',
onSelect(sp: URLSearchParams) {
sp.set('s', 'forks');
sp.set('o', 'desc');
return sp;
onSelect() {
return { s: 'forks', o: 'desc' };
}
},
{
name: 'Fewest forks',
onSelect(sp: URLSearchParams) {
sp.set('s', 'forks');
sp.set('o', 'asc');
return sp;
onSelect() {
return { s: 'forks', o: 'asc' };
}
},
{
name: 'Most help wanted issues',
onSelect(sp: URLSearchParams) {
sp.set('s', 'help-wanted-issues');
sp.set('o', 'desc');
return sp;
onSelect() {
return { s: 'help-wanted-issues', o: 'desc' };
}
},
{
name: 'Recently updated',
onSelect(sp: URLSearchParams) {
sp.set('s', 'updated');
sp.set('o', 'desc');
return sp;
onSelect() {
return { s: 'updated', o: 'desc' };
}
},
{
name: 'Least recently updated',
onSelect(sp: URLSearchParams) {
sp.set('s', 'updated');
sp.set('o', 'asc');
return sp;
onSelect() {
return { s: 'updated', o: 'asc' };
}
}
];

function selectedSort(): SortTypes {
if (searchParams.get('o') === 'asc') {
if (searchParams.get('s') === 'stars') return SortTypes.FewestStars;
if (searchParams.get('s') === 'forks') return SortTypes.FewestForks;
if (searchParams.get('s') === 'updated')
if (sortOrder === 'asc') {
if (sortField === 'stars') return SortTypes.FewestStars;
if (sortField === 'forks') return SortTypes.FewestForks;
if (sortField === 'updated')
return SortTypes.LeastRecentlyUpdated;
return SortTypes.BestMatch;
} else if (searchParams.get('o') === 'desc') {
if (searchParams.get('s') === 'stars') return SortTypes.MostStars;
if (searchParams.get('s') === 'forks') return SortTypes.MostForks;
if (searchParams.get('s') === 'updated') return SortTypes.RecentlyUpdated;
if (searchParams.get('s') === 'help-wanted-issues')
} else if (sortOrder === 'desc') {
if (sortField === 'stars') return SortTypes.MostStars;
if (sortField === 'forks') return SortTypes.MostForks;
if (sortField === 'updated') return SortTypes.RecentlyUpdated;
if (sortField === 'help-wanted-issues')
return SortTypes.MostHelpWantedIssues;
return SortTypes.BestMatch;
} else {
Expand All @@ -127,12 +114,17 @@ export function Sorter() {
<div className="z-[9999] h-64 p-2 overflow-y-auto shadow-lg dropdown-content hidden group-hover:block bg-white/95 backdrop-blur-sm rounded-xl w-60 border border-gray-200/50">
<ul tabIndex={0} className="menu menu-vertical">
{mainLanguages.sort(sortByName).map(language => {
const sp = new URLSearchParams(searchParams);
sp.delete('p');
const languageParams = `l=${language.toLowerCase()}`;
const currentParams = new URLSearchParams();
if (sortField) currentParams.set('s', sortField);
if (sortOrder) currentParams.set('o', sortOrder);
const queryString = currentParams.toString();
const fullQuery = queryString ? `${languageParams}&${queryString}` : languageParams;

return (
<li key={language} onClick={handleClick}>
<Link
href={`/repos/${language.toLowerCase()}?${sp.toString()}`}
href={`/repos/${language.toLowerCase()}?${fullQuery}`}
className="text-gray-700 hover:text-white hover:bg-hacktoberfest-light-blue rounded-lg transition-colors duration-200 px-3 py-2"
>
{language}
Expand All @@ -151,16 +143,19 @@ export function Sorter() {
<div className="z-[9999] h-64 p-2 overflow-y-auto shadow-lg dropdown-content hidden group-hover:block -ml-16 bg-white/95 backdrop-blur-sm rounded-xl w-60 border border-gray-200/50">
<ul tabIndex={0} className="menu menu-vertical">
{navigationItems.map((item, index) => {
const sp = item.onSelect(new URLSearchParams(searchParams));
sp.delete('p');
if (item.name === SortTypes.BestMatch) {
sp.delete('o');
sp.delete('s');
const { s, o } = item.onSelect();
const currentParams = new URLSearchParams();
if (s) currentParams.set('s', s);
if (o) currentParams.set('o', o);
if (languages_ && languages_.length > 0) {
currentParams.set('l', languages_.join(','));
}
const queryString = currentParams.toString();

return (
<li key={index} onClick={handleClick}>
<Link
href={`${pathname}?${sp.toString()}`}
href={`${pathname}?${queryString}`}
className="text-gray-700 hover:text-white hover:bg-hacktoberfest-light-blue rounded-lg transition-colors duration-200 px-3 py-2"
>
{item.name}
Expand Down
Loading
Loading