Skip to content

Commit

Permalink
feat(routes/show): add consumer review sections
Browse files Browse the repository at this point in the history
This patch adds the "Rate and review" section to the show page where
users can submit their own consumer reviews.

This patch also adds the "Consumer reviews" section which contains a
very basic list of the consumer reviews currently in database.

This patch includes a couple other small drive-by fixes and linking
improvements (e.g. we now associate an ID with each section on the show
page, making it easier to provide `#` based links to specific parts of
the increasingly long page).

Closes: NC-625
  • Loading branch information
nicholaschiang committed Jul 23, 2023
1 parent 6f7cc79 commit a2818b2
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 20 deletions.
61 changes: 61 additions & 0 deletions app/routes/shows.$showId/consumer-reviews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useLoaderData } from '@remix-run/react'

import { Empty } from 'components/empty'

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

export function ConsumerReviews() {
const show = useLoaderData<typeof loader>()
const reviews = show.reviews.filter((r) => r.publication == null)
return (
<Section header={`Consumer reviews for ${show.name}`} id='consumer-reviews'>
{reviews.length === 0 && (
<Empty className='mt-2'>
No consumer reviews to show yet. Try submitting one above.
</Empty>
)}
{reviews.length > 0 && (
<ol className='mt-2 grid gap-4 grid-cols-2'>
{reviews.map((review) => (
<li key={review.id}>
<Review
score={review.score}
author={review.author}
content={review.content}
/>
</li>
))}
</ol>
)}
</Section>
)
}

type ReviewProps = {
author: { name: string; url: string | null }
score: string
content: string
}

function Review({ author, score, content }: ReviewProps) {
return (
<figure className='border border-gray-200 dark:border-gray-700 rounded-md'>
<blockquote className='px-4 py-2'>
<p className='font-medium'>{Math.floor(Number(score) * 100)}% Fresh</p>
<p>{content}</p>
</blockquote>
<figcaption className='border-t bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 px-4 py-2'>
<cite>
<span className='text-gray-500'></span>
{author.url != null && (
<a href={author.url} target='_blank' rel='noopener noreferrer'>
{author.name}
</a>
)}
{author.url == null && <span>{author.name}</span>}
</cite>
</figcaption>
</figure>
)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { useLoaderData } from '@remix-run/react'
import { ExternalLink } from 'lucide-react'

import { Empty } from 'components/empty'

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

export function Reviews() {
export function CriticReviews() {
const show = useLoaderData<typeof loader>()
const reviews = show.reviews.filter(
(review) => review.publication != null && review.url != null,
)
return (
<Section header={`Critic reviews for ${show.name}`}>
<ol className='mt-2 grid gap-4'>
{show.reviews
.filter((r) => r.publication != null && r.url != null)
.map((review) => (
<Section header={`Critic reviews for ${show.name}`} id='critic-reviews'>
{reviews.length === 0 && (
<Empty className='mt-2'>
No critic reviews to show yet. Try checking back later.
</Empty>
)}
{reviews.length > 0 && (
<ol className='mt-2 grid gap-4'>
{reviews.map((review) => (
<li key={review.id}>
<Review
author={review.author}
Expand All @@ -21,7 +30,8 @@ export function Reviews() {
/>
</li>
))}
</ol>
</ol>
)}
</Section>
)
}
Expand All @@ -35,7 +45,7 @@ type ReviewProps = {

function Review({ author, publication, url, content }: ReviewProps) {
return (
<figure className='flex-none bg-gray-100 dark:bg-gray-800 overflow-hidden pb-4'>
<figure className='overflow-hidden bg-gray-100 dark:bg-gray-800 pb-4'>
<figcaption className='mt-8 text-center'>
<cite className='text-lg underline underline-offset-4 decoration-2 decoration-gray-300 dark:decoration-gray-600'>
<span className='text-gray-500'>By </span>
Expand Down
105 changes: 105 additions & 0 deletions app/routes/shows.$showId/rate-and-review.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useForm } from '@conform-to/react'
import { parse } from '@conform-to/zod'
import {
Form as RemixForm,
useActionData,
useNavigation,
} from '@remix-run/react'
import { type ActionArgs, json, redirect } from '@vercel/remix'
import { z } from 'zod'

import {
Form,
FormField,
FormLabel,
FormLabelWrapper,
FormControl,
FormSubmit,
FormMessage,
} from 'components/form'
import { Button } from 'components/ui/button'
import { Input } from 'components/ui/input'
import { Textarea } from 'components/ui/textarea'

import { prisma } from 'db.server'
import { log } from 'log.server'
import { getUserId } from 'session.server'

import { Section } from './section'

const schema = z.object({
score: z.preprocess(
(score) => Number(score),
z
.number()
.lte(1, 'Score cannot be larger than 1.0')
.gte(0, 'Score cannot be negative'),
),
content: z.string().trim().min(1, 'Required').min(10, 'Too short'),
})

export async function action({ request, params }: ActionArgs) {
const showId = Number(params.showId)
if (Number.isNaN(showId)) throw new Response(null, { status: 404 })
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}#rate-and-review`)
log.info('creating review... %o', submission.value)
const review = await prisma.review.create({
data: {
score: submission.value.score,
content: submission.value.content,
author: { connect: { id: userId } },
show: { connect: { id: showId } },
},
})
log.info('created review: %o', review)
return redirect(`/shows/${showId}`)
}

export function RateAndReview() {
const lastSubmission = useActionData<typeof action>()
const [form, { score, content }] = useForm({
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema })
},
})
const navigation = useNavigation()
return (
<Section header='Rate and review' id='rate-and-review'>
<Form
asChild
className='max-w-sm mt-2 shadow-sm border border-gray-200 dark:border-gray-700 rounded-md p-4'
>
<RemixForm method='post' {...form.props}>
<FormField name={score.name}>
<FormLabelWrapper>
<FormLabel>Review score</FormLabel>
{score.error && <FormMessage>{score.error}</FormMessage>}
</FormLabelWrapper>
<FormControl asChild>
<Input type='number' max={1} min={0} />
</FormControl>
</FormField>
<FormField name={content.name}>
<FormLabelWrapper>
<FormLabel>What did you think of the runway?</FormLabel>
{content.error && <FormMessage>{content.error}</FormMessage>}
</FormLabelWrapper>
<FormControl asChild>
<Textarea />
</FormControl>
</FormField>
<FormSubmit asChild>
<Button disabled={navigation.state !== 'idle'}>Submit</Button>
</FormSubmit>
</RemixForm>
</Form>
</Section>
)
}
16 changes: 11 additions & 5 deletions app/routes/shows.$showId/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { log } from 'log.server'
import { type Handle } from 'root'
import { cn } from 'utils/cn'

import { Reviews } from './reviews'
import { ConsumerReviews } from './consumer-reviews'
import { CriticReviews } from './critic-reviews'
import { RateAndReview } from './rate-and-review'
import { ScoresHeader, getScores } from './scores-header'
import { ShowInfo } from './show-info'
import { WhatToKnow } from './what-to-know'
import { WhereToBuy } from './where-to-buy'

export { action } from './rate-and-review'

export const handle: Handle = {
breadcrumb: (match) => (
<Link to={`/shows/${match.params.showId as string}`}>
Expand Down Expand Up @@ -47,9 +51,9 @@ export async function loader({ params }: LoaderArgs) {

export default function ShowPage() {
return (
<main className='fixed inset-0 overflow-hidden max-w-screen-xl mx-auto grid grid-cols-5 gap-6 px-6'>
<About className='col-span-3 pb-6 pt-16' />
<Looks className='col-span-2 pb-6 pt-16' />
<main className='fixed inset-0 overflow-hidden max-w-screen-xl mx-auto grid grid-cols-5 gap-6'>
<About className='col-span-3 pl-6 pb-6 pt-16' />
<Looks className='col-span-2 pr-6 pb-6 pt-16' />
</main>
)
}
Expand Down Expand Up @@ -82,8 +86,10 @@ function About({ className }: { className: string }) {
<ScoresHeader />
<WhatToKnow />
<WhereToBuy />
<RateAndReview />
<ConsumerReviews />
<ShowInfo />
<Reviews />
<CriticReviews />
</div>
)
}
5 changes: 3 additions & 2 deletions app/routes/shows.$showId/section.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { type PropsWithChildren } from 'react'

export function Section({
id,
header,
children,
}: PropsWithChildren<{ header: string }>) {
}: PropsWithChildren<{ id: string; header: string }>) {
return (
<section className='grid gap-2'>
<section className='grid gap-2' id={id}>
<h1 className='border-l-2 border-emerald-700 pl-1.5 font-medium text-base uppercase'>
{header}
</h1>
Expand Down
8 changes: 5 additions & 3 deletions app/routes/shows.$showId/show-info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Section } from './section'
export function ShowInfo() {
const show = useLoaderData<typeof loader>()
return (
<Section header='Show info'>
<Section header='Show info' id='show-info'>
<article>{show.description}</article>
<dl className='mt-2'>
<InfoItem label='Date'>
Expand All @@ -27,9 +27,11 @@ export function ShowInfo() {
>
{show.brands.map((brand) =>
brand.url ? (
<ExternalLink href={brand.url}>{brand.name}</ExternalLink>
<ExternalLink key={brand.id} href={brand.url}>
{brand.name}
</ExternalLink>
) : (
<span>{brand.name}</span>
<span key={brand.id}>{brand.name}</span>
),
)}
</InfoItem>
Expand Down
2 changes: 1 addition & 1 deletion app/routes/shows.$showId/what-to-know.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Section } from './section'
export function WhatToKnow() {
const show = useLoaderData<typeof loader>()
return (
<Section header='What to know'>
<Section header='What to know' id='what-to-know'>
<Subheader>Critics Consensus</Subheader>
{show.criticReviewSummary ? (
<p className='mb-2'>{show.criticReviewSummary}</p>
Expand Down
2 changes: 1 addition & 1 deletion app/routes/shows.$showId/where-to-buy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function WhereToBuy() {
const links = show.collections.map((collection) => collection.links).flat()
const brands = show.brands.filter((brand) => brand.url)
return (
<Section header='Where to buy'>
<Section header='Where to buy' id='where-to-buy'>
{links.length === 0 && (
<Empty className='mt-2'>
<p>
Expand Down

0 comments on commit a2818b2

Please sign in to comment.