Skip to content

Commit

Permalink
fix: users can only submit one review per show
Browse files Browse the repository at this point in the history
This patch adds a unique constraint on the `showId` and `authorId`
fields on the `Review` table in Postgres. This patch then updates the
"rate and review" section to show the user's existing review if it
exists (along with a submit button that reads "Edit review").

Users can only submit a single review per show.

Closes: NC-643
  • Loading branch information
nicholaschiang committed Jul 23, 2023
1 parent 7e65896 commit 2fbbd45
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 14 deletions.
2 changes: 1 addition & 1 deletion app/routes/shows.$showId/consumer-reviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function ConsumerReviews() {
<TimeAgo
locale='en_short'
className='text-gray-400 dark:text-gray-500 text-3xs'
datetime={review.createdAt}
datetime={review.updatedAt}
/>
</figcaption>
<blockquote className='text-sm'>
Expand Down
40 changes: 32 additions & 8 deletions app/routes/shows.$showId/rate-and-review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useActionData,
useNavigation,
useLocation,
useLoaderData,
} from '@remix-run/react'
import { type ActionArgs, json, redirect } from '@vercel/remix'
import { z } from 'zod'
Expand All @@ -28,6 +29,7 @@ import { log } from 'log.server'
import { getUserId } from 'session.server'
import { useOptionalUser } from 'utils'

import { type loader } from './route'
import { Section } from './section'

const id = 'rate-and-review'
Expand All @@ -45,19 +47,33 @@ const schema = z.object({
content: z.string().trim().min(1, 'Required').min(10, 'Too short'),
})

export async function getReview(showId: number, request: Request) {
const userId = await getUserId(request)
return userId
? prisma.review.findUnique({
where: { authorId_showId: { showId, authorId: userId } },
})
: undefined
}

export async function action({ request, params }: ActionArgs) {
const showId = Number(params.showId)
if (Number.isNaN(showId)) throw new Response(null, { status: 404 })
const userId = await getUserId(request)
if (userId == null)
return redirect(`/login?redirectTo=/shows/${showId}%23${id}`)
const formData = await request.formData()
const submission = parse(formData, { schema })
if (!submission.value || submission.intent !== 'submit')
return json(submission, { status: 400 })
const userId = await getUserId(request)
if (userId == null)
return redirect(`/login?redirectTo=/shows/${showId}%23${id}`)
log.info('creating review... %o', submission.value)
const review = await prisma.review.create({
data: {
const review = await prisma.review.upsert({
where: { authorId_showId: { showId, authorId: userId } },
update: {
score: submission.value.score / 5,
content: submission.value.content,
},
create: {
score: submission.value.score / 5,
content: submission.value.content,
author: { connect: { id: userId } },
Expand All @@ -79,6 +95,7 @@ export function RateAndReview() {
})
const navigation = useNavigation()
const location = useLocation()
const { review } = useLoaderData<typeof loader>()
return (
<Section header='Rate and review' id={id}>
<Form asChild>
Expand All @@ -99,7 +116,12 @@ export function RateAndReview() {
{score.error && <FormMessage>{score.error}</FormMessage>}
</FormLabelWrapper>
<FormControl asChild>
<ScoreInput required />
<ScoreInput
required
defaultValue={
review ? `${Number(review.score) * 5}` : undefined
}
/>
</FormControl>
</FormField>
<FormField name={content.name}>
Expand All @@ -108,11 +130,13 @@ export function RateAndReview() {
{content.error && <FormMessage>{content.error}</FormMessage>}
</FormLabelWrapper>
<FormControl asChild>
<Textarea required />
<Textarea required defaultValue={review?.content} />
</FormControl>
</FormField>
<FormSubmit asChild>
<Button disabled={navigation.state !== 'idle'}>Submit</Button>
<Button disabled={navigation.state !== 'idle'}>
{review ? 'Edit review' : 'Submit review'}
</Button>
</FormSubmit>
</RemixForm>
</Form>
Expand Down
11 changes: 6 additions & 5 deletions app/routes/shows.$showId/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { cn } from 'utils/cn'

import { ConsumerReviews } from './consumer-reviews'
import { CriticReviews } from './critic-reviews'
import { RateAndReview } from './rate-and-review'
import { RateAndReview, getReview } from './rate-and-review'
import { ScoresHeader, getScores } from './scores-header'
import { ShowInfo } from './show-info'
import { WhatToKnow } from './what-to-know'
Expand All @@ -24,11 +24,11 @@ export const handle: Handle = {
),
}

export async function loader({ params }: LoaderArgs) {
export async function loader({ request, params }: LoaderArgs) {
log.debug('getting show...')
const showId = Number(params.showId)
if (Number.isNaN(showId)) throw new Response(null, { status: 404 })
const [show, scores] = await Promise.all([
const [show, scores, review] = await Promise.all([
prisma.show.findUnique({
where: { id: showId },
include: {
Expand All @@ -40,16 +40,17 @@ export async function loader({ params }: LoaderArgs) {
},
reviews: {
include: { author: true, publication: true },
orderBy: { createdAt: 'desc' },
orderBy: { updatedAt: 'desc' },
},
looks: { include: { image: true }, orderBy: { number: 'asc' } },
},
}),
getScores(showId),
getReview(showId, request),
])
log.debug('got show %o', show)
if (show == null) throw new Response(null, { status: 404 })
return { ...show, scores }
return { ...show, scores, review }
}

export default function ShowPage() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[authorId,showId]` on the table `Review` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "Review_authorId_showId_key" ON "Review"("authorId", "showId");
6 changes: 6 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,12 @@ model Review {
// The review article URL (if it was posted online e.g. on Vogue).
url String? @unique
// Each user can only submit one review per show. This may be different for
// critic reviews, but, for now, I've yet to encounter the same journalist
// publishing two different reviews for the same show. If that does happen, I
// can always just replace their review with whichever is the most recent.
@@unique([authorId, showId])
}

// A publication is a resource that publishes fashion reviews (e.g. Vogue).
Expand Down

0 comments on commit 2fbbd45

Please sign in to comment.