diff --git a/project2/server/index.ts b/project2/server/index.ts index c7bcc1e..723420e 100644 --- a/project2/server/index.ts +++ b/project2/server/index.ts @@ -41,17 +41,57 @@ const typeDefs = `#graphql totalPages: Int! } + enum PokemonSortField { + ID + NAME + HP + ATTACK + DEFENCE + SPECIAL_ATTACK + SPECIAL_DEFENCE + SPEED + } + + enum SortOrder { + ASC + DESC + } + type Query { - pokemons(search: String, page: Int = 1, perPage: Int = 10): PokemonPage! + pokemons( + search: String + page: Int = 1 + perPage: Int = 10 + sortBy: PokemonSortField = ID + sortOrder: SortOrder = ASC + ): PokemonPage! pokemon(id: Int!): Pokemon } `; +// Whitelist: column names cannot be SQL parameters, so map enum -> column +const SORT_COLUMNS = { + ID: 'id', + NAME: 'name COLLATE NOCASE', + HP: 'hp', + ATTACK: 'attack', + DEFENCE: 'defence', + SPECIAL_ATTACK: 'special_attack', + SPECIAL_DEFENCE: 'special_defence', + SPEED: 'speed', +} as const; + const resolvers = { Query: { pokemons: ( _parent: unknown, - args: { search?: string; page?: number; perPage?: number } + args: { + search?: string; + page?: number; + perPage?: number; + sortBy?: keyof typeof SORT_COLUMNS; + sortOrder?: 'ASC' | 'DESC'; + } ) => { const page = Math.max(1, args.page ?? 1); const perPage = Math.min(50, Math.max(1, args.perPage ?? 10)); // clamp 1–50 @@ -67,10 +107,15 @@ const resolvers = { .get(pattern) as { n: number } ).n; + const column = SORT_COLUMNS[args.sortBy ?? 'ID'] ?? SORT_COLUMNS.ID; + const direction = args.sortOrder === 'DESC' ? 'DESC' : 'ASC'; + + // id as tiebreaker keeps page order stable const items = db .prepare( `SELECT id, name, attack, defence, special_attack, special_defence, speed, hp - FROM pokemons WHERE name LIKE ? ORDER BY id LIMIT ? OFFSET ?` + FROM pokemons WHERE name LIKE ? + ORDER BY ${column} ${direction}, id ASC LIMIT ? OFFSET ?` ) .all(pattern, perPage, offset) as Pokemon[]; diff --git a/project2/src/App.tsx b/project2/src/App.tsx index 931856d..80fa05c 100644 --- a/project2/src/App.tsx +++ b/project2/src/App.tsx @@ -1,15 +1,31 @@ import { useState } from 'react'; import { gql } from '@apollo/client'; import { useQuery } from '@apollo/client/react'; -import { SearchBar } from './components/SearchBar'; +import { + SearchBar, + type SortField, + type SortOrder, +} from './components/SearchBar'; import { PokemonCard } from './components/PokemonCard'; import { PokemonDetail } from './components/PokemonDetail'; import { Pagination } from './components/Pagination'; // Graph QL query for getting/searching for pokemons const GET_POKEMONS = gql` - query GetPokemons($search: String, $page: Int, $perPage: Int) { - pokemons(search: $search, page: $page, perPage: $perPage) { + query GetPokemons( + $search: String + $page: Int + $perPage: Int + $sortBy: PokemonSortField + $sortOrder: SortOrder + ) { + pokemons( + search: $search + page: $page + perPage: $perPage + sortBy: $sortBy + sortOrder: $sortOrder + ) { items { id name @@ -48,12 +64,14 @@ function App() { const [searchTerm, setSearchTerm] = useState(''); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(10); + const [sortBy, setSortBy] = useState('ID'); + const [sortOrder, setSortOrder] = useState('ASC'); const [selectedId, setSelectedId] = useState(null); // Get data and utilize Apollo cache, it handles basicly everything const { loading, error, data, previousData } = useQuery( GET_POKEMONS, - { variables: { search: searchTerm, page, perPage } } + { variables: { search: searchTerm, page, perPage, sortBy, sortOrder } } ); // Object of all our pokemons that are currently rendered, pluss features @@ -69,7 +87,20 @@ function App() { <>

PokeDOX

- + { + setSortBy(s); + setPage(1); + }} + onSortOrderChange={(o) => { + setSortOrder(o); + setPage(1); + }} + />
{pageInfo && ( diff --git a/project2/src/components/SearchBar.tsx b/project2/src/components/SearchBar.tsx index d9f775b..fc3689d 100644 --- a/project2/src/components/SearchBar.tsx +++ b/project2/src/components/SearchBar.tsx @@ -1,21 +1,84 @@ /********* - Searchbar, pretty self explainatory. + Searchbar with sort controls: search text, which field to sort by and the direction. + The actual sorting happens on the server, across the whole result set. Will also house the filtering abilities in the future. **********/ +export type SortField = + | 'ID' + | 'NAME' + | 'HP' + | 'ATTACK' + | 'DEFENCE' + | 'SPECIAL_ATTACK' + | 'SPECIAL_DEFENCE' + | 'SPEED'; + +export type SortOrder = 'ASC' | 'DESC'; + +const SORT_OPTIONS: { value: SortField; label: string }[] = [ + { value: 'ID', label: 'Pokédex no.' }, + { value: 'NAME', label: 'Name' }, + { value: 'HP', label: 'HP' }, + { value: 'ATTACK', label: 'Attack' }, + { value: 'DEFENCE', label: 'Defence' }, + { value: 'SPECIAL_ATTACK', label: 'Special attack' }, + { value: 'SPECIAL_DEFENCE', label: 'Special defence' }, + { value: 'SPEED', label: 'Speed' }, +]; + interface SearchBarProps { value: string; onChange: (value: string) => void; + sortBy: SortField; + sortOrder: SortOrder; + onSortByChange: (sortBy: SortField) => void; + onSortOrderChange: (sortOrder: SortOrder) => void; } -export function SearchBar({ value, onChange }: SearchBarProps) { +export function SearchBar({ + value, + onChange, + sortBy, + sortOrder, + onSortByChange, + onSortOrderChange, +}: SearchBarProps) { return ( - onChange(e.target.value)} - /> +
+ onChange(e.target.value)} + /> + + +
); } diff --git a/project2/src/index.css b/project2/src/index.css index b143812..92b5fc4 100644 --- a/project2/src/index.css +++ b/project2/src/index.css @@ -91,6 +91,11 @@ header { align-items: center; justify-content: space-between; } +.search-bar { + display: flex; + align-items: center; + gap: 0.5rem; +} footer { padding: 1rem 2rem; } @@ -109,7 +114,11 @@ select { } header input:focus, -header input:hover { +header input:hover, +header select:focus, +header select:hover, +header button:focus, +header button:hover { background: var(--bg); }