move sort picker below comment form

This commit is contained in:
Pavel Mineev
2021-08-08 14:25:10 -05:00
committed by Umputun
parent be41016266
commit a04204b3d5
18 changed files with 199 additions and 204 deletions
@@ -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';
+3 -17
View File
@@ -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], {
+15
View File
@@ -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;
+5 -13
View File
@@ -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" />
+1
View File
@@ -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;
}
+39
View File
@@ -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',
},
});
+4 -4
View File
@@ -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;
}