Dommmin's avatar

Laravel API + React Query Filter, Sort, Paginate

I have simple app with search, filter, sort and pagination. Can someone give me feedback if everything is ok or maybe something should be improved.

In Laravel Controller:

public function index(Request $request)
{
    $perPage = $request->get('perPage', 10);

    return Post::with(['user:id,name', 'category:id,name'])
        ->when($request->filled('category'), function ($query) use ($request) {
            return $query->where('category_id', $request->get('category'));
        })
        ->when($request->filled('search'), function ($query) use ($request) {
            return $query->where('title', 'like', '%' . $request->get('search') . '%');
        })
        ->when($request->filled('sortColumn'), function ($query) use ($request) {
            if ($request->get('sortColumn') === 'category') {
                $query->join('categories', 'categories.id', '=', 'posts.category_id')
                    ->select('posts.*', 'categories.name')
                    ->orderBy('categories.name', $request->get('sortOrder', 'asc'));
            } else {
                $query->orderBy($request->get('sortColumn'), $request->get('sortOrder', 'asc'));
            }
        })
    ->simplePaginate($perPage);
}

public function categories()
{
    return Category::get(['name', 'id']);
}

and React:

export default function Posts() { const [params, setParams] = useSearchParams({ sortOrder: 'asc', }); const [data, setData] = useState()

function handlePerPageUpdate(event) {
    event.preventDefault();

    const newParams = new URLSearchParams(params);
    newParams.set('perPage', event.target.value);

    setParams(newParams);
}

function handleColumnSort(column) {
    const newParams = new URLSearchParams(params);

    newParams.set('sortColumn', column);

    if (params.get('sortOrder') === 'asc') {
        newParams.set('sortOrder', 'desc');
    } else {
        newParams.set('sortOrder', 'asc');
    }

    setParams(newParams);
}

function handleCategoryUpdate(event) {
    event.preventDefault();

    const newParams = new URLSearchParams(params);

    if (event.target.value === '') {
        newParams.delete('category');
    } else {
        newParams.set('category', event.target.value);
    }

    setParams(newParams);

    newParams.set('page', '1');
    setParams(newParams);
}

function handleSearchUpdate(event) {
    event.preventDefault();

    setTimeout(() => {
        const newParams = new URLSearchParams(params);

        if (event.target.value === '') {
            newParams.delete('search');
        } else {
            newParams.set('search', event.target.value);
        }

        setParams(newParams);

        newParams.set('page', '1');
        setParams(newParams);
    }, 1000);
}


const { isPending, error, data: posts, refetch } = useQuery({
    queryKey: ['posts'],
    queryFn: () => axios.get('http://127.0.0.1:8000/api/posts', {params: params})
        .then((res) => {
            setData(res.data);

            return res.data.data
        }
    ),
    refetchOnWindowFocus: true,
});

useEffect(() => {
    refetch()
}, [params, refetch]);

const { isPending: isPendingCategories, error: errorCategories, data: categories } = useQuery({
    queryKey: ['categories'],
    queryFn: () =>
        fetch('http://127.0.0.1:8000/api/categories').then(
            (res) => res.json(),
        ),
});

if (isPending || isPendingCategories) return 'Loading...';

if (error) return 'An error has occurred: ' + error.message;
if (errorCategories) return 'An error has occurred: ' + errorCategories.message;

function handleChangePage(button) {
    const newParams = new URLSearchParams(params);

    if (button === 'next') {
        const page = data.current_page + 1;
        newParams.set('page', page.toString())
    } else {
        const page = data.current_page - 1;
        newParams.set('page', page.toString())
    }

    setParams(newParams);
}

return (
    <div className="max-w-7xl mx-auto p-4">
        <div className="relative overflow-x-auto shadow-md sm:rounded-lg">
            <div className="flex items-center justify-between pb-4">
                <div>
                    <select onChange={handleCategoryUpdate} value={params.get('category') || ''} className="rounded-xl" name="category" id="category">
                        <option value="">All categories</option>
                        {categories.map((category) => (
                            <option key={category.id} value={category.id}>{category.name}</option>
                        ))}
                    </select>
                </div>
                <label htmlFor="table-search" className="sr-only">Search</label>
                <div className="relative">
                    <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
                        <svg className="w-5 h-5 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clipRule="evenodd"></path></svg>
                    </div>
                    <input defaultValue={params.get('search')} onChange={handleSearchUpdate} type="text" id="table-search" className="block p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg w-80 bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Search by title" />
                </div>
            </div>
            {posts.length
                ?
                <table className="w-full text-sm text-left text-gray-500 dark:text-gray-400">
                    <thead className="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                    <tr>
                        <th onClick={() => handleColumnSort('id')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
                            ID
                        </th>
                        <th onClick={() => handleColumnSort('title')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
                            Title
                        </th>
                        <th onClick={() => handleColumnSort('category')} scope="col" className="px-6 py-3 cursor-pointer hover:bg-gray-100">
                            Category
                        </th>
                        <th scope="col" className="px-6 py-3">
                            Body
                        </th>
                        <th scope="col" className="px-6 py-3">
                            Created at
                        </th>
                    </tr>
                    </thead>
                    <tbody>
                        {posts.map((post) => (
                            <Post key={post.id} post={post} />
                        ))}
                    </tbody>
                </table>
                : <div className="p-2">No results found...</div>
            }
            <div className="p-2">
                <select onChange={handlePerPageUpdate} value={params.get('perPage') || 10} name="perPage" id="perPage" className="rounded-xl">
                    <option value="10">10 per page</option>
                    <option value="20">20 per page</option>
                    <option value="50">50 per page</option>
                    <option value="100">100 per page</option>
                </select>
            </div>
            <div className="flex items-center justify-between p-4">
                    <button
                        onClick={() => handleChangePage('prev')}
                        disabled={!data.prev_page_url}
                        className={`rounded-xl px-4 py-1 border-2 border-black hover:border-cyan-500`}
                    >Prev</button>
                    <button
                        onClick={() => handleChangePage('next')}
                        disabled={!data.next_page_url}
                        className={`rounded-xl px-4 py-1 border-2 border-black hover:border-cyan-500`}
                    >Next</button>
            </div>
        </div>
    </div>
)

}

0 likes
0 replies

Please or to participate in this conversation.