move sort picker below comment form
This commit is contained in:
-9
@@ -1,9 +0,0 @@
|
||||
.auth-panel__select-label-value_focused {
|
||||
outline: 1px dotted;
|
||||
outline-color: inherit;
|
||||
|
||||
@supports (outline-color: -webkit-focus-ring-color) {
|
||||
outline-color: -webkit-focus-ring-color;
|
||||
outline-style: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.auth-panel__select-label {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
font-weight: 700;
|
||||
|
||||
&::after {
|
||||
content: '▾';
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
.auth-panel__select {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ interface OwnProps {
|
||||
postInfo: PostInfo;
|
||||
|
||||
signout(): Promise<void>;
|
||||
onSortChange(s: Sorting): Promise<void>;
|
||||
onCommentsChangeReadOnlyMode(readOnly: boolean): Promise<void>;
|
||||
onBlockedUsersShow(): void;
|
||||
onBlockedUsersHide(): void;
|
||||
@@ -35,34 +34,17 @@ interface OwnProps {
|
||||
export interface Props extends OwnProps {
|
||||
intl: IntlShape;
|
||||
theme: Theme;
|
||||
sort: Sorting;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isBlockedVisible: boolean;
|
||||
anonymousUsernameInputValue: string;
|
||||
sortSelectFocused: boolean;
|
||||
}
|
||||
|
||||
class AuthPanelComponent extends Component<Props, State> {
|
||||
state = {
|
||||
isBlockedVisible: false,
|
||||
anonymousUsernameInputValue: 'anon',
|
||||
sortSelectFocused: false,
|
||||
};
|
||||
|
||||
onSortChange = (e: Event) => {
|
||||
const { value } = e.target as HTMLOptionElement;
|
||||
this.props.onSortChange(value as Sorting);
|
||||
};
|
||||
|
||||
onSortFocus = () => {
|
||||
this.setState({ sortSelectFocused: true });
|
||||
};
|
||||
|
||||
onSortBlur = (e: Event) => {
|
||||
this.setState({ sortSelectFocused: false });
|
||||
this.onSortChange(e);
|
||||
};
|
||||
|
||||
toggleBlockedVisibility = () => {
|
||||
@@ -165,35 +147,6 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderSort = () => {
|
||||
const { sort } = this.props;
|
||||
const { sortSelectFocused } = this.state;
|
||||
const sortArray = getSortArray(sort, this.props.intl);
|
||||
|
||||
return (
|
||||
<span className="auth-panel__sort">
|
||||
<FormattedMessage id="commentSort.sort-by" defaultMessage="Sort by" />{' '}
|
||||
<span className="auth-panel__select-label">
|
||||
<span className={b('auth-panel__select-label-value', {}, { focused: sortSelectFocused })}>
|
||||
{sortArray.find((x) => 'selected' in x && x.selected!)!.label}
|
||||
</span>
|
||||
<select
|
||||
className="auth-panel__select"
|
||||
onChange={this.onSortChange}
|
||||
onFocus={this.onSortFocus}
|
||||
onBlur={this.onSortBlur}
|
||||
>
|
||||
{sortArray.map((sort) => (
|
||||
<option key={sort.value} value={sort.value} selected={sort.selected}>
|
||||
{sort.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render({ user, postInfo, theme }: Props, { isBlockedVisible }: State) {
|
||||
const { read_only } = postInfo;
|
||||
const isAdmin = user && user.admin;
|
||||
@@ -208,108 +161,21 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
{isSettingsLabelVisible && this.renderSettingsLabel()}
|
||||
{isSettingsLabelVisible && ' • '}
|
||||
{isAdmin && this.renderReadOnlySwitch()}
|
||||
{isAdmin && ' • '}
|
||||
{isAdmin && read_only && ' • '}
|
||||
{!isAdmin && read_only && (
|
||||
<span className="auth-panel__readonly-label">
|
||||
<FormattedMessage id="authPanel.read-only" defaultMessage="Read-only" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{this.renderSort()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sortMessages = 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',
|
||||
},
|
||||
});
|
||||
|
||||
function getSortArray(currentSort: Sorting, intl: IntlShape) {
|
||||
const sortArray: {
|
||||
value: Sorting;
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
}[] = [
|
||||
{
|
||||
value: '-score',
|
||||
label: intl.formatMessage(sortMessages.best),
|
||||
},
|
||||
{
|
||||
value: '+score',
|
||||
label: intl.formatMessage(sortMessages.worst),
|
||||
},
|
||||
{
|
||||
value: '-time',
|
||||
label: intl.formatMessage(sortMessages.newest),
|
||||
},
|
||||
{
|
||||
value: '+time',
|
||||
label: intl.formatMessage(sortMessages.oldest),
|
||||
},
|
||||
{
|
||||
value: '-active',
|
||||
label: intl.formatMessage(sortMessages.recentlyUpdated),
|
||||
},
|
||||
{
|
||||
value: '+active',
|
||||
label: intl.formatMessage(sortMessages.leastRecentlyUpdated),
|
||||
},
|
||||
{
|
||||
value: '-controversy',
|
||||
label: intl.formatMessage(sortMessages.mostControversial),
|
||||
},
|
||||
{
|
||||
value: '+controversy',
|
||||
label: intl.formatMessage(sortMessages.leastControversial),
|
||||
},
|
||||
];
|
||||
|
||||
return sortArray.map((sort) => {
|
||||
if (sort.value === currentSort) {
|
||||
sort.selected = true;
|
||||
}
|
||||
|
||||
return sort;
|
||||
});
|
||||
}
|
||||
|
||||
export function AuthPanel(props: OwnProps) {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const sort = useSelector<StoreState, Sorting>((state) => state.comments.sort);
|
||||
|
||||
return <AuthPanelComponent intl={intl} theme={theme} sort={sort} {...props} />;
|
||||
return <AuthPanelComponent intl={intl} theme={theme} {...props} />;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ import './auth-panel.css';
|
||||
|
||||
import './__readonly-label/auth-panel__readonly-label.css';
|
||||
import './__column/auth-panel__column.css';
|
||||
import './__select/auth-panel__select.css';
|
||||
import './__select-label/auth-panel__select-label.css';
|
||||
import './__select-label-value/auth-panel__select-label-value.css';
|
||||
import './__sort/auth-panel__sort.css';
|
||||
|
||||
export * from './auth-panel';
|
||||
|
||||
@@ -8,6 +8,8 @@ import { setUser } from 'store/user/actions';
|
||||
import { Input } from 'components/input';
|
||||
import { CrossIcon } from 'components/icons/cross';
|
||||
import { TextareaAutosize } from 'components/textarea-autosize';
|
||||
import { Spinner } from 'components/spinner/spinner';
|
||||
import { Arrow } from 'components/icons/arrow';
|
||||
|
||||
import { Button } from './components/button';
|
||||
import { OAuth } from './components/oauth';
|
||||
@@ -17,7 +19,6 @@ import { getProviders, getTokenInvalidReason } from './auth.utils';
|
||||
import { emailSignin, verifyEmailSignin, anonymousSignin } from './auth.api';
|
||||
|
||||
import styles from './auth.module.css';
|
||||
import { Spinner } from 'components/spinner/spinner';
|
||||
|
||||
export function Auth() {
|
||||
const intl = useIntl();
|
||||
@@ -115,22 +116,7 @@ export function Auth() {
|
||||
);
|
||||
return (
|
||||
<div className={clsx('auth', styles.root)}>
|
||||
<Button
|
||||
className="auth-button"
|
||||
selected={isDropdownShowed}
|
||||
onClick={handleClickSingIn}
|
||||
suffix={
|
||||
<svg width="14" height="14" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6 11.5L14.5 19L22 11"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<Button className="auth-button" selected={isDropdownShowed} onClick={handleClickSingIn} suffix={<Arrow />}>
|
||||
{intl.formatMessage(messages.signin)}
|
||||
</Button>
|
||||
{isDropdownShowed && (
|
||||
|
||||
@@ -11,7 +11,7 @@ type Props = Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'size'> & {
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
export function Button({ children, size, kind, suffix, selected, className, ...props }: Props) {
|
||||
export function Button({ children, size, kind, suffix, selected, className, onChange, ...props }: Props) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(className, styles.button, kind && styles[kind], size && styles[size], {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { h, JSX } from 'preact';
|
||||
|
||||
export function Arrow(props: JSX.HTMLAttributes<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="14" height="14" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M6 11.5L14.5 19L22 11"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
.root__threads {
|
||||
margin-top: 24px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
.sortPicker {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.moreComments {
|
||||
display: block;
|
||||
max-width: 320px;
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
unhideUser,
|
||||
signout,
|
||||
} from 'store/user/actions';
|
||||
import { fetchComments, updateSorting, addComment, updateComment, unsetCommentMode } from 'store/comments/actions';
|
||||
import { fetchComments, addComment, updateComment, unsetCommentMode } from 'store/comments/actions';
|
||||
import { setCommentsReadOnlyState } from 'store/post-info/actions';
|
||||
import { setTheme } from 'store/theme/actions';
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Button } from 'components/auth/components/button';
|
||||
import { Preloader } from 'components/preloader';
|
||||
import { Settings } from 'components/settings';
|
||||
import { AuthPanel } from 'components/auth-panel';
|
||||
import { SortPicker } from 'components/sort-picker';
|
||||
import { CommentForm } from 'components/comment-form';
|
||||
import { Thread } from 'components/thread';
|
||||
import { ConnectedComment as Comment } from 'components/comment/connected-comment';
|
||||
@@ -39,7 +40,6 @@ import { bindActions } from 'utils/actionBinder';
|
||||
import { postMessageToParent, parseMessage } from 'utils/post-message';
|
||||
import { useActions } from 'hooks/useAction';
|
||||
import { setCollapse } from 'store/thread/actions';
|
||||
import { Sorting } from 'common/types';
|
||||
|
||||
import styles from './root.module.css';
|
||||
|
||||
@@ -66,7 +66,6 @@ const mapStateToProps = (state: StoreState) => ({
|
||||
});
|
||||
|
||||
const boundActions = bindActions({
|
||||
updateSorting,
|
||||
fetchComments,
|
||||
setUser,
|
||||
fetchUser,
|
||||
@@ -130,12 +129,6 @@ export class Root extends Component<Props, State> {
|
||||
window.addEventListener('message', this.onMessage);
|
||||
}
|
||||
|
||||
changeSort = async (sort: Sorting) => {
|
||||
if (sort === this.props.sort) return;
|
||||
|
||||
await this.props.updateSorting(sort);
|
||||
};
|
||||
|
||||
checkUrlHash = (e: Event & { newURL: string }) => {
|
||||
const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash;
|
||||
|
||||
@@ -224,7 +217,6 @@ export class Root extends Component<Props, State> {
|
||||
<AuthPanel
|
||||
user={this.props.user}
|
||||
hiddenUsers={this.props.hiddenUsers}
|
||||
onSortChange={this.changeSort}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
postInfo={this.props.info}
|
||||
signout={this.props.signout}
|
||||
@@ -261,7 +253,6 @@ export class Root extends Component<Props, State> {
|
||||
simpleView={StaticStore.config.simple_view}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.props.pinnedComments.length > 0 && (
|
||||
<div
|
||||
className="root__pinned-comments"
|
||||
@@ -282,7 +273,9 @@ export class Root extends Component<Props, State> {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx('sort-picker', styles.sortPicker)}>
|
||||
<SortPicker />
|
||||
</div>
|
||||
{!!this.props.topComments.length && !props.isCommentsLoading && (
|
||||
<div className="root__threads" role="list">
|
||||
{(IS_MOBILE && commentsShown < this.props.topComments.length
|
||||
@@ -305,7 +298,6 @@ export class Root extends Component<Props, State> {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.isCommentsLoading && (
|
||||
<div className="root__threads" role="list">
|
||||
<Preloader className="root__preloader" />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './select';
|
||||
@@ -0,0 +1,30 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
border-radius: 2px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rootFocused {
|
||||
box-shadow: 0 0 0 2px rgba(var(--primary-color), 0.4);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-left: 0.4em;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import clsx from 'clsx';
|
||||
import { Arrow } from 'components/icons/arrow';
|
||||
import { h, JSX } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import styles from './select.module.css';
|
||||
|
||||
type Item = {
|
||||
label: string | number;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
items: Item[];
|
||||
selected: Item;
|
||||
onChange?: JSX.GenericEventHandler<HTMLSelectElement>;
|
||||
};
|
||||
|
||||
export function Select({ items, selected, onChange }: Props) {
|
||||
const [focus, setFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<span className={clsx('select', styles.root, focus && styles.rootFocused)}>
|
||||
{selected.label}
|
||||
<Arrow className={clsx('select-arrow', styles.arrow)} />
|
||||
<select
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
className={clsx('select-element', styles.select)}
|
||||
onChange={onChange}
|
||||
>
|
||||
{items.map((i) => (
|
||||
<option key={i.value} value={i.value} selected={selected.value === i.value}>
|
||||
{i.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './sort-picker';
|
||||
@@ -0,0 +1,90 @@
|
||||
import { h } from 'preact';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { useMemo } from 'preact/hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StoreState } from 'store';
|
||||
import { Select } from 'components/select';
|
||||
import { updateSorting } from 'store/comments/actions';
|
||||
import type { Sorting } from 'common/types';
|
||||
|
||||
import styles from './sort-picker.module.css';
|
||||
|
||||
export function SortPicker() {
|
||||
const dispatch = useDispatch();
|
||||
const sort = useSelector((s: StoreState) => s.comments.sort);
|
||||
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 sort: SortItem[] = Object.entries(sortOptions).map(([k, v]) => ({ value: k, label: v }));
|
||||
const sortById = sort.reduce(
|
||||
(accum, s) => ({ ...accum, [s.value]: s }),
|
||||
{} as Record<keyof typeof sortOptions, SortItem>
|
||||
);
|
||||
|
||||
return [sort, sortById];
|
||||
}, []);
|
||||
|
||||
function handleSortChange(evt: Event) {
|
||||
const { value } = evt.target as HTMLOptionElement;
|
||||
|
||||
if (!(value in itemsById)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(updateSorting(value as Sorting));
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={clsx('sort-picker', styles.root)}>
|
||||
<FormattedMessage id="sort-by" defaultMessage="Sort by" />{' '}
|
||||
<Select items={items} selected={itemsById[sort]} onChange={handleSortChange} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -43,12 +43,12 @@ export function getPinnedComments(threads: Node[]): Comment[] {
|
||||
return threads.reduce((acc: Comment[], thread: Node) => acc.concat(findPinnedComments(thread)), []);
|
||||
}
|
||||
|
||||
export function getInitialSort() {
|
||||
export function getInitialSort(): Sorting {
|
||||
const sort = getItem(LS_SORT_KEY) as Sorting;
|
||||
|
||||
if (sort) {
|
||||
return sort;
|
||||
if (!sort) {
|
||||
return DEFAULT_SORT;
|
||||
}
|
||||
|
||||
return DEFAULT_SORT;
|
||||
return sort;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user