Skip to content

Commit

Permalink
Add sort functionality to server and client
Browse files Browse the repository at this point in the history
  • Loading branch information
itaiche committed Jan 19, 2021
1 parent b9dfcd3 commit c135e2a
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 69 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ Otherwise, it is mandatory.
a. Add at least 3 automated browser tests using puppeteer, testing key features of your choice.
b. Add component tests (using `jest`) to your work from *part 1*.

### part 5
Add a sort component to your system.
Allow sorting by ticket title, creator email and creation date.

#### Client
Add a client component that hightlights the current sort (if any) and states the sort direction.
Clicking on sort, sorts the collection in the server and returns the correct data sorted.
Clicking on a currently active sort changes it's direction (Ascending / Descending).
Make sure the view contains the words : title, data, email and the direction of sort.
Example UI:
![sort](images/sort.png)

#### Server
On the `GET:Tickets` API add parameters to support sort.
Return the correct data sorted according to the request.




## General notes
- Test your work well. Think of edge cases. Think of how users will use it, and make sure your work is of high quality
Expand Down
23 changes: 23 additions & 0 deletions client/src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,30 @@ body {
font-weight: 500;
font-size: 12px;
}
.sort {
padding-top: 5px;

button {
border: none;
padding: 5px;
box-shadow: 0 2px 6px 1px #e1e5e8;
margin:0 2px;
border-radius: 5px;
cursor: pointer;
outline: none;
background-color: white;
color: lightgray;

&.selected {
box-shadow: 0 2px 6px 1px #3899ec;
color: #7a92a5;
}
&:active {
box-shadow: 0 2px 6px 1px #20455e;
color: #7a92a5;
}
}
}
.tickets {

margin: 0;
Expand Down
147 changes: 92 additions & 55 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,104 @@
import React from 'react';
import './App.scss';
import {createApiClient, Ticket} from './api';
import {createApiClient, Ticket, Sort, SortDirection, SortCriteria} from './api';

export type AppState = {
tickets?: Ticket[],
search: string;
tickets?: Ticket[],
search: string;
sort?: Sort
}

const api = createApiClient();

export class App extends React.PureComponent<{}, AppState> {

state: AppState = {
search: ''
}

searchDebounce: any = null;

async componentDidMount() {
this.setState({
tickets: await api.getTickets()
});
}

renderTickets = (tickets: Ticket[]) => {

const filteredTickets = tickets
.filter((t) => (t.title.toLowerCase() + t.content.toLowerCase()).includes(this.state.search.toLowerCase()));


return (<ul className='tickets'>
{filteredTickets.map((ticket) => (<li key={ticket.id} className='ticket'>
<h5 className='title'>{ticket.title}</h5>
<footer>
<div className='meta-data'>By {ticket.userEmail} | { new Date(ticket.creationTime).toLocaleString()}</div>
</footer>
</li>))}
</ul>);
}

onSearch = async (val: string, newPage?: number) => {

clearTimeout(this.searchDebounce);

this.searchDebounce = setTimeout(async () => {
this.setState({
search: val
});
}, 300);
}

render() {
const {tickets} = this.state;

return (<main>
<h1>Tickets List</h1>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{tickets ? <div className='results'>Showing {tickets.length} results</div> : null }
{tickets ? this.renderTickets(tickets) : <h2>Loading..</h2>}
</main>)
}
state: AppState = {
search: ''
}

searchDebounce: any = null;

async componentDidMount() {
this.setState({
tickets: await api.getTickets()
});
}

renderSort = () => {
const {sort} = this.state;
const sortBy = sort && sort.by;
const direction = (sort && sort.direction) || '';
return (
<div className='sort'>
<label>Sort By: </label>
{this.renderSortButton('Title', 'title', sortBy)}
{this.renderSortButton('Date', 'date', sortBy)}
{this.renderSortButton('Email', 'email', sortBy)}
<label id='sort-direction'>{ direction ? (direction === 'ASC' ? ' Ascending' : ' Descending') : '' }</label>
</div>
);
}

renderSortButton = (text: string, criteria: SortCriteria, currentSortBy: SortCriteria | undefined) => {
return <button className={criteria === currentSortBy ? 'selected' : ''} id={`sort-${criteria}`}
onClick={() => {this.getSortedItems({by: criteria})} }>{text}</button>;
}

getSortedItems = async (sortData: Sort) => {
const {sort} = this.state;
if ((sort && sort.by) === sortData.by) {
sortData.direction = (sort && sort.direction) === 'ASC' ? 'DESC' : 'ASC';
} else {
sortData.direction = 'ASC';
}
const tickets = await api.getTickets(sortData);
this.setState({
tickets,
sort:sortData
});
}

renderTickets = (tickets: Ticket[]) => {

const filteredTickets = tickets
.filter((t) => (t.title.toLowerCase() + t.content.toLowerCase()).includes(this.state.search.toLowerCase()));


return (<ul className='tickets'>
{filteredTickets.map((ticket) => (<li key={ticket.id} className='ticket'>
<h5 className='title'>{ticket.title}</h5>
<footer>
<div
className='meta-data'>By {ticket.userEmail} | {new Date(ticket.creationTime).toLocaleString()}</div>
</footer>
</li>))}
</ul>);
}

onSearch = async (val: string, newPage?: number) => {

clearTimeout(this.searchDebounce);

this.searchDebounce = setTimeout(async () => {
this.setState({
search: val
});
}, 300);
}

render() {
const {tickets} = this.state;

return (<main>
<h1>Tickets List</h1>
<header>
<input type="search" placeholder="Search..." onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{this.renderSort()}
{tickets ? <div className='results'>Showing {tickets.length} results</div> : null}
{tickets ? this.renderTickets(tickets) : <h2>Loading..</h2>}
</main>)
}
}

export default App;
export default App;
18 changes: 15 additions & 3 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@ export type Ticket = {
}

export type ApiClient = {
getTickets: () => Promise<Ticket[]>;
getTickets: (sort?: Sort) => Promise<Ticket[]>;
}

export type Sort = {
direction?: SortDirection,
by: SortCriteria
};

export type SortCriteria = 'title' | 'date' | 'email' ;
export type SortDirection = 'ASC' | 'DESC';

const buildQuery = (sort?: Sort) => {
return sort && sort.by ? `?sortBy=${sort.by}&sortDir=${sort.direction || `ASC`}`: '';
}

export const createApiClient = (): ApiClient => {
return {
getTickets: () => {
return axios.get(`http://localhost:3232/api/tickets`).then((res) => res.data);
getTickets: (sort?: Sort) => {
return axios.get(`http://localhost:3232/api/tickets${buildQuery(sort)}`).then((res) => res.data);
}
}
}
Expand Down
Binary file added images/sort.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 30 additions & 11 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import express from 'express';

import bodyParser = require('body-parser');
import { tempData } from './temp-data';
import {tempData} from './temp-data';
import {SortCriteria, SortDirection, Ticket} from '../client/src/api';

const app = express();

Expand All @@ -12,19 +13,37 @@ const PAGE_SIZE = 20;
app.use(bodyParser.json());

app.use((_, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
next();
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
next();
});

app.get('/api/tickets', (req, res) => {

const page = req.query.page || 1;
const sort = (data: Ticket[], by: SortCriteria, dir: SortDirection) => {
interface mapper {
[key: string]: string;
}

const mapToKey: mapper = {date: 'creationTime', email: 'userEmail', title: 'title'};
const key = mapToKey[by];
if(key) {
data.sort((a: Ticket, b: Ticket): number => {
// @ts-ignore
return dir === 'ASC' ? (a[key] < b[key] ? -1 : 0) : (a[key] > b[key] ? -1 : 0)
});
}
};

const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

res.send(paginatedData);
app.get('/api/tickets', (req, res) => {
const page = req.query.page || 1;
const sortBy = req.query.sortBy || '';
const direction = req.query.sortDir || '';
if(sortBy){
sort(tempData, sortBy, direction);
}
const paginatedData = tempData.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);

res.send(paginatedData);
});

app.listen(PORT);
Expand Down

0 comments on commit c135e2a

Please sign in to comment.