247 lines
7.4 KiB
TypeScript
Executable File
247 lines
7.4 KiB
TypeScript
Executable File
import {User} from '@common/auth/user';
|
|
import React, {Fragment, useContext, useState} from 'react';
|
|
import {Checkbox} from '@common/ui/forms/toggle/checkbox';
|
|
import {UserAvatar} from '@common/ui/images/user-avatar';
|
|
import {FormattedRelativeTime} from '@common/i18n/formatted-relative-time';
|
|
import {queryClient} from '@common/http/query-client';
|
|
import {Button} from '@common/ui/buttons/button';
|
|
import {Trans} from '@common/i18n/trans';
|
|
import {FormTextField} from '@common/ui/forms/input-field/text-field/text-field';
|
|
import {SiteConfigContext} from '@common/core/settings/site-config-context';
|
|
import {Link} from 'react-router-dom';
|
|
import {LinkStyle} from '@common/ui/buttons/external-link';
|
|
import {NormalizedModel} from '@common/datatable/filters/normalized-model';
|
|
import {Review} from '@app/titles/models/review';
|
|
import {TitleRating} from '@app/reviews/title-rating';
|
|
import {useUpdateReview} from '@app/admin/reviews/requests/use-update-review';
|
|
import {useForm} from 'react-hook-form';
|
|
import {CreateReviewPayload} from '@app/reviews/requests/use-create-review';
|
|
import {Form} from '@common/ui/forms/form';
|
|
import {StarSelector} from '@app/reviews/review-list/star-selector';
|
|
import {DeleteReviewsButton} from '@app/admin/reviews/delete-reviews-button';
|
|
import {BulletSeparatedItems} from '@app/titles/bullet-separated-items';
|
|
|
|
interface Props {
|
|
review: Review;
|
|
isSelected: boolean;
|
|
onToggle: () => void;
|
|
}
|
|
export function ReviewDatatableItem({review, isSelected, onToggle}: Props) {
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
const helpfulCount = review.helpful_count || 1;
|
|
const totalFeedbackCount =
|
|
review.helpful_count + review.not_helpful_count || 1;
|
|
|
|
return (
|
|
<div className="border-b p-14">
|
|
{review.reviewable && (
|
|
<ReviewableHeader
|
|
isSelected={isSelected}
|
|
onToggle={onToggle}
|
|
reviewable={review.reviewable}
|
|
/>
|
|
)}
|
|
<div className="flex items-start gap-10 pt-14 md:pl-20">
|
|
<UserAvatar className="flex-shrink-0" user={review.user} size="md" />
|
|
<div className="min-w-0 flex-auto overflow-hidden">
|
|
<ReviewHeader review={review} />
|
|
{isEditing ? (
|
|
<EditReviewForm
|
|
review={review}
|
|
onClose={isSaved => {
|
|
setIsEditing(false);
|
|
if (isSaved) {
|
|
queryClient.invalidateQueries({queryKey: ['comment']});
|
|
}
|
|
}}
|
|
/>
|
|
) : (
|
|
<Fragment>
|
|
<div className="my-14">
|
|
<TitleRating className="mb-8" score={review.score} />
|
|
{review.title && (
|
|
<div className="mb-8 text-base font-medium">
|
|
{review.title}
|
|
</div>
|
|
)}
|
|
<div className="whitespace-break-spaces text-sm">
|
|
{review.body}
|
|
</div>
|
|
<div className="mt-8 text-xs text-muted">
|
|
<BulletSeparatedItems>
|
|
<Trans
|
|
message=":helpfulCount out of :total people found this helpful"
|
|
values={{helpfulCount, total: totalFeedbackCount}}
|
|
/>
|
|
{review.reports_count ? (
|
|
<Trans
|
|
message=":count reports"
|
|
values={{count: review.reports_count || 0}}
|
|
/>
|
|
) : null}
|
|
</BulletSeparatedItems>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<DeleteReviewsButton reviewIds={[review.id]} />
|
|
<Button
|
|
variant="outline"
|
|
size="xs"
|
|
onClick={() => setIsEditing(true)}
|
|
>
|
|
<Trans message="Edit" />
|
|
</Button>
|
|
</div>
|
|
</Fragment>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ReviewableHeaderProps {
|
|
isSelected: boolean;
|
|
onToggle: Props['onToggle'];
|
|
reviewable: NormalizedModel;
|
|
}
|
|
function ReviewableHeader({
|
|
isSelected,
|
|
onToggle,
|
|
reviewable,
|
|
}: ReviewableHeaderProps) {
|
|
return (
|
|
<div className="flex items-center">
|
|
<div className="mr-14">
|
|
<Checkbox checked={isSelected} onChange={() => onToggle()} />
|
|
</div>
|
|
{reviewable.image && (
|
|
<img
|
|
className="mr-6 h-20 w-20 overflow-hidden rounded object-cover"
|
|
src={reviewable.image}
|
|
alt=""
|
|
/>
|
|
)}
|
|
<div className="mr-4 text-sm">{reviewable.name}</div>
|
|
<div className="text-xs text-muted">({reviewable.model_type})</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CommentHeaderProps {
|
|
review: Review;
|
|
}
|
|
function ReviewHeader({review}: CommentHeaderProps) {
|
|
return (
|
|
<div className="flex items-center gap-4 text-sm">
|
|
<div>
|
|
{review.user && (
|
|
<UserDisplayName user={review.user} show="display_name" />
|
|
)}
|
|
</div>
|
|
<div>•</div>
|
|
<time>
|
|
<FormattedRelativeTime date={review.created_at} />
|
|
</time>
|
|
{review.user && (
|
|
<div className="ml-auto hidden md:block">
|
|
{<UserDisplayName user={review.user} show="email" />}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface EditReviewFormProps {
|
|
review: Review;
|
|
onClose: (saved: boolean) => void;
|
|
}
|
|
function EditReviewForm({review, onClose}: EditReviewFormProps) {
|
|
const [content, setContent] = useState(review.body);
|
|
const updateReview = useUpdateReview(review);
|
|
const form = useForm<CreateReviewPayload>({
|
|
defaultValues: {
|
|
score: review.score,
|
|
title: review.title,
|
|
body: review.body,
|
|
},
|
|
});
|
|
return (
|
|
<Form
|
|
className="mt-24"
|
|
form={form}
|
|
onSubmit={newValues => {
|
|
updateReview.mutate(newValues, {onSuccess: () => onClose(true)});
|
|
}}
|
|
>
|
|
<StarSelector
|
|
className="-ml-8 mb-12"
|
|
count={10}
|
|
value={form.watch('score')}
|
|
onValueChange={newScore => {
|
|
form.setValue('score', newScore);
|
|
}}
|
|
/>
|
|
<FormTextField
|
|
name="title"
|
|
className="mb-24"
|
|
label={<Trans message="Title" />}
|
|
labelSuffix={<Trans message="10 character minimum" />}
|
|
autoFocus
|
|
minLength={10}
|
|
required
|
|
/>
|
|
<FormTextField
|
|
className="mb-24"
|
|
name="body"
|
|
label={<Trans message="Review" />}
|
|
labelSuffix={<Trans message="100 character minimum" />}
|
|
inputElementType="textarea"
|
|
rows={5}
|
|
minLength={100}
|
|
required
|
|
/>
|
|
<Button
|
|
size="xs"
|
|
variant="outline"
|
|
color="primary"
|
|
type="submit"
|
|
className="mr-6"
|
|
disabled={updateReview.isPending}
|
|
>
|
|
<Trans message="Save" />
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
variant="outline"
|
|
className="mr-6"
|
|
onClick={e => onClose(false)}
|
|
disabled={updateReview.isPending}
|
|
>
|
|
<Trans message="Cancel" />
|
|
</Button>
|
|
</Form>
|
|
);
|
|
}
|
|
|
|
interface UserDisplayNameProps {
|
|
user: User;
|
|
show: 'display_name' | 'email';
|
|
}
|
|
function UserDisplayName({user, show}: UserDisplayNameProps) {
|
|
const {auth} = useContext(SiteConfigContext);
|
|
if (auth.getUserProfileLink) {
|
|
return (
|
|
<Link
|
|
to={auth.getUserProfileLink(user)}
|
|
className={LinkStyle}
|
|
target="_blank"
|
|
>
|
|
{user[show]}
|
|
</Link>
|
|
);
|
|
}
|
|
return <div className="text-muted">{user[show]}</div>;
|
|
}
|