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

curator edit/delete reviews #1009

Merged
merged 6 commits into from
Oct 2, 2023
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
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