Skip to content
Closed
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
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
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
38 changes: 20 additions & 18 deletions src/app/(public)/repos/_components/stars-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

import {
useParams,
usePathname,
useRouter,
useSearchParams
usePathname
} from 'next/navigation';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useQueryStates } from 'nuqs';

interface FormValues {
startStars: number | '';
Expand All @@ -17,40 +16,43 @@ interface FormValues {
type Pathname = '/repos' | `/repos/${string}`;

export function StarsFilter() {
const router = useRouter();
const pathname = usePathname() as Pathname;
const searchParams = useSearchParams();
const params = useParams();
const [{ startStars, endStars, p }, setStarsParams] = useQueryStates({
startStars: { parse: (value: string) => (value ? +value : ''), serialize: (value: number | '') => value === '' ? '' : String(value) },
endStars: { parse: (value: string) => (value ? +value : ''), serialize: (value: number | '') => value === '' ? '' : String(value) },
p: { parse: (value: string) => (value ? +value : 1), serialize: (value: number) => String(value) }
});

const { handleSubmit, control, reset } = useForm<FormValues>({
defaultValues: {
startStars: !searchParams.get('startStars')
? ''
: +(searchParams.get('startStars') as string),
endStars: !searchParams.get('endStars')
? ''
: +(searchParams.get('endStars') as string)
startStars: startStars === '' ? '' : startStars || '',
endStars: endStars === '' ? '' : endStars || ''
}
});

// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => reset(), [params.language]);

function onSubmit({ startStars, endStars }: FormValues) {
const sp = new URLSearchParams(searchParams);
if (
typeof endStars === 'number' &&
typeof startStars === 'number' &&
endStars < startStars
) {
reset({ startStars, endStars: '' });
sp.delete('endStars');
sp.set('startStars', startStars.toString());
void setStarsParams({
startStars,
endStars: '',
p: 1
});
} else {
sp.set('startStars', startStars.toString());
sp.set('endStars', endStars.toString());
void setStarsParams({
startStars: typeof startStars === 'number' ? startStars : '',
endStars: typeof endStars === 'number' ? endStars : '',
p: 1
});
}
sp.delete('p');
router.push(`${pathname}?${sp.toString()}`);
}

return (
Expand Down
1 change: 0 additions & 1 deletion src/app/(public)/repos/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
Loading