Show dummy search results.

pull/3/head
Joshua Potter 2023-12-04 13:35:01 -07:00
parent bb650f6d35
commit 99b9519ab7
15 changed files with 347 additions and 110 deletions

View File

@ -1,13 +1,18 @@
import * as React from "react"
import { RouterProvider } from "react-router-dom"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { RootLayout } from "./components/RootLayout"
import { router } from "./router"
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<RootLayout>
<RouterProvider router={router} />
</RootLayout>
</QueryClientProvider>
)
}

View File

@ -1,27 +0,0 @@
import * as React from "react"
interface CaptionImageProps {
title?: string
subtitle?: string
src: string
}
export function CaptionImage({ title, subtitle, src }: CaptionImageProps) {
return (
<div className="group relative h-96 overflow-hidden rounded-3xl bg-neutral-100">
<img
alt=""
src={src}
className="h-full w-full object-cover transition duration-500 motion-safe:group-hover:scale-105"
/>
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black to-black/0 to-40% p-6">
{title && (
<p className="font-display text-base/6 font-semibold tracking-wide text-white">
{title}
</p>
)}
{subtitle && <p className="mt-2 text-sm text-white">{subtitle}</p>}
</div>
</div>
)
}

View File

@ -1,7 +1,7 @@
import * as React from "react"
import clsx from "clsx"
import type { Query } from "../types/Query"
import type { SearchParams } from "../types/SearchParams"
import FilterIcon from "../icons/Filter"
import RightArrowIcon from "../icons/RightArrow"
@ -11,8 +11,8 @@ import { Button } from "./Button"
interface FilterOption {
title: string
Icon: ({ ...props }: { [x: string]: any }) => React.JSX.Element
enable: (q: Query) => Query
isEnabled: (q: Query) => boolean
enable: (p: SearchParams) => SearchParams
isEnabled: (p: SearchParams) => boolean
}
const filters: FilterOption[] = [
@ -33,12 +33,12 @@ enum Direction {
}
interface FilterScrollProps {
query: Query
params: SearchParams
onModal: () => void
onEnable: (q: Query) => void
onSelect: (p: SearchParams) => void
}
export function FilterScroll({ query, onModal, onEnable }: FilterScrollProps) {
export function FilterScroll({ params, onModal, onSelect }: FilterScrollProps) {
const viewport = React.useRef<HTMLDivElement>(null)
const [isFlush, setIsFlush] = React.useState([true, false])
@ -67,9 +67,9 @@ export function FilterScroll({ query, onModal, onEnable }: FilterScrollProps) {
<div
key={e.title}
className={clsx("flex-none cursor-pointer text-center", {
"fill-amber-500 text-amber-500": e.isEnabled(query),
"fill-amber-500 text-amber-500": e.isEnabled(params),
})}
onClick={() => onEnable(e.enable({ ...query }))}
onClick={() => onSelect(e.enable({ ...params }))}
>
<e.Icon className="mx-auto h-6 w-6" />
<span className="text-xs">{e.title}</span>

View File

@ -0,0 +1,41 @@
import * as React from "react"
import clsx from "clsx"
type SearchResultProps = {
title?: string
subtitle?: string
src?: string
} & React.ComponentPropsWithoutRef<"div">
export function SearchResult({
title,
subtitle,
src,
className,
...props
}: SearchResultProps) {
return (
<div
className={clsx(
"group relative h-96 overflow-hidden rounded-3xl bg-neutral-100",
className
)}
{...props}
>
<img
src={src}
className="h-full w-full object-cover transition duration-500 motion-safe:group-hover:scale-105"
/>
<div className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black to-black/0 to-40% p-6">
{title ? (
<p className="font-display text-base/6 font-semibold tracking-wide text-white">
{title}
</p>
) : null}
{subtitle ? (
<p className="mt-2 text-sm text-white">{subtitle}</p>
) : null}
</div>
</div>
)
}

View File

@ -1,63 +1,68 @@
import * as React from "react"
import axios from "axios"
import { useQuery } from "@tanstack/react-query"
import type { Query } from "../types/Query"
import type { Coach } from "../types/Coach"
import { type SearchParams, defaultSearchParams } from "../types/SearchParams"
import { CaptionImage } from "../components/CaptionImage"
import { Container } from "../components/Container"
import { FadeIn, FadeInStagger } from "../components/FadeIn"
import { FallbackMessage } from "../components/FallbackMessage"
import { FilterScroll } from "../components/FilterScroll"
import { Loading } from "../components/Loading"
import { SearchResult } from "../components/SearchResult"
const FIDE_RATING_MIN = 1500
const FIDE_RATING_MAX = 3200
function SearchResults() {
const { isLoading, isError, data } = useQuery({
queryKey: ["coaches"],
queryFn: async () => {
const response = await axios.get<{ data: Coach[] }>("/api/coaches/")
return response.data.data
},
})
interface Coach {
id: string
imageUrl: string
name: string
title: string
slug: string
if (isLoading) {
return <Loading loading />
}
const defaultQuery: Query = {
fideRating: [FIDE_RATING_MIN, FIDE_RATING_MAX],
if (isError) {
return (
<FallbackMessage
title="Unexpected Error"
body="We're looking into this. Please refresh or try again later."
/>
)
}
export function Search() {
const [query, setQuery] = React.useState<Query>(defaultQuery)
const [loading, setLoading] = React.useState(true)
const [coaches, setCoaches] = React.useState<Coach[]>([])
return (
<Container className="pt-8">
<FilterScroll query={query} onEnable={setQuery} onModal={() => {}} />
<Loading
className={loading || coaches.length === 0 ? "mt-40" : "mt-10"}
loading={loading}
>
{coaches.length > 0 ? (
<FadeInStagger
className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
faster
>
{coaches.map((coach, index) => (
{data?.map((coach, index) => (
<FadeIn key={index} className="flex cursor-pointer flex-col">
<CaptionImage
title={coach.name}
subtitle={coach.title || undefined}
src={coach.imageUrl}
<SearchResult
src={coach.image_url ?? ""}
title={coach.name ?? ""}
subtitle={coach.name ?? ""}
/>
</FadeIn>
))}
</FadeInStagger>
) : (
<FallbackMessage
title="Coming Soon"
body="Full search functionality will be added soon! Please come back later."
)
}
export function Search() {
const [searchParams, setSearchParams] = React.useState(defaultSearchParams)
return (
<Container className="pt-8">
<FilterScroll
params={searchParams}
onSelect={setSearchParams}
onModal={() => {}}
/>
)}
</Loading>
<SearchResults />
</Container>
)
}

View File

@ -0,0 +1,9 @@
export type Coach = {
site: string
username: string
name: string | null
image_url: string | null
rapid: number | null
blitz: number | null
bullet: number | null
}

View File

@ -1,3 +0,0 @@
export type Query = {
fideRating: [number, number]
}

View File

@ -0,0 +1,10 @@
export type SearchParams = {
fideRating: [number, number]
}
const FIDE_RATING_MIN = 1500
const FIDE_RATING_MAX = 3200
export const defaultSearchParams: SearchParams = {
fideRating: [FIDE_RATING_MIN, FIDE_RATING_MAX],
}

190
assets/package-lock.json generated
View File

@ -9,6 +9,8 @@
"version": "0.1.0",
"dependencies": {
"@mui/base": "^5.0.0-beta.25",
"@tanstack/react-query": "^5.12.2",
"axios": "^1.6.2",
"clsx": "^2.0.0",
"framer-motion": "^10.16.12",
"react": "^18.2.0",
@ -192,6 +194,30 @@
"node": ">=14.0.0"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.12.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.12.1.tgz",
"integrity": "sha512-WbZztNmKq0t6QjdNmHzezbi/uifYo9j6e2GLJkodsYaYUlzMbAp91RDyeHkIZrm7EfO4wa6Sm5sxJZm5SPlh6w==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.12.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.12.2.tgz",
"integrity": "sha512-BeWZu8zVFH20oRc+S/K9ADPgWjEzP/XQCGBNz5IbApUwPQAdwkQYbXODVL5AyAlWiSxhx+P2xlARPBApj2Yrog==",
"dependencies": {
"@tanstack/query-core": "5.12.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18.0.0"
}
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
@ -250,6 +276,21 @@
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
@ -258,12 +299,63 @@
"node": ">=6"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"devOptional": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "10.16.12",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.12.tgz",
@ -303,6 +395,25 @@
"loose-envify": "cli.js"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -326,6 +437,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@ -498,6 +614,19 @@
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.13.1.tgz",
"integrity": "sha512-so+DHzZKsoOcoXrILB4rqDkMDy7NLMErRdOxvzvOKb507YINKUP4Di+shbTZDhSE/pBZ+vr7XGIpcOO0VLSA+Q=="
},
"@tanstack/query-core": {
"version": "5.12.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.12.1.tgz",
"integrity": "sha512-WbZztNmKq0t6QjdNmHzezbi/uifYo9j6e2GLJkodsYaYUlzMbAp91RDyeHkIZrm7EfO4wa6Sm5sxJZm5SPlh6w=="
},
"@tanstack/react-query": {
"version": "5.12.2",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.12.2.tgz",
"integrity": "sha512-BeWZu8zVFH20oRc+S/K9ADPgWjEzP/XQCGBNz5IbApUwPQAdwkQYbXODVL5AyAlWiSxhx+P2xlARPBApj2Yrog==",
"requires": {
"@tanstack/query-core": "5.12.1"
}
},
"@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
@ -556,17 +685,60 @@
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
"devOptional": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"csstype": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"devOptional": true
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"framer-motion": {
"version": "10.16.12",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.16.12.tgz",
@ -589,6 +761,19 @@
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -611,6 +796,11 @@
}
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",

View File

@ -3,6 +3,8 @@
"version": "0.1.0",
"dependencies": {
"@mui/base": "^5.0.0-beta.25",
"@tanstack/react-query": "^5.12.2",
"axios": "^1.6.2",
"clsx": "^2.0.0",
"framer-motion": "^10.16.12",
"react": "^18.2.0",

View File

@ -1,21 +0,0 @@
defmodule BoardWise.Coach do
use Ecto.Schema
import Ecto.Changeset
schema "coaches" do
field :blitz, :integer
field :bullet, :integer
field :rapid, :integer
field :site, :string
field :username, :string
timestamps(type: :utc_datetime)
end
@doc false
def changeset(coach, attrs) do
coach
|> cast(attrs, [:site, :username, :rapid, :blitz, :bullet])
|> validate_required([:site, :username, :rapid, :blitz, :bullet])
end
end

View File

@ -20,7 +20,10 @@ defmodule BoardWise.Coaches do
"""
def list_coaches do
Repo.all(Coach, prefix: @prefix)
Coach
|> limit(6)
|> where(site: "lichess")
|> Repo.all(prefix: @prefix)
end
@doc """

View File

@ -15,18 +15,27 @@ defmodule BoardWise.Coaches.Coach do
import Ecto.Changeset
schema "export" do
field :site, :string
field :username, :string
field :name, :string
field :image_url, :string
field :blitz, :integer
field :bullet, :integer
field :rapid, :integer
field :site, :string
field :username, :string
end
@doc false
def changeset(coach, attrs) do
coach
|> cast(attrs, [:rapid, :blitz, :bullet, :site, :username])
|> cast(attrs, [
:site,
:username,
:name,
:image_url,
:rapid,
:blitz,
:bullet
])
|> validate_required([:site, :username])
|> unique_constraint(:site_username_unique, name: :site_username_unique)
end

View File

@ -12,6 +12,8 @@ defmodule BoardWiseWeb.CoachJSON do
%{
site: coach.site,
username: coach.username,
name: coach.name,
image_url: coach.image_url,
rapid: coach.rapid,
blitz: coach.blitz,
bullet: coach.bullet

View File

@ -0,0 +1,12 @@
defmodule BoardWise.Repo.Migrations.NameImageUrl do
use Ecto.Migration
@prefix "coach_scraper"
def change do
alter table(:export, prefix: @prefix) do
add :name, :string
add :image_url, :string
end
end
end