Skip to content

Commit

Permalink
Merge pull request #16 from Ayon95/cabin_filter_and_sort
Browse files Browse the repository at this point in the history
Add cabin filter and sort controls
  • Loading branch information
Ayon95 authored Feb 20, 2024
2 parents c54b5f3 + 1baece9 commit 27ad55f
Show file tree
Hide file tree
Showing 14 changed files with 512 additions and 13 deletions.
60 changes: 60 additions & 0 deletions src/features/cabins/CabinFilterAndSortControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styled from 'styled-components';

import Filter from '@/ui/Filter';
import SortDropdown from '@/ui/SortDropdown';
import { CabinFields } from '@/types/cabins';
import { SortDirections } from '@/types/sort';

type SortOptions = {
label: string;
value: `${CabinFields}-${SortDirections}`;
}[];

const filterOptions = [
{ label: 'Discount', value: 'true' },
{ label: 'No discount', value: 'false' },
];

const sortOptions: SortOptions = [
{
label: 'Name (A-Z)',
value: 'name-asc',
},
{
label: 'Name (Z-A)',
value: 'name-desc',
},
{
label: 'Price (lowest to highest)',
value: 'regular_price-asc',
},
{
label: 'Price (highest to lowest)',
value: 'regular_price-desc',
},
{
label: 'Max capacity (lowest to highest)',
value: 'max_capacity-asc',
},
{
label: 'Max capacity (highest to lowest)',
value: 'max_capacity-desc',
},
];

function CabinFilterAndSortControls() {
return (
<Container>
<Filter<CabinFields> options={filterOptions} field="discount" />
<SortDropdown options={sortOptions} />
</Container>
);
}

export default CabinFilterAndSortControls;

const Container = styled.div`
& > *:first-child {
margin-bottom: 1rem;
}
`;
47 changes: 42 additions & 5 deletions src/features/cabins/CabinTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { useSearchParams } from 'react-router-dom';

import CabinRow from './CabinRow';
import { Tables } from '@/types/database';
Expand All @@ -8,13 +9,49 @@ import { useDeleteCabin } from './hooks/useDeleteCabin';
import Modal from '@/ui/Modal/Modal';
import UpdateCabinForm from './UpdateCabinForm';
import Table from '@/ui/Table';
import { CabinFields } from '@/types/cabins';
import { createObjectCompareFunction } from '@/utils/helpers';
import { SortDirections } from '@/types/sort';

type Cabin = Tables<'cabin'>;

interface CabinTableProps {
cabins: Tables<'cabin'>[];
cabins: Cabin[];
}

function CabinTable({ cabins }: CabinTableProps) {
const [selectedCabin, setSelectedCabin] = useState<null | Tables<'cabin'>>(null);
const [selectedCabin, setSelectedCabin] = useState<null | Cabin>(null);
const [searchParams] = useSearchParams();

const discountFilterValue = searchParams.get('discount');
const sortValue = searchParams.get('sort');

const filteredCabins = discountFilterValue ? getFilteredCabins() : cabins;
const sortedCabins = getSortedCabins(filteredCabins);

function getFilteredCabins() {
return cabins.filter(cabin => {
if (discountFilterValue === 'true') {
return cabin.discount !== null && cabin.discount > 0;
}
if (discountFilterValue === 'false') {
return cabin.discount === null || cabin.discount === 0;
}
});
}

function getSortedCabins(cabins: Cabin[]) {
if (sortValue) {
const [sortBy, sortDirection] = sortValue.split('-');
const compareFunction = createObjectCompareFunction<Cabin>(
sortBy as CabinFields,
sortDirection as SortDirections
);

return [...cabins].sort(compareFunction);
}
return cabins;
}

const {
shouldShowModal: shouldShowUpdateModal,
Expand Down Expand Up @@ -47,12 +84,12 @@ function CabinTable({ cabins }: CabinTableProps) {
}
}

function showUpdateModalForSelectedCabin(cabin: Tables<'cabin'>) {
function showUpdateModalForSelectedCabin(cabin: Cabin) {
setSelectedCabin(cabin);
openUpdateModal();
}

function showConfirmDeleteModalForSelectedCabin(cabin: Tables<'cabin'>) {
function showConfirmDeleteModalForSelectedCabin(cabin: Cabin) {
setSelectedCabin(cabin);
openConfirmDeleteModal();
}
Expand All @@ -74,7 +111,7 @@ function CabinTable({ cabins }: CabinTableProps) {
</Table.Row>
</Table.Head>
<Table.Body>
{cabins.map(cabin => (
{sortedCabins.map(cabin => (
<CabinRow
cabin={cabin}
key={cabin.id}
Expand Down
13 changes: 11 additions & 2 deletions src/features/cabins/__tests__/CabinTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import { renderWithQueryClient } from '@/test/utils';

import CabinTable from '../CabinTable';
import { cabins } from '@/test/fixtures/cabins';
import { MemoryRouter } from 'react-router-dom';

function setup() {
renderWithQueryClient(
<MemoryRouter>
<CabinTable cabins={cabins} />
</MemoryRouter>
);
}

describe('CabinTable', () => {
it('should have all column headers in the correct order', async () => {
renderWithQueryClient(<CabinTable cabins={cabins} />);
setup();

const columnHeaders = await screen.findAllByRole('columnheader');

Expand All @@ -19,7 +28,7 @@ describe('CabinTable', () => {
});

it('should have rows with cabin details in the correct order', async () => {
renderWithQueryClient(<CabinTable cabins={cabins} />);
setup();

const cabinRows = await screen.findAllByRole('row');
// The first row contains column headers
Expand Down
3 changes: 2 additions & 1 deletion src/pages/Cabins.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Spinner from '@/ui/spinner/Spinner';
import { useCabins } from '@/features/cabins/hooks/useCabins';
import CreateCabinForm from '@/features/cabins/CreateCabinForm';
import { useUser } from '@/features/authentication/hooks/useUser';
import CabinFilterAndSortControls from '@/features/cabins/CabinFilterAndSortControls';

function Cabins() {
const { data: user } = useUser();
Expand All @@ -14,7 +15,7 @@ function Cabins() {
<>
<FlexRow>
<Heading as="h1">Cabins</Heading>
<span>Filter / Sort</span>
<CabinFilterAndSortControls />
</FlexRow>
{isLoading ? (
<Spinner />
Expand Down
151 changes: 147 additions & 4 deletions src/pages/__tests__/Cabins.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { rest } from 'msw';
import { Toaster } from 'react-hot-toast';
import { MemoryRouter } from 'react-router-dom';

import { renderWithQueryClient } from '@/test/utils';
import Cabins from '../Cabins';
Expand All @@ -19,15 +20,23 @@ const updateCabinModalTitleRegex = /update cabin/i;
const confirmDeleteButtonRegex = /delete/i;
const confirmDeleteModalTitleRegex = /are you sure you want to delete?/i;

function setupWithLogin() {
function setup() {
renderWithQueryClient(
<MemoryRouter>
<Cabins />
</MemoryRouter>
);
}

function setupWithLogin(initialPath: string = '') {
// Setting logged in user
vi.spyOn(supabase.auth, 'getSession').mockResolvedValue(userSession);

renderWithQueryClient(
<>
<MemoryRouter initialEntries={[initialPath]}>
<Cabins />
<Toaster />
</>
</MemoryRouter>
);
}

Expand All @@ -37,13 +46,147 @@ afterEach(() => {

describe('Cabins', () => {
it('should not show any cabins if there is no logged in user', async () => {
renderWithQueryClient(<Cabins />);
setup();

const noCabinsTextElement = await screen.findByText(/no cabins to show/i);

expect(noCabinsTextElement).toBeInTheDocument();
});

it('should show discount filter controls', () => {
setup();

const elements = [
screen.getByRole('button', { name: /all/i }),
screen.getByRole('button', { name: /^discount$/i }),
screen.getByRole('button', { name: /^no discount$/i }),
];

elements.forEach(element => {
expect(element).toBeInTheDocument();
});
});

it('should show a select control for sorting by various options', () => {
setup();

const sortBySelect = screen.getByLabelText(/sort by/i);

expect(sortBySelect).toBeInTheDocument();

const options = [
within(sortBySelect).getByRole('option', { name: /sort by/i }),
within(sortBySelect).getByRole('option', { name: /^name \(a-z\)$/i }),
within(sortBySelect).getByRole('option', { name: /^name \(z-a\)$/i }),
within(sortBySelect).getByRole('option', { name: /^price \(lowest to highest\)$/i }),
within(sortBySelect).getByRole('option', { name: /^price \(highest to lowest\)$/i }),
within(sortBySelect).getByRole('option', { name: /^max capacity \(lowest to highest\)$/i }),
within(sortBySelect).getByRole('option', { name: /^max capacity \(highest to lowest\)$/i }),
];

options.forEach(option => {
expect(option).toBeInTheDocument();
});
});

it('should only show cabins with discount if discount filter is selected', async () => {
setupWithLogin('?discount=true');

const cabinRows = await screen.findAllByRole('row');

// In cabins fixture, there is one cabin with no discount
// The first row in cabinRows is the column header row
expect(cabinRows.length - 1).toEqual(cabins.length - 1);
});

it('should only show cabins with no discount if no discount filter is selected', async () => {
setupWithLogin('?discount=false');

const cabinRows = await screen.findAllByRole('row');

// In cabins fixture, there is one cabin with no discount
// The first row in cabinRows is the column header row
expect(cabinRows.length - 1).toEqual(1);
});

it('should sort by name in ascending order correctly', async () => {
setupWithLogin('?sort=name-asc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[1]).toHaveTextContent('Test 001');
expect(secondCabinCells[1]).toHaveTextContent('Test 002');
expect(thirdCabinCells[1]).toHaveTextContent('Test 003');
});

it('should sort by name in descending order correctly', async () => {
setupWithLogin('?sort=name-desc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[1]).toHaveTextContent('Test 003');
expect(secondCabinCells[1]).toHaveTextContent('Test 002');
expect(thirdCabinCells[1]).toHaveTextContent('Test 001');
});

it('should sort by price in ascending order correctly', async () => {
setupWithLogin('?sort=regular_price-asc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[3]).toHaveTextContent('$250.00');
expect(secondCabinCells[3]).toHaveTextContent('$500.00');
expect(thirdCabinCells[3]).toHaveTextContent('$700.00');
});

it('should sort by price in descending order correctly', async () => {
setupWithLogin('?sort=regular_price-desc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[3]).toHaveTextContent('$700.00');
expect(secondCabinCells[3]).toHaveTextContent('$500.00');
expect(thirdCabinCells[3]).toHaveTextContent('$250.00');
});

it('should sort by max capacity in ascending order correctly', async () => {
setupWithLogin('?sort=max_capacity-asc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[2]).toHaveTextContent('2');
expect(secondCabinCells[2]).toHaveTextContent('4');
expect(thirdCabinCells[2]).toHaveTextContent('6');
});

it('should sort by max capacity in descending order correctly', async () => {
setupWithLogin('?sort=max_capacity-desc');

const cabinRows = await screen.findAllByRole('row');
const firstCabinCells = within(cabinRows[1]).getAllByRole('cell');
const secondCabinCells = within(cabinRows[2]).getAllByRole('cell');
const thirdCabinCells = within(cabinRows[3]).getAllByRole('cell');

expect(firstCabinCells[2]).toHaveTextContent('6');
expect(secondCabinCells[2]).toHaveTextContent('4');
expect(thirdCabinCells[2]).toHaveTextContent('2');
});

it('should delete a cabin after confirmation and show a deletion success message', async () => {
setupWithLogin();

Expand Down
13 changes: 13 additions & 0 deletions src/test/fixtures/cabins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const cabins: Tables<'cabin'>[] = [
image_url: 'test url 1',
user_id: user.id,
},

{
id: 2,
created_at: '2023-09-16T01:38:13.379438+00:00',
Expand All @@ -24,4 +25,16 @@ export const cabins: Tables<'cabin'>[] = [
image_url: 'test url 2',
user_id: user.id,
},

{
id: 3,
created_at: '2023-09-16T01:38:13.379438+00:00',
name: 'Test 003',
max_capacity: 6,
regular_price: 700,
discount: 0,
description: 'Comfortable cabin for a large family',
image_url: 'test url 3',
user_id: user.id,
},
];
Loading

0 comments on commit 27ad55f

Please sign in to comment.