Building Sort Selector
SortSelector.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { BsChevronDown } from "react-icons/bs";
const SortSelector = () => {
return (
<Menu>
<MenuButton as={Button} rightIcon={<BsChevronDown />}>
{"Order by: Relvevance"}
</MenuButton>
<MenuList>
<MenuItem>Relevance</MenuItem>
<MenuItem>Data added </MenuItem>
<MenuItem>Name</MenuItem>
<MenuItem>Release date</MenuItem>
<MenuItem>Popularity</MenuItem>
<MenuItem>Average rating</MenuItem>
</MenuList>
</Menu>
);
};
export default SortSelector;
App.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 import { Grid, GridItem, HStack, Show } from "@chakra-ui/react";
import NavBar from "./components/NavBar";
import GameGrid from "./components/GameGrid";
import GenreList from "./components/GenreList";
import { useState } from "react";
import { Genre } from "./hooks/useGenres";
import PlatfromSelector from "./components/PlatfromSelector";
import { Platform } from "./hooks/useGames";
import SortSelector from "./components/sortSelector";
export interface GameQuery {
genre: Genre | null;
platform: Platform | null;
}
function App() {
const [gameQuery, setGameQuery] = useState<GameQuery>({} as GameQuery);
return (
<Grid
templateAreas={{
base: `"nav" "main"`,
lg: `"nav nav" "aside main"`,
}}
templateColumns={{
base: "1fr",
lg: "250px 1fr",
}}
>
<GridItem area="nav">
<NavBar />
</GridItem>
<Show above="lg">
<GridItem area="aside" paddingX={5}>
<GenreList
onSelectGenre={(genre) => setGameQuery({ ...gameQuery, genre })}
selectedGenre={gameQuery.genre}
/>
</GridItem>
</Show>
<GridItem area="main">
<HStack spacing={5} paddingLeft={2} marginBottom={5}>
<PlatfromSelector
onSelectPlatform={(platform) =>
setGameQuery({ ...gameQuery, platform })
}
selectedPlatform={gameQuery.platform}
/>
<SortSelector />
</HStack>
<GameGrid gameQuery={gameQuery} />
</GridItem>
</Grid>
);
}
export default App;
Sort Games
Endpoint 为 games 的 query Parameters 中有一项是 ordering,
Available fields: name
, released
, added
, created
, updated
, rating
, metacritic
. You can reverse the sort order adding a hyphen, for example: -released
.|
实现步骤和关键点:
组件结构和数据流:
有一个 SortSelector
组件和一个父组件 App
。
App
组件维护一个 gameQuery
状态对象,包含查询参数。
SortSelector
组件接收 onSelectSortOrder
回调函数和当前的 sortOrder
作为 props。
SortSelector
组件实现:
定义一个包含排序选项的数组,每个选项是一个对象,包含 label
和 value
。
使用 map
函数渲染这些选项,创建 UI 元素,并为每个选项添加点击事件处理函数,点击时调用 onSelectSortOrder(option.value)
。
使用传入的 sortOrder
prop 和 find
方法确定当前选中的选项。
在 UI 中通过条件渲染(使用逻辑运算符和可选链)来显示当前选中状态。
数据流动:
用户点击排序选项 -> 触发 onSelectSortOrder
-> 更新 App
组件中的 gameQuery.sortOrder
。
App
组件将更新后的 sortOrder
传回 SortSelector
组件,用于更新 UI 显示。
查询逻辑:
在 useGames
hook 中,使用 gameQuery.sortOrder
作为 ordering
参数进行游戏数据查询。
SortSelector.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react";
import { BsChevronDown } from "react-icons/bs";
interface Props {
onSelectSortOrder: (sortOrder: string) => void;
sortOrder: string;
}
const SortSelector = ({ onSelectSortOrder, sortOrder }: Props) => {
const sortOrders = [
{ value: "", label: "Relevance" },
{ value: "-added", label: "Data added" },
{ value: "name", label: "Name" },
{ value: "-released", label: "Release date" },
{ value: "-metacritic", label: "Popularity" },
{ value: "-rating", label: "Average rating" },
];
const currentSortOrder = sortOrders.find(
(order) => order.value === sortOrder
);
return (
<Menu>
<MenuButton as={Button} rightIcon={<BsChevronDown />}>
Order by: {currentSortOrder?.label || "Relevance"}
</MenuButton>
<MenuList>
{sortOrders.map((order) => (
<MenuItem
onClick={() => onSelectSortOrder(order.value)}
key={order.value}
value={order.value}
>
{order.label}
</MenuItem>
))}
</MenuList>
</Menu>
);
};
export default SortSelector;
App.tsx 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62 import { Grid, GridItem, HStack, Show } from "@chakra-ui/react";
import NavBar from "./components/NavBar";
import GameGrid from "./components/GameGrid";
import GenreList from "./components/GenreList";
import { useState } from "react";
import { Genre } from "./hooks/useGenres";
import PlatfromSelector from "./components/PlatfromSelector";
import { Platform } from "./hooks/useGames";
import SortSelector from "./components/SortSelector";
export interface GameQuery {
genre: Genre | null;
platform: Platform | null;
sortOrder: string;
}
function App() {
const [gameQuery, setGameQuery] = useState<GameQuery>({} as GameQuery);
return (
<Grid
templateAreas={{
base: `"nav" "main"`,
lg: `"nav nav" "aside main"`,
}}
templateColumns={{
base: "1fr",
lg: "250px 1fr",
}}
>
<GridItem area="nav">
<NavBar />
</GridItem>
<Show above="lg">
<GridItem area="aside" paddingX={5}>
<GenreList
onSelectGenre={(genre) => setGameQuery({ ...gameQuery, genre })}
selectedGenre={gameQuery.genre}
/>
</GridItem>
</Show>
<GridItem area="main">
<HStack spacing={5} paddingLeft={2} marginBottom={5}>
<PlatfromSelector
onSelectPlatform={(platform) =>
setGameQuery({ ...gameQuery, platform })
}
selectedPlatform={gameQuery.platform}
/>
<SortSelector
onSelectSortOrder={(sortOrder) =>
setGameQuery({ ...gameQuery, sortOrder })
}
sortOrder={gameQuery.sortOrder}
/>
</HStack>
<GameGrid gameQuery={gameQuery} />
</GridItem>
</Grid>
);
}
export default App;
useGames.ts 1
2
3
4
5
6
7
8
9
10
11
12
13 const useGames = ( gameQuery : GameQuery ) =>
// 'selectedGenre' is possibly 'null'
useData < Game > (
"/games" ,
{
params : {
genres : gameQuery.genre?.id ,
platforms : gameQuery.platform?.id ,
ordering : gameQuery.sortOrder ,
},
},
[ gameQuery ]
);
此外还有一个 bug 需要修复,由于有些响应没有图片,所以要增加一个判断。
image-url.ts export default function getCroppedImageUrl ( url : string ) {
if ( ! url ) return "" ;
const target = "media/" ;
const index = url . indexOf ( target ) + target . length ;
return url . slice ( 0 , index ) + "crop/600/400/" + url . slice ( index );
}
对于这些没有图片的游戏卡片,使用占位图。
由于是静态资源,因此不能直接使用相对路径,而是使用导入的方式。
image-url.ts import noImagge from "../assets/no-image-placeholder.webp" ;
export default function getCroppedImageUrl ( url : string ) {
if ( ! url ) return noImagge ;
const target = "media/" ;
const index = url . indexOf ( target ) + target . length ;
return url . slice ( 0 , index ) + "crop/600/400/" + url . slice ( index );
}
2024-08-18 21:27 2024-09-08 21:07