Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to report #1111

Merged
merged 12 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/forms/signupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TERMS_OF_SERVICE_URL } from '@root/constants/externalLinks';
import { blueButton } from '@root/helpers/className';
import classNames from 'classnames';
import Link from 'next/link';
Expand Down Expand Up @@ -219,7 +220,7 @@ export default function SignupForm({ recaptchaPublicKey }: SignupFormProps) {
<div className='flex gap-3'>
<input type='checkbox' id='terms_agree_checkbox' required />
<label htmlFor='terms_agree_checkbox' className='text-xs'>
I agree to the <a className='underline' href='https://docs.google.com/document/d/e/2PACX-1vR4E-RcuIpXSrRtR3T3y9begevVF_yq7idcWWx1A-I9w_VRcHhPTkW1A7DeUx2pGOcyuKifEad3Qokn/pub' rel='noreferrer' target='_blank'>terms of service</a> and reviewed the <a className='underline' href='https://docs.google.com/document/d/e/2PACX-1vSNgV3NVKlsgSOEsnUltswQgE8atWe1WCLUY5fQUVjEdu_JZcVlRkZcpbTOewwe3oBNa4l7IJlOnUIB/pub' rel='noreferrer' target='_blank'>privacy policy</a>.
I agree to the <a className='underline' href={TERMS_OF_SERVICE_URL} rel='noreferrer' target='_blank'>terms of service</a> and reviewed the <a className='underline' href='https://docs.google.com/document/d/e/2PACX-1vSNgV3NVKlsgSOEsnUltswQgE8atWe1WCLUY5fQUVjEdu_JZcVlRkZcpbTOewwe3oBNa4l7IJlOnUIB/pub' rel='noreferrer' target='_blank'>privacy policy</a>.
</label>
</div>
<div className='flex justify-center'>
Expand Down
29 changes: 28 additions & 1 deletion components/level/info/levelDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import ArchiveLevelModal from '@root/components/modal/archiveLevelModal';
import DeleteLevelModal from '@root/components/modal/deleteLevelModal';
import EditLevelModal from '@root/components/modal/editLevelModal';
import PublishLevelModal from '@root/components/modal/publishLevelModal';
import ReportModal from '@root/components/modal/reportModal';
import SaveToCollectionModal from '@root/components/modal/saveToCollectionModal';
import UnpublishLevelModal from '@root/components/modal/unpublishLevelModal';
import { ReportType } from '@root/constants/ReportType';
import { AppContext } from '@root/contexts/appContext';
import { PageContext } from '@root/contexts/pageContext';
import isCurator from '@root/helpers/isCurator';
Expand All @@ -27,13 +29,17 @@ export default function LevelDropdown({ level }: LevelDropdownProps) {
const [isSaveToCollectionOpen, setIsSaveToCollectionOpen] = useState(false);
const [isUnpublishLevelOpen, setIsUnpublishLevelOpen] = useState(false);
const { mutatePlayLater, playLater, user } = useContext(AppContext);
const { setPreventKeyDownEvent } = useContext(PageContext);
const { setPreventKeyDownEvent, setModal } = useContext(PageContext);

const isAuthor = level.userId === user?._id || level.userId._id === user?._id;
const canEdit = isAuthor || isCurator(user);
const boldedLevelName = <span className='font-bold'>{level.name}</span>;
const isInPlayLater = !!(playLater && playLater[level._id.toString()]);

const modal = <ReportModal targetId={level._id.toString()} reportType={ReportType.LEVEL} />;
const reportLevel = async () => {
setModal(modal);
};
const fetchPlayLater = async (remove: boolean) => {
if (!user) {
return;
Expand Down Expand Up @@ -324,7 +330,28 @@ export default function LevelDropdown({ level }: LevelDropdownProps) {
</>
}
</>}
<Menu.Item>
{({ active }) => (
<div
className={classNames('flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3 text-yellow-500')}
onClick={() => {
reportLevel();
}}
style={{
backgroundColor: active ? 'var(--bg-color-3)' : undefined,
}}
>
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' className='bi bi-exclamation-triangle' viewBox='0 0 16 16'>
<path d='M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z' />
<path d='M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z' />
</svg>
Report
</div>
)}
</Menu.Item>

</div>

</Menu.Items>
</Transition>
</Menu>
Expand Down
50 changes: 49 additions & 1 deletion components/level/reviews/commentThread.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Menu, Transition } from '@headlessui/react';
import ReportModal from '@root/components/modal/reportModal';
import { ReportType } from '@root/constants/ReportType';
import { PageContext } from '@root/contexts/pageContext';
import classNames from 'classnames';
import { Types } from 'mongoose';
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { Fragment, useContext, useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
import ReactTextareaAutosize from 'react-textarea-autosize';
import { KeyedMutator } from 'swr';
Expand Down Expand Up @@ -30,6 +34,11 @@ export default function CommentThread({ className, comment, mutateComments, onSe
const [totalRows, setTotalRows] = useState(comment.totalReplies || 0);
const [page, setPage] = useState(0);
const { user } = useContext(AppContext);
const { setModal } = useContext(PageContext);
const modal = <ReportModal targetId={comment._id.toString()} reportType={ReportType.COMMENT} />;
const reportComment = async () => {
setModal(modal);
};

useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -189,6 +198,45 @@ export default function CommentThread({ className, comment, mutateComments, onSe
}
<FormattedDate date={comment.createdAt} />
</div>
{ user && comment.author._id !== user._id && <Menu as='div' className='relative'>
<Menu.Button className='flex items-center' id='dropdownMenuBtn' aria-label='dropdown menu'>
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke='currentColor' className='w-6 h-6 hover:opacity-100 opacity-50'>
<path strokeLinecap='round' strokeLinejoin='round' d='M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z' />
</svg>
</Menu.Button>
<Transition
as={Fragment}
enter='transition ease-out duration-100'
enterFrom='transform opacity-0 scale-95'
enterTo='transform opacity-100 scale-100'
leave='transition ease-in duration-75'
leaveFrom='transform opacity-100 scale-100'
leaveTo='transform opacity-0 scale-95'
>
<Menu.Items className='absolute right-0 m-1 w-fit origin-top-right rounded-[10px] shadow-lg border z-20 bg-1 border-color-3'>
<Menu.Item>
{({ active }) => (
<div
className={classNames('flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3 text-yellow-500')}
onClick={() => {
reportComment();
}}
style={{
backgroundColor: active ? 'var(--bg-color-3)' : undefined,
}}
>
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' className='bi bi-exclamation-triangle' viewBox='0 0 16 16'>
<path d='M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z' />
<path d='M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z' />
</svg>
Report
</div>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
}
{(comment.author._id.toString() === user?._id.toString() || (user?._id === comment.target)) && (
<button
className='text-white font-bold p-1 rounded-lg text-sm disabled:opacity-25 '
Expand Down
1 change: 1 addition & 0 deletions components/level/reviews/formattedReview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export default function FormattedReview({ hideBorder, inModal, level, onEditClic
</div>
{onEditClick && user && (
<ReviewDropdown
review={review}
inModal={inModal}
onEditClick={onEditClick}
userId={user._id.toString()}
Expand Down
39 changes: 36 additions & 3 deletions components/level/reviews/reviewDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { Menu, Transition } from '@headlessui/react';
import ReportModal from '@root/components/modal/reportModal';
import { ReportType } from '@root/constants/ReportType';
import { AppContext } from '@root/contexts/appContext';
import { LevelContext } from '@root/contexts/levelContext';
import { PageContext } from '@root/contexts/pageContext';
import isCurator from '@root/helpers/isCurator';
import isGuest from '@root/helpers/isGuest';
import Review from '@root/models/db/review';
import classNames from 'classnames';
import React, { Fragment, useContext, useState } from 'react';
import toast from 'react-hot-toast';
import DeleteReviewModal from '../../modal/deleteReviewModal';

interface ReviewDropdownProps {
review: Review
inModal?: boolean;
onEditClick: () => void;
userId: string;
}

export default function ReviewDropdown({ inModal, onEditClick, userId }: ReviewDropdownProps) {
export default function ReviewDropdown({ inModal, onEditClick, userId, review }: ReviewDropdownProps) {
const { setModal } = useContext(PageContext);
const [isDeleteReviewOpen, setIsDeleteReviewOpen] = useState(false);
const levelContext = useContext(LevelContext);
const { setPreventKeyDownEvent } = useContext(PageContext);
Expand All @@ -22,10 +29,15 @@ export default function ReviewDropdown({ inModal, onEditClick, userId }: ReviewD
const canEdit = userId === user?._id.toString() || isCurator(user);
const isNotAuthor = user?._id.toString() !== userId;

if (!canEdit) {
if (!user || isGuest(user)) {
return null;
}

const modal = <ReportModal targetId={review._id.toString()} reportType={ReportType.REVIEW} />;
const reportReview = async () => {
setModal(modal);
};

return (<>
<Menu as='div' className='relative'>
<Menu.Button className='flex items-center' id='dropdownMenuBtn' aria-label='dropdown menu'>
Expand All @@ -45,6 +57,25 @@ export default function ReviewDropdown({ inModal, onEditClick, userId }: ReviewD
<Menu.Items className='absolute right-0 m-1 w-fit origin-top-right rounded-[10px] shadow-lg border z-20 bg-1 border-color-3'>
<div className='px-1 py-1'>
<Menu.Item>
{({ active }) => (
<div
className={classNames('flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3 text-yellow-500')}
onClick={() => {
reportReview();
}}
style={{
backgroundColor: active ? 'var(--bg-color-3)' : undefined,
}}
>
<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' className='bi bi-exclamation-triangle' viewBox='0 0 16 16'>
<path d='M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z' />
<path d='M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z' />
</svg>
Report
</div>
)}
</Menu.Item>
{canEdit && <Menu.Item>
{({ active }) => (
<div
className={classNames('flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3', { 'text-red-500': isNotAuthor })}
Expand All @@ -60,7 +91,8 @@ export default function ReviewDropdown({ inModal, onEditClick, userId }: ReviewD
</div>
)}
</Menu.Item>
<Menu.Item>
}
{ canEdit && <Menu.Item>
{({ active }) => (
<div
className={classNames('flex w-full items-center rounded-md cursor-pointer px-3 py-2 gap-3', { 'text-red-500': isNotAuthor })}
Expand All @@ -82,6 +114,7 @@ export default function ReviewDropdown({ inModal, onEditClick, userId }: ReviewD
</div>
)}
</Menu.Item>
}
</div>
</Menu.Items>
</Transition>
Expand Down
148 changes: 148 additions & 0 deletions components/modal/reportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { TERMS_OF_SERVICE_URL } from '@root/constants/externalLinks';
import { ReportReason } from '@root/constants/ReportReason';
import { ReportType } from '@root/constants/ReportType';
import { PageContext } from '@root/contexts/pageContext';
import Link from 'next/link';
import React, { useContext, useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import Modal from '.';

interface ReportModalProps {
targetId: string;
reportType: ReportType;

}

export default function ReportModal({ targetId, reportType }: ReportModalProps) {
const { setModal, setPreventKeyDownEvent } = useContext(PageContext);
const [reason, setReason] = useState<ReportReason | ''>('');
const [message, setMessage] = useState('');
const fileReport = async () => {
const confirm = window.confirm('Are you sure you want to report this ' + reportType.toLocaleLowerCase() + '?');

if (!confirm) {
return;
}

const res = await fetch('/api/report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
targetId: targetId,
reportReason: 'OTHER',
reportType: reportType,
message: reason,
}),
});
const resp = await res.json();

toast.dismiss();

if (res.status !== 200) {
toast.error(resp.error || 'Error reporting this ' + reportType.toLocaleLowerCase() + '. Please try again later.');

return;
}

toast.success('Report filed successfully. Please allow some time for review!');
setModal(null);
};

useEffect(() => {
setPreventKeyDownEvent(true);

return () => {
setPreventKeyDownEvent(false);
};
}
, [setPreventKeyDownEvent]);
const reportReasons = {
[ReportType.COMMENT]: [ReportReason.SPAM, ReportReason.HARASSMENT],
[ReportType.LEVEL]: [ReportReason.SPAM, ReportReason.HARASSMENT],
[ReportType.REVIEW]: [ReportReason.SPAM, ReportReason.HARASSMENT, ReportReason.REVIEW_BOMBING],
};
const reasonDisplayName = {
[ReportReason.HARASSMENT]: 'Harassment',
[ReportReason.OTHER]: 'Other',
[ReportReason.REVIEW_BOMBING]: 'Review bombing',
[ReportReason.SPAM]: 'Spam',
};

const resasonTypeTipsBullets = {
[ReportReason.HARASSMENT]: [
'The vibe of our community is meant to be positive and welcoming. Please report any behavior that goes against this.',
'Harrassment is words or behavior that are intended to offend, threaten, or demeans a person.',
'DO NOT respond to harrassment with more harrassment. That is also against the rules. Just report it.',
'Remember, disagreement is not harrassment. Please only report if the review is clearly intended to offend or threaten.',
],
[ReportReason.REVIEW_BOMBING]: [
'Review bombing is when an individual leaves reviews with the intent to manipulate the rating of a level.',
'Please only report review bombing if you have evidence that the reviews are not genuine.',
'Simply disagreeing with a review does not make it review bombing.',
],
[ReportReason.SPAM]: [
'Spam is any content that is irrelevant or unsolicited.',
'Please report any content that is not relevant to the site or is intended to promote something.',
],
[ReportReason.OTHER]: [
'If you are reporting for a reason not listed here, please provide a detailed explanation in the message box.',
],

};

return (
<Modal
title='Report'
isOpen={true}
closeModal={() => {
setPreventKeyDownEvent(false);
setModal(null);
}}
>
<div className='flex flex-col gap-4'>
<label htmlFor='reason' className='text-sm'>Flag this {reportType.toLowerCase()} for violating the <Link className='underline' href={TERMS_OF_SERVICE_URL}>Thinky.gg terms</Link>.</label>
<select
id='reason'
className='border border-color-3 rounded-md p-2'
value={reason}
onChange={(e) => setReason(e.target.value as ReportReason)}
>
<option value='' disabled>
Select a reason
</option>
{reportReasons[reportType].concat(ReportReason.OTHER).map((reason) => (
<option key={reason} value={reason}>
{reasonDisplayName[reason]}
</option>
))}
</select>
{reason && (
<ul className='text-sm list-disc pl-4'>
{resasonTypeTipsBullets[reason].map((bullet: string) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
)}
<textarea
id='message'
className='border border-color-3 rounded-md p-2'
value={message}
placeholder={'Please provide a reason for reporting this ' + reportType.toLocaleLowerCase() + '.'}
onChange={e => setMessage(e.target.value)}
/>
<button
className='bg-blue-500 enabled:hover:bg-blue-600 text-white w-full font-medium py-2 px-3 rounded disabled:opacity-50'
disabled={!reason}
onClick={fileReport}
>
File report
</button>
<span className='text-xs text-center text-gray-500'>
By reporting this review, you agree to our <Link className='underline' href={TERMS_OF_SERVICE_URL}>terms of service</Link>. Abusing filing a report is also against the rules.
</span>
</div>
</Modal>
);
}
Loading
Loading