diff --git a/frontend/app/components/sort-picker.spec.tsx b/frontend/app/components/sort-picker.spec.tsx
new file mode 100644
index 00000000..6c4fe4cb
--- /dev/null
+++ b/frontend/app/components/sort-picker.spec.tsx
@@ -0,0 +1,50 @@
+import '@testing-library/jest-dom';
+import { fireEvent, waitFor } from '@testing-library/preact';
+import { h } from 'preact';
+
+import { render } from 'tests/utils';
+import * as commentsActions from 'store/comments/actions';
+import type { StoreState } from 'store';
+
+import { SortPicker } from './sort-picker';
+
+const defaultState = { comments: {} as StoreState['comments'], hiddenUsers: {} };
+const stateWithSort = { comments: { sort: '-active' } as StoreState['comments'] };
+
+describe('', () => {
+ it('should render sort picker with options', () => {
+ const { container, queryAllByText, queryByText } = render(, defaultState);
+
+ expect(container.querySelectorAll('option')).toHaveLength(8);
+ expect(queryAllByText('Best')).toHaveLength(2);
+ expect(queryByText('Sort by')).toBeInTheDocument();
+ });
+
+ it('should has static class names', () => {
+ const { container } = render(, defaultState);
+
+ expect(container.querySelector('.sort-picker')).toBeInTheDocument();
+ });
+
+ it('should render selected element', () => {
+ const { container, queryAllByText } = render(, stateWithSort);
+
+ expect(queryAllByText('Recently updated')).toHaveLength(2);
+ expect(container.querySelector('[value="-active"]')?.selected).toBeTruthy();
+ });
+
+ it('should change selected store', async () => {
+ const nextOption = '-controversy';
+ const updateSorting = jest.spyOn(commentsActions, 'updateSorting');
+ const { container } = render(, defaultState);
+ const select = container.querySelector('select') as HTMLSelectElement;
+
+ expect(select).toBeInTheDocument();
+
+ fireEvent.change(select, { target: { value: nextOption } });
+
+ await waitFor(() => expect(updateSorting).toHaveBeenCalledWith(nextOption));
+
+ expect(container.querySelector(`[value="${nextOption}"]`)?.selected).toBeTruthy();
+ });
+});
diff --git a/frontend/app/components/sort-picker.tsx b/frontend/app/components/sort-picker.tsx
new file mode 100644
index 00000000..2e639154
--- /dev/null
+++ b/frontend/app/components/sort-picker.tsx
@@ -0,0 +1,88 @@
+import { h } from 'preact';
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+import { useMemo } from 'preact/hooks';
+import { useSelector, useDispatch } from 'react-redux';
+
+import { StoreState } from 'store';
+import { Select } from 'components/select';
+import { updateSorting } from 'store/comments/actions';
+import type { Sorting } from 'common/types';
+
+export function SortPicker() {
+ const dispatch = useDispatch();
+ const intl = useIntl();
+ const [items, itemsById] = useMemo(() => {
+ const sortOptions = {
+ '-score': intl.formatMessage(messages.best),
+ '+score': intl.formatMessage(messages.worst),
+ '-time': intl.formatMessage(messages.newest),
+ '+time': intl.formatMessage(messages.oldest),
+ '-active': intl.formatMessage(messages.recentlyUpdated),
+ '+active': intl.formatMessage(messages.leastRecentlyUpdated),
+ '-controversy': intl.formatMessage(messages.mostControversial),
+ '+controversy': intl.formatMessage(messages.leastControversial),
+ };
+ type SortItem = { value: string; label: string };
+ const sortItems: SortItem[] = Object.entries(sortOptions).map(([k, v]) => ({ value: k, label: v }));
+ const sortById = sortItems.reduce(
+ (accum, s) => ({ ...accum, [s.value]: s }),
+ {} as Record
+ );
+
+ return [sortItems, sortById];
+ }, [intl]);
+ const sort = useSelector((s: StoreState) => s.comments.sort) || items[0].value;
+ const selected = itemsById[sort];
+
+ function handleSortChange(evt: Event) {
+ const { value } = evt.target as HTMLOptionElement;
+
+ if (!(value in itemsById)) {
+ return;
+ }
+
+ dispatch(updateSorting(value as Sorting));
+ }
+
+ return (
+
+ {' '}
+
+
+ );
+}
+
+const messages = defineMessages({
+ best: {
+ id: 'commentsSort.best',
+ defaultMessage: 'Best',
+ },
+ worst: {
+ id: 'commentsSort.worst',
+ defaultMessage: 'Worst',
+ },
+ newest: {
+ id: 'commentsSort.newest',
+ defaultMessage: 'Newest',
+ },
+ oldest: {
+ id: 'commentsSort.oldest',
+ defaultMessage: 'Oldest',
+ },
+ recentlyUpdated: {
+ id: 'commentsSort.recently-updated',
+ defaultMessage: 'Recently updated',
+ },
+ leastRecentlyUpdated: {
+ id: 'commentsSort.least-recently-updated',
+ defaultMessage: 'Least recently updated',
+ },
+ mostControversial: {
+ id: 'commentsSort.most-controversial',
+ defaultMessage: 'Most controversial',
+ },
+ leastControversial: {
+ id: 'commentsSort.least-controversial',
+ defaultMessage: 'Least controversial',
+ },
+});