Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions project2/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[];

Expand Down
41 changes: 36 additions & 5 deletions project2/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -48,12 +64,14 @@ function App() {
const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(10);
const [sortBy, setSortBy] = useState<SortField>('ID');
const [sortOrder, setSortOrder] = useState<SortOrder>('ASC');
const [selectedId, setSelectedId] = useState<number | null>(null);

// Get data and utilize Apollo cache, it handles basicly everything
const { loading, error, data, previousData } = useQuery<PokemonsPageData>(
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
Expand All @@ -69,7 +87,20 @@ function App() {
<>
<header>
<h1>PokeDOX</h1>
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<SearchBar
value={searchTerm}
onChange={setSearchTerm}
sortBy={sortBy}
sortOrder={sortOrder}
onSortByChange={(s) => {
setSortBy(s);
setPage(1);
}}
onSortOrderChange={(o) => {
setSortOrder(o);
setPage(1);
}}
/>
</header>
<main>
{pageInfo && (
Expand Down
81 changes: 72 additions & 9 deletions project2/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<input
type="search"
placeholder="Search"
aria-label="Search pokemon"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
<div className="search-bar">
<input
type="search"
placeholder="Search"
aria-label="Search pokemon"
value={value}
onChange={(e) => onChange(e.target.value)}
/>
<label>
Sort by:{' '}
<select
value={sortBy}
onChange={(e) => onSortByChange(e.target.value as SortField)}
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
<button
type="button"
aria-label={
sortOrder === 'ASC'
? 'Ascending, switch to descending'
: 'Descending, switch to ascending'
}
onClick={() =>
onSortOrderChange(sortOrder === 'ASC' ? 'DESC' : 'ASC')
}
>
{sortOrder === 'ASC' ? '↑' : '↓'}
</button>
</div>
);
}
11 changes: 10 additions & 1 deletion project2/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
}

Expand Down