Skip to content

Commit

Permalink
curator edit/delete reviews (#1009)
Browse files Browse the repository at this point in the history
* curator edit/delete reviews

* red ellipsis

* test

* fix flaky tests

* merge
  • Loading branch information
sspenst committed Oct 4, 2023
1 parent a71bd74 commit ba1e687
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 147 deletions.
2 changes: 1 addition & 1 deletion components/homepage/homeLoggedIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import Card from '../cards/card';
import ChapterSelectCard from '../cards/chapterSelectCard';
import LevelSelect from '../cards/levelSelect';
import LoadingCard from '../cards/loadingCard';
import FormattedReview from '../formatted/formattedReview';
import FormattedUser from '../formatted/formattedUser';
import FormattedReview from '../level/reviews/formattedReview';
import LoadingSpinner from '../page/loadingSpinner';
import MultiSelectUser from '../page/multiSelectUser';
import RecommendedLevel from './recommendedLevel';
Expand Down
2 changes: 1 addition & 1 deletion components/level/info/formattedLevelInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Tab } from '@headlessui/react';
import FormattedDate from '@root/components/formatted/formattedDate';
import FormattedLevelReviews from '@root/components/formatted/formattedLevelReviews';
import Solved from '@root/components/level/info/solved';
import FormattedLevelReviews from '@root/components/level/reviews/formattedLevelReviews';
import Image from 'next/image';
import React, { useContext, useState } from 'react';
import toast from 'react-hot-toast';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { AppContext } from '../../contexts/appContext';
import { LevelContext } from '../../contexts/levelContext';
import ReviewForm from '../forms/reviewForm';
import FormattedReview from './formattedReview';
import { AppContext } from '../../../contexts/appContext';
import { LevelContext } from '../../../contexts/levelContext';
import ReviewForm from './reviewForm';

interface FormattedLevelReviewsProps {
hideReviews?: boolean;
Expand Down Expand Up @@ -34,20 +33,7 @@ export default function FormattedLevelReviews({ inModal, hideReviews: hideOtherR
if (hideOtherReviews) { continue; }

reviewDivs.push(
<div key={`review-${review._id.toString()}-line`}>
<FormattedReview
hideBorder={true}
review={review}
user={review.userId}
/>
<div
className='mt-3 opacity-30'
style={{
backgroundColor: 'var(--bg-color-4)',
height: 1,
}}
/>
</div>
<ReviewForm inModal={inModal} key={`review-${review._id.toString()}`} review={review} />
);
}
}
Expand All @@ -60,7 +46,7 @@ export default function FormattedLevelReviews({ inModal, hideReviews: hideOtherR
<>{levelContext.reviews.length} review{levelContext.reviews.length !== 1 && 's'}</>
}
</div>)}
<ReviewForm inModal={inModal} key={`user-review-${userReview?._id.toString()}`} userReview={userReview} />
<ReviewForm inModal={inModal} key={`user-review-${userReview?._id.toString()}`} review={userReview} />
{hideReviews === undefined ? null : hideReviews ?
<div className='flex justify-center'>
<button className='font-medium px-2 py-1 bg-neutral-200 hover:bg-white transition text-black rounded-lg border border-neutral-400 mt-2' onClick={() => setHideReviews(false)}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { AppContext } from '@root/contexts/appContext';
import isCurator from '@root/helpers/isCurator';
import classNames from 'classnames';
import moment from 'moment';
import React from 'react';
import { EnrichedLevel } from '../../models/db/level';
import { ReviewWithStats } from '../../models/db/review';
import User from '../../models/db/user';
import { FolderDivider } from '../header/directory';
import Solved from '../level/info/solved';
import ReviewDropdown from '../level/reviews/reviewDropdown';
import StyledTooltip from '../page/styledTooltip';
import FormattedDate from './formattedDate';
import FormattedLevelLink from './formattedLevelLink';
import FormattedUser from './formattedUser';
import React, { useContext } from 'react';
import { EnrichedLevel } from '../../../models/db/level';
import { ReviewWithStats } from '../../../models/db/review';
import User from '../../../models/db/user';
import FormattedDate from '../../formatted/formattedDate';
import FormattedLevelLink from '../../formatted/formattedLevelLink';
import FormattedUser from '../../formatted/formattedUser';
import { FolderDivider } from '../../header/directory';
import StyledTooltip from '../../page/styledTooltip';
import Solved from '../info/solved';
import ReviewDropdown from './reviewDropdown';

interface StarProps {
empty: boolean;
Expand Down Expand Up @@ -74,6 +76,9 @@ interface FormattedReviewProps {
}

export default function FormattedReview({ hideBorder, level, onEditClick, review, user }: FormattedReviewProps) {
const { user: reqUser } = useContext(AppContext);
const canEdit = user._id === reqUser?._id || isCurator(reqUser);

return (
<div className='flex align-center justify-center text-left break-words'>
<div
Expand All @@ -88,7 +93,7 @@ export default function FormattedReview({ hideBorder, level, onEditClick, review
<FormattedUser id={level ? `review-${level._id.toString()}` : 'review'} user={user} />
<FormattedDate ts={review.ts} />
</div>
{onEditClick && <ReviewDropdown onEditClick={onEditClick} />}
{onEditClick && canEdit && <ReviewDropdown onEditClick={onEditClick} userId={user._id.toString()} />}
</div>
{level && <FormattedLevelLink id={`review-${user._id.toString()}`} level={level} />}
</div>
Expand Down
13 changes: 8 additions & 5 deletions components/level/reviews/reviewDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { Menu, Transition } from '@headlessui/react';
import { AppContext } from '@root/contexts/appContext';
import { LevelContext } from '@root/contexts/levelContext';
import { PageContext } from '@root/contexts/pageContext';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import DeleteReviewModal from '../../modal/deleteReviewModal';

interface ReviewDropdownProps {
onEditClick: () => void;
userId: string;
}

export default function ReviewDropdown({ onEditClick }: ReviewDropdownProps) {
export default function ReviewDropdown({ onEditClick, userId }: ReviewDropdownProps) {
const [isDeleteReviewOpen, setIsDeleteReviewOpen] = useState(false);
const levelContext = useContext(LevelContext);
const { setPreventKeyDownEvent } = useContext(PageContext);
const { user } = useContext(AppContext);

useEffect(() => {
setPreventKeyDownEvent(isDeleteReviewOpen);
}, [isDeleteReviewOpen, setPreventKeyDownEvent]);

return (<>
<Menu as='div' className='relative z-10'>
<Menu as='div' className='relative'>
<Menu.Button 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'>
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' strokeWidth={1.5} stroke={user?._id.toString() === userId ? 'currentColor' : 'red'} 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>
Expand All @@ -33,11 +36,10 @@ export default function ReviewDropdown({ onEditClick }: ReviewDropdownProps) {
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' style={{
<Menu.Items className='absolute right-0 m-1 w-fit origin-top-right rounded-[10px] shadow-lg border z-20' style={{
backgroundColor: 'var(--bg-color-2)',
borderColor: 'var(--bg-color-4)',
color: 'var(--color)',
// top: Dimensions.MenuHeight,
}}>
<div className='px-1 py-1'>
<Menu.Item>
Expand Down Expand Up @@ -82,6 +84,7 @@ export default function ReviewDropdown({ onEditClick }: ReviewDropdownProps) {
levelContext?.getReviews();
}}
isOpen={isDeleteReviewOpen}
userId={userId}
/>
</>);
}
Original file line number Diff line number Diff line change
@@ -1,40 +1,30 @@
import classNames from 'classnames';
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useState } from 'react';
import toast from 'react-hot-toast';
import { Rating } from 'react-simple-star-rating';
import TextareaAutosize from 'react-textarea-autosize';
import Theme from '../../constants/theme';
import { AppContext } from '../../contexts/appContext';
import { LevelContext } from '../../contexts/levelContext';
import { PageContext } from '../../contexts/pageContext';
import Review from '../../models/db/review';
import FormattedReview, { Star } from '../formatted/formattedReview';
import DeleteReviewModal from '../modal/deleteReviewModal';
import ProfileAvatar from '../profile/profileAvatar';
import isNotFullAccountToast from '../toasts/isNotFullAccountToast';
import Theme from '../../../constants/theme';
import { AppContext } from '../../../contexts/appContext';
import { LevelContext } from '../../../contexts/levelContext';
import { PageContext } from '../../../contexts/pageContext';
import { ReviewWithStats } from '../../../models/db/review';
import ProfileAvatar from '../../profile/profileAvatar';
import isNotFullAccountToast from '../../toasts/isNotFullAccountToast';
import FormattedReview, { Star } from './formattedReview';

interface ReviewFormProps {
inModal?: boolean;
userReview?: Review;
review?: ReviewWithStats;
}

export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
const [isDeleteReviewOpen, setIsDeleteReviewOpen] = useState(false);
export default function ReviewForm({ inModal, review }: ReviewFormProps) {
const [isEditing, setIsEditing] = useState(!review);
const [isUpdating, setIsUpdating] = useState(false);
const levelContext = useContext(LevelContext);
const [rating, setRating] = useState(userReview?.score || 0);
const [reviewBody, setReviewBody] = useState(userReview?.text || '');
const [rating, setRating] = useState(review?.score || 0);
const [reviewBody, setReviewBody] = useState(review?.text || '');
const { setPreventKeyDownEvent } = useContext(PageContext);
const [showUserReview, setShowUserReview] = useState(!!userReview);
const { theme, user } = useContext(AppContext);

// only prevent keydown when the delete modal is the first modal open
// (not opened from within the review modal)
useEffect(() => {
if (!inModal) {
setPreventKeyDownEvent(isDeleteReviewOpen);
}
}, [inModal, isDeleteReviewOpen, setPreventKeyDownEvent]);
const { theme, user: reqUser } = useContext(AppContext);

function onUpdateReview() {
setIsUpdating(true);
Expand All @@ -43,14 +33,15 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
toast.loading('Saving...');

fetch('/api/review/' + levelContext?.level._id, {
method: userReview ? 'PUT' : 'POST',
method: review ? 'PUT' : 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
score: rating,
text: reviewBody,
})
userId: review?.userId._id.toString(),
}),
}).then(res => {
if (res.status === 401) {
isNotFullAccountToast('Reviewing');
Expand All @@ -61,7 +52,7 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
toast.success('Saved');

levelContext?.getReviews();
setShowUserReview(true);
setIsEditing(false);
}
}).catch(async err => {
console.error(err);
Expand All @@ -72,19 +63,21 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
});
}

const user = review?.userId ?? reqUser;

if (!user) {
return null;
}

if (showUserReview && userReview) {
if (!isEditing && review) {
return (
<>
<FormattedReview
hideBorder={true}
key={'user-formatted-review'}
onEditClick={() => setShowUserReview(false)}
review={userReview}
user={userReview.userId}
onEditClick={() => setIsEditing(true)}
review={review}
user={user}
/>
<div
className='opacity-30'
Expand All @@ -93,13 +86,6 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
height: 1,
}}
/>
<DeleteReviewModal
closeModal={() => {
setIsDeleteReviewOpen(false);
levelContext?.getReviews();
}}
isOpen={isDeleteReviewOpen}
/>
</>
);
}
Expand All @@ -108,7 +94,7 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
<div className='block w-full reviewsSection flex flex-col gap-2 mb-2' style={{
borderColor: 'var(--bg-color-4)',
}}>
<h2 className='font-bold'>{`${userReview ? 'Edit' : 'Add a'} review`}</h2>
<h2 className='font-bold'>{`${review ? 'Edit' : 'Add a'} review`}</h2>
<div className='flex items-center gap-2'>
<ProfileAvatar user={user} />
<Rating
Expand Down Expand Up @@ -159,9 +145,9 @@ export default function ReviewForm({ inModal, userReview }: ReviewFormProps) {
disabled={isUpdating || (rating === 0 && reviewBody.length === 0)}
onClick={() => {
// restore the pre-edit user review if available, otherwise reset
setShowUserReview(!!userReview);
setRating(userReview?.score || 0);
setReviewBody(userReview?.text || '');
setIsEditing(!review);
setRating(review?.score || 0);
setReviewBody(review?.text || '');
}}>
Cancel
</button>
Expand Down
9 changes: 5 additions & 4 deletions components/modal/deleteReviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import Modal from '.';
interface DeleteReviewModalProps {
closeModal: () => void;
isOpen: boolean;
userId: string;
}

export default function DeleteReviewModal({ closeModal, isOpen }: DeleteReviewModalProps) {
export default function DeleteReviewModal({ closeModal, isOpen, userId }: DeleteReviewModalProps) {
const levelContext = useContext(LevelContext);

function onConfirm() {
toast.dismiss();
toast.loading('Deleting review...');

fetch(`/api/review/${levelContext?.level._id}`, {
fetch(`/api/review/${levelContext?.level._id}?userId=${userId}`, {
method: 'DELETE',
credentials: 'include',
}).then(res => {
Expand All @@ -26,10 +27,10 @@ export default function DeleteReviewModal({ closeModal, isOpen }: DeleteReviewMo
} else {
throw res.text();
}
}).catch(err => {
}).catch(async err => {
console.error(err);
toast.dismiss();
toast.error('Error deleting review');
toast.error(JSON.parse(await err)?.error || 'Error deleting review');
});
}

Expand Down
31 changes: 1 addition & 30 deletions components/modal/postGameModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,11 @@ import Level, { EnrichedLevel } from '@root/models/db/level';
import User from '@root/models/db/user';
import Link from 'next/link';
import React, { useEffect, useState } from 'react';
import {
EmailIcon,
EmailShareButton,
FacebookIcon,
FacebookMessengerIcon,
FacebookShareButton,
HatenaIcon,
InstapaperIcon,
LineIcon,
LinkedinIcon,
LivejournalIcon,
MailruIcon,
OKIcon,
PinterestIcon,
PocketIcon,
RedditIcon,
RedditShareButton,
TelegramIcon,
TelegramShareButton,
TumblrIcon,
TwitterIcon,
TwitterShareButton,
ViberIcon,
VKIcon,
WeiboIcon,
WhatsappIcon,
WorkplaceIcon
} from 'react-share';
import Card from '../cards/card';
import ChapterSelectCard from '../cards/chapterSelectCard';
import { getDifficultyFromValue } from '../formatted/formattedDifficulty';
import FormattedLevelReviews from '../formatted/formattedLevelReviews';
import RecommendedLevel from '../homepage/recommendedLevel';
import FormattedLevelReviews from '../level/reviews/formattedLevelReviews';
import LoadingSpinner from '../page/loadingSpinner';
import ShareBar from '../social/shareBar';
import Modal from '.';
Expand Down Expand Up @@ -105,7 +77,6 @@ export default function PostGameModal({ chapter, closeModal, collection, isOpen,

const hrefOverride = nextLevel ? `/level/${nextLevel.slug}?${queryParams}` : undefined;
const url = `https://pathology.gg/level/${level.slug}`;

const quote = 'Just completed Pathology.gg puzzle "' + level.name + '" (Difficulty: ' + getDifficultyFromValue(level.calc_difficulty_estimate).name + ')';

return (
Expand Down
Loading

0 comments on commit ba1e687

Please sign in to comment.