feat: add mod menu

This commit is contained in:
Tom (plebeius.eth)
2024-06-03 13:51:41 +02:00
parent 018e030987
commit daaba0f006
4 changed files with 260 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
export { default } from './mod-menu';
@@ -0,0 +1,84 @@
.checkbox {
display: inline-block;
padding: 3px 7px 3px 4px;
filter: var(--filter80);
}
.button {
color: var(--text-info);
font-weight: bold;
text-decoration: none;
padding: 0 4px 0 4px;
cursor: pointer;
}
.modal {
z-index: 7;
background-color: var(--challenge-modal-background-color);
padding: 1px 0;
padding-top: 2px;
color: var(--text);
border: var(--challenge-modal-border);
min-width: 200px;
}
.modal input[type="checkbox"] {
margin-right: 3px;
outline: none;
cursor: pointer;
margin: 0 3px;
vertical-align: middle;
transform: translateY(-2px);
}
.menuItem {
padding: 2px 3px 1px 3px;
}
.menuItem label {
cursor: pointer;
}
.menuReason {
padding-bottom: 2px;
}
.menuItem input[type="text"] {
padding: 2px;
box-shadow: var(--box-shadow-input);
margin-top: 2px;
width: calc(100% - 6px);
margin-bottom: 2px;
}
.menuItem input[type="text"], .menuItem input[type='number'] {
border: var(--post-form-field-input-border);
outline: none;
}
.menuItem input[type="text"]:focus, .menuItem input[type='number']:focus {
border: var(--post-form-field-input-focus-border);
}
.bottom {
clear: both;
display: block;
}
.bottom button {
text-transform: capitalize;
cursor: pointer;
padding: 2px 6px 3px;
margin-left: 2px;
margin-bottom: 2px;
}
.optional {
color: var(--text-info);
}
@supports (-moz-appearance: none) {
.banInput {
width: 3.5em;
}
}
+175
View File
@@ -0,0 +1,175 @@
import { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { autoUpdate, flip, FloatingFocusManager, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react';
import { PublishCommentEditOptions, useComment, useEditedComment, usePublishCommentEdit } from '@plebbit/plebbit-react-hooks';
import styles from './mod-menu.module.css';
import { alertChallengeVerificationFailed } from '../../../lib/utils/challenge-utils';
import useChallengesStore from '../../../stores/use-challenges-store';
import _ from 'lodash';
const { addChallenge } = useChallengesStore.getState();
type ModMenuProps = {
cid: string;
isCommentAuthorMod?: boolean;
};
const ModMenu = ({ cid, isCommentAuthorMod }: ModMenuProps) => {
const { t } = useTranslation();
let post: any;
const comment = useComment({ commentCid: cid });
const { editedComment } = useEditedComment({ comment });
if (editedComment) {
post = editedComment;
} else if (comment) {
post = comment;
}
const isReply = post?.parentCid;
const [isModMenuOpen, setIsModMenuOpen] = useState(false);
const defaultPublishOptions: PublishCommentEditOptions = {
removed: post?.removed,
locked: post?.locked,
spoiler: post?.spoiler,
pinned: post?.pinned,
commentAuthor: { banExpiresAt: post?.banExpiresAt },
commentCid: post?.cid,
subplebbitAddress: post?.subplebbitAddress,
onChallenge: (...args: any) => addChallenge([...args, post]),
onChallengeVerification: alertChallengeVerificationFailed,
onError: (error: Error) => {
console.warn(error);
alert(error.message);
},
};
const [publishCommentEditOptions, setPublishCommentEditOptions] = useState(defaultPublishOptions);
const { publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);
const [banDuration, setBanDuration] = useState(1);
const daysToTimestampInSeconds = (days: number) => {
const now = new Date();
now.setDate(now.getDate() + days);
return Math.floor(now.getTime() / 1000);
};
const onCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
const { id, checked } = e.target;
if (id === 'banUser') {
setPublishCommentEditOptions((state) => ({
...state,
commentAuthor: { ...state.commentAuthor, banExpiresAt: checked ? daysToTimestampInSeconds(banDuration) : undefined },
}));
} else {
setPublishCommentEditOptions((state) => ({ ...state, [id]: checked }));
}
};
const onBanDurationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const days = parseInt(e.target.value, 10) || 1;
setBanDuration(days);
setPublishCommentEditOptions((state) => ({
...state,
commentAuthor: { ...state.commentAuthor, banExpiresAt: daysToTimestampInSeconds(days) },
}));
};
const onReason = (e: React.ChangeEvent<HTMLInputElement>) =>
setPublishCommentEditOptions((state) => ({ ...state, reason: e.target.value ? e.target.value : undefined }));
const { refs, floatingStyles, context } = useFloating({
placement: 'bottom-start',
open: isModMenuOpen,
onOpenChange: setIsModMenuOpen,
middleware: [offset(2), flip({ fallbackAxisSideDirection: 'end' }), shift()],
whileElementsMounted: autoUpdate,
});
const click = useClick(context);
const dismiss = useDismiss(context);
const role = useRole(context);
const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]);
const headingId = useId();
const handleSaveClick = async () => {
await publishCommentEdit();
setIsModMenuOpen(false);
};
return (
<>
<span className={styles.checkbox} ref={refs.setReference} {...(cid && getReferenceProps())}>
<input type='checkbox' onChange={() => cid && setIsModMenuOpen(!isModMenuOpen)} checked={isModMenuOpen} />
</span>
{isModMenuOpen && (
<FloatingFocusManager context={context} modal={false}>
<div className={styles.modal} ref={refs.setFloating} style={floatingStyles} aria-labelledby={headingId} {...getFloatingProps()}>
<div className={styles.ModMenu}>
<div className={styles.menuItem}>
<label>
[
<input onChange={onCheckbox} checked={publishCommentEditOptions.removed} type='checkbox' id='removed' />
{_.capitalize(t('remove'))}? ]
</label>
</div>
{!isReply && (
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.locked} type='checkbox' id='locked' />
{_.capitalize(t('close_thread'))}?
</label>
]
</div>
)}
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.spoiler} type='checkbox' id='spoiler' />
{_.capitalize(t('spoiler'))}?
</label>
]
</div>
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={publishCommentEditOptions.pinned} type='checkbox' id='pinned' />
{_.capitalize(t('sticky'))}?
</label>
]
</div>
{!isCommentAuthorMod && (
<div className={styles.menuItem}>
[
<label>
<input onChange={onCheckbox} checked={!!publishCommentEditOptions.commentAuthor?.banExpiresAt} type='checkbox' id='banUser' />
<Trans
i18nKey='ban_user_for'
shouldUnescape={true}
components={{ 1: <input className={styles.banInput} onChange={onBanDurationChange} type='number' min={1} max={100} defaultValue={banDuration} /> }}
/>
?
</label>
]
</div>
)}
<div className={`${styles.menuItem} ${styles.menuReason}`}>
{_.capitalize(t('reason'))}? <span className={styles.optional}>({t('optional')})</span>
<input type='text' onChange={onReason} defaultValue={post?.reason} size={14} />
</div>
<div className={styles.bottom}>
<button onClick={handleSaveClick}>{t('save')}</button>
</div>
</div>
</div>
</FloatingFocusManager>
)}
</>
);
};
export default ModMenu;