Allow infinite scrolling of coaches.
parent
2a4d969030
commit
2660cac8a8
|
@ -1,10 +1,8 @@
|
|||
import * as React from "react"
|
||||
import { motion, useReducedMotion } from "framer-motion"
|
||||
import { MotionProps, motion, useReducedMotion } from "framer-motion"
|
||||
|
||||
const FadeInStaggerContext = React.createContext(false)
|
||||
|
||||
const viewport = { once: true, margin: "0px 0px -200px" }
|
||||
|
||||
export function FadeIn({ ...props }) {
|
||||
let shouldReduceMotion = useReducedMotion()
|
||||
let isInStaggerGroup = React.useContext(FadeInStaggerContext)
|
||||
|
@ -21,23 +19,42 @@ export function FadeIn({ ...props }) {
|
|||
: {
|
||||
initial: "hidden",
|
||||
whileInView: "visible",
|
||||
viewport,
|
||||
viewport: {
|
||||
once: true,
|
||||
margin: "0px 0px -200px",
|
||||
},
|
||||
})}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function FadeInStagger({ faster = false, ...props }) {
|
||||
export type FadeInStaggerProps = {
|
||||
faster?: boolean
|
||||
className?: string
|
||||
} & MotionProps
|
||||
|
||||
export const FadeInStagger = React.forwardRef(function FadeInStagger(
|
||||
props: FadeInStaggerProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
const { faster = false, ...other } = props
|
||||
|
||||
// Consider dropping framer-motion:
|
||||
// https://github.com/framer/motion/issues/776
|
||||
return (
|
||||
<FadeInStaggerContext.Provider value={true}>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
viewport={{
|
||||
once: true,
|
||||
margin: "0px 0px -200px",
|
||||
}}
|
||||
transition={{ staggerChildren: faster ? 0.12 : 0.2 }}
|
||||
{...props}
|
||||
{...other}
|
||||
/>
|
||||
</FadeInStaggerContext.Provider>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,10 +3,10 @@ import clsx from "clsx"
|
|||
|
||||
import { FadeIn } from "./FadeIn"
|
||||
|
||||
export type FallbackMessageProps = React.ComponentPropsWithoutRef<"div"> & {
|
||||
export type FallbackMessageProps = {
|
||||
title: string
|
||||
body: string
|
||||
}
|
||||
} & React.ComponentPropsWithoutRef<"div">
|
||||
|
||||
export const FallbackMessage = React.forwardRef(function FallbackMessage(
|
||||
props: FallbackMessageProps,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { SelectProps } from "@mui/base/Select"
|
|||
import type { Language } from "../types/Language"
|
||||
|
||||
import { Select, Option } from "./Select"
|
||||
import { useFetchLanguages } from "../utils/queries"
|
||||
import { useLanguagesQuery } from "../utils/queries"
|
||||
|
||||
export type SelectLanguageProps = SelectProps<{}, boolean>
|
||||
|
||||
|
@ -15,7 +15,7 @@ export const SelectLanguage = React.forwardRef(function SelectLanguage(
|
|||
const id = React.useId()
|
||||
const [options, setOptions] = React.useState<Language[] | null>(null)
|
||||
const { defaultValue, ...other } = props
|
||||
const { isLoading, data } = useFetchLanguages()
|
||||
const { isLoading, data } = useLanguagesQuery()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (data) {
|
||||
|
|
|
@ -1,17 +1,39 @@
|
|||
import * as React from "react"
|
||||
|
||||
import type { SearchParams } from "../types/SearchParams"
|
||||
|
||||
import { Container } from "../components/Container"
|
||||
import { FadeIn, FadeInStagger } from "../components/FadeIn"
|
||||
import { FadeIn } from "../components/FadeIn"
|
||||
import { FallbackMessage } from "../components/FallbackMessage"
|
||||
import { FilterModal } from "../components/FilterModal"
|
||||
import { FilterScroll } from "../components/FilterScroll"
|
||||
import { Loading } from "../components/Loading"
|
||||
import { SearchResult } from "../components/SearchResult"
|
||||
import { defaultSearchParams } from "../types/SearchParams"
|
||||
import { useFetchCoaches } from "../utils/queries"
|
||||
import { useCoachesInfiniteQuery } from "../utils/queries"
|
||||
|
||||
function SearchResults() {
|
||||
const { isLoading, isError, data } = useFetchCoaches()
|
||||
function SearchResults({ searchParams }: { searchParams: SearchParams }) {
|
||||
const { isLoading, isError, data, fetchNextPage, hasNextPage, isFetching } =
|
||||
useCoachesInfiniteQuery(searchParams)
|
||||
|
||||
const resultsRef = React.useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const handleScroll = React.useCallback(() => {
|
||||
if (!resultsRef.current) {
|
||||
return
|
||||
}
|
||||
const resultRect = resultsRef.current.getBoundingClientRect()
|
||||
if (!isFetching && hasNextPage && resultRect.bottom < 800) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}, [isFetching, hasNextPage, resultsRef])
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("scroll", handleScroll, { passive: true })
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll)
|
||||
}
|
||||
}, [isFetching, hasNextPage, resultsRef])
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading className="mt-40" loading />
|
||||
|
@ -28,28 +50,38 @@ function SearchResults() {
|
|||
}
|
||||
|
||||
return (
|
||||
<FadeInStagger
|
||||
className="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
faster
|
||||
>
|
||||
{data?.map((coach, index) => (
|
||||
<FadeIn key={index} className="flex cursor-pointer flex-col">
|
||||
<SearchResult
|
||||
src={coach.image_url ?? ""}
|
||||
title={coach.name ?? ""}
|
||||
subtitle={coach.name ?? ""}
|
||||
target="_blank"
|
||||
href={
|
||||
coach.site === "lichess"
|
||||
? `https://lichess.org/coach/${coach.username}`
|
||||
: coach.site === "chesscom"
|
||||
? `https://www.chess.com/member/${coach.username}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FadeIn>
|
||||
))}
|
||||
</FadeInStagger>
|
||||
<>
|
||||
<div
|
||||
ref={resultsRef}
|
||||
className="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
>
|
||||
{data?.pages.map((group, index) => (
|
||||
<React.Fragment key={index}>
|
||||
{group.map((coach) => (
|
||||
<FadeIn
|
||||
key={`${coach.site}-${coach.username}`}
|
||||
className="flex cursor-pointer flex-col"
|
||||
>
|
||||
<SearchResult
|
||||
src={coach.image_url ?? ""}
|
||||
title={coach.name ?? ""}
|
||||
subtitle={coach.name ?? ""}
|
||||
target="_blank"
|
||||
href={
|
||||
coach.site === "lichess"
|
||||
? `https://lichess.org/coach/${coach.username}`
|
||||
: coach.site === "chesscom"
|
||||
? `https://www.chess.com/member/${coach.username}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FadeIn>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{hasNextPage ? <Loading className="pt-20" loading /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -73,7 +105,7 @@ export function Search() {
|
|||
setModalOpen(false)
|
||||
}}
|
||||
/>
|
||||
<SearchResults />
|
||||
<SearchResults searchParams={searchParams} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,22 +1,35 @@
|
|||
import axios from "axios"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useQuery, useInfiniteQuery } from "@tanstack/react-query"
|
||||
|
||||
import type { Coach } from "../types/Coach"
|
||||
import type { Language } from "../types/Language"
|
||||
import type { SearchParams } from "../types/SearchParams"
|
||||
|
||||
export const useFetchCoaches = () => {
|
||||
return useQuery({
|
||||
queryKey: ["api", "coaches"],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<{ data: Coach[] }>("/api/coaches/")
|
||||
export const useCoachesInfiniteQuery = (searchParams: SearchParams) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["coaches", searchParams],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const response = await axios.get<{ data: Coach[] }>("/api/coaches/", {
|
||||
params: {
|
||||
page_no: pageParam,
|
||||
page_size: 15,
|
||||
},
|
||||
})
|
||||
return response.data.data
|
||||
},
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage, _pages, lastPageParam) => {
|
||||
if (lastPage.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return lastPageParam + 1
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useFetchLanguages = () => {
|
||||
export const useLanguagesQuery = () => {
|
||||
return useQuery({
|
||||
queryKey: ["api", "languages"],
|
||||
queryKey: ["languages"],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<{ data: Language[] }>("/api/languages/")
|
||||
return response.data.data
|
||||
|
|
|
@ -11,17 +11,20 @@ defmodule BoardWise.Coaches do
|
|||
@prefix "coach_scraper"
|
||||
|
||||
@doc """
|
||||
Returns the list of coaches.
|
||||
Return the list of coaches at the given page, based on page size.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> list_coaches()
|
||||
iex> page_coaches(1, 25)
|
||||
[%Coach{}, ...]
|
||||
|
||||
"""
|
||||
def list_coaches do
|
||||
def page_coaches(page_no, page_size) do
|
||||
offset = (page_no - 1) * page_size
|
||||
|
||||
Coach
|
||||
|> limit(6)
|
||||
|> limit(^page_size)
|
||||
|> offset(^offset)
|
||||
|> Repo.all(prefix: @prefix)
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ defmodule BoardWise.Coaches.Coach do
|
|||
:username,
|
||||
:name,
|
||||
:image_url,
|
||||
:languages,
|
||||
:rapid,
|
||||
:blitz,
|
||||
:bullet
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
defmodule BoardWiseWeb.CoachController do
|
||||
use BoardWiseWeb, :controller
|
||||
require Logger
|
||||
|
||||
alias BoardWise.Coaches
|
||||
|
||||
def index(conn, _params) do
|
||||
coaches = Coaches.list_coaches()
|
||||
plug :fetch_query_params
|
||||
|
||||
def index(conn, params) do
|
||||
page_no = get_integer_param(params, "page_no", 1)
|
||||
page_size = get_integer_param(params, "page_size", 10)
|
||||
coaches = Coaches.page_coaches(page_no, page_size)
|
||||
render(conn, :index, coaches: coaches)
|
||||
end
|
||||
|
||||
defp get_integer_param(params, key, default) do
|
||||
val = Map.get(params, key)
|
||||
|
||||
if is_nil(val) do
|
||||
default
|
||||
else
|
||||
case Integer.parse(val) do
|
||||
{parsed, ""} -> parsed
|
||||
_ -> default
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,9 +10,9 @@ defmodule BoardWise.CoachesTest do
|
|||
|
||||
@invalid_attrs %{blitz: nil, bullet: nil, rapid: nil, site: nil, username: nil}
|
||||
|
||||
test "list_coaches/0 returns all coaches" do
|
||||
test "page_coaches/2 returns all coaches" do
|
||||
coach = coach_fixture()
|
||||
assert Coaches.list_coaches() == [coach]
|
||||
assert Coaches.page_coaches(1, 1000) == [coach]
|
||||
end
|
||||
|
||||
test "get_coach!/1 returns the coach with given id" do
|
||||
|
|
Loading…
Reference in New Issue