Skip to content

Commit

Permalink
Merge pull request #46 from emiliosheinz/feat/report-issue-with-answe…
Browse files Browse the repository at this point in the history
…r-validation

feat/report-issue-with-answer-validation
  • Loading branch information
emiliosheinz authored Mar 19, 2023
2 parents 9fcb6b3 + 61a5358 commit c0855a9
Show file tree
Hide file tree
Showing 28 changed files with 788 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `answer` on the `Card` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Card" DROP COLUMN "answer",
ADD COLUMN "validAnswers" TEXT[];
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "AnswerValidationReport" (
"id" TEXT NOT NULL,
"cardId" TEXT NOT NULL,
"userId" TEXT,
"answer" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "AnswerValidationReport_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "AnswerValidationReport" ADD CONSTRAINT "AnswerValidationReport_cardId_fkey" FOREIGN KEY ("cardId") REFERENCES "Card"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "AnswerValidationReport" ADD CONSTRAINT "AnswerValidationReport_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "AnswerValidationReportStatus" AS ENUM ('Pending', 'Accepted', 'Rejected');

-- AlterTable
ALTER TABLE "AnswerValidationReport" ADD COLUMN "status" "AnswerValidationReportStatus" NOT NULL DEFAULT 'Pending';
62 changes: 41 additions & 21 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ model Session {
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
decks Deck[]
studySessions StudySession[]
favorites Favorite[]
description String?
topics Topic[]
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
decks Deck[]
studySessions StudySession[]
favorites Favorite[]
description String?
topics Topic[]
answerValidationReports AnswerValidationReport[]
}

model VerificationToken {
Expand All @@ -62,6 +63,12 @@ enum Visibility {
WithLink
}

enum AnswerValidationReportStatus {
Pending
Accepted
Rejected
}

model Deck {
id String @id @default(cuid())
title String
Expand All @@ -88,15 +95,28 @@ model Topic {
}

model Card {
id String @id @default(cuid())
question String
answer String
deckId String
deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
studySessionBoxCards StudySessionBoxCard[]
isAiPowered Boolean @default(false)
id String @id @default(cuid())
question String
validAnswers String[]
deckId String
deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
studySessionBoxCards StudySessionBoxCard[]
isAiPowered Boolean @default(false)
answerValidationReports AnswerValidationReport[]
}

model AnswerValidationReport {
id String @id @default(cuid())
cardId String
card Card @relation(fields: [cardId], references: [id], onDelete: Cascade)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
answer String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
status AnswerValidationReportStatus @default(Pending)
}

model StudySession {
Expand Down
2 changes: 2 additions & 0 deletions src/components/modal/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BaseModal } from './base/base-modal.component'
import { NewCardModal } from './new-card/new-card-modal.component'
import { NewTopicModal } from './new-topic/new-topic-modal.component'
import { ReportAnswerValidationModal } from './report-answer-validation/report-answer-validation.component'

export const Modal = {
Base: BaseModal,
NewTopic: NewTopicModal,
NewCard: NewCardModal,
ReportAnswerValidation: ReportAnswerValidationModal,
}
5 changes: 3 additions & 2 deletions src/components/modal/new-card/new-card-modal.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ export function NewCardModal(props: NewCardModalProps) {
<TextArea
id='answer'
label='Resposta'
{...register('answer')}
error={formState.errors['answer']?.message as string}
{...register('validAnswers')}
error={formState.errors['validAnswers']?.message as string}
tooltip='Você pode adicionar mais de uma resposta válida separando-as por ponto e vírgula (;) sem nenhum tipo de espaço. Ex.: Resposta 1;Resposta 2;Resposta 3'
/>
<div className='flex flex-row justify-end gap-5'>
<Button type='button' variant='bad' onClick={close}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useEffect, useMemo } from 'react'
import { useForm } from 'react-hook-form'

import { Button } from '~/components/button'
import { TextArea } from '~/components/text-area'
import { api } from '~/utils/api'
import { withoutPropagation } from '~/utils/forms'
import { notify } from '~/utils/toast'

import { BaseModal } from '../base/base-modal.component'
import type { ReportAnswerValidationModalProps } from './report-answer-validation.types'

export function ReportAnswerValidationModal(
props: ReportAnswerValidationModalProps,
) {
const { isOpen, setIsOpen, answer, cardId } = props

const { mutate: sendReport } =
api.answerValidationReports.reportAnswerValidation.useMutation()

const { handleSubmit, reset, register } = useForm({
defaultValues: useMemo(() => ({ answer }), [answer]),
})

useEffect(() => {
reset({ answer })
}, [reset, answer])

const close = () => {
setIsOpen(false)
reset()
}

const handleSubmitWithoutPropagation = withoutPropagation(
handleSubmit(values => {
sendReport({
...values,
cardId,
})
close()

setTimeout(() => {
notify.success('Solicitação enviada com sucesso!')
}, 500)
}),
)

return (
<BaseModal isOpen={isOpen} setIsOpen={setIsOpen} title='Solicitar revisão'>
<span>
Caso você acredite que sua resposta deveria ter sido considerada
correta, é possível enviar uma solicitação de revisão para o criador do
Deck submetendo este formulário. Dessa forma, caso a solicitação seja
aceita, futuras respostas como a sua serão consideradas corretas.
</span>
<form
className='mt-5 flex flex-col'
onSubmit={handleSubmitWithoutPropagation}
>
<TextArea id='answer' label='Resposta' {...register('answer')} />
<div className='mt-2 flex flex-col justify-end gap-5 md:flex-row'>
<Button fullWidth type='button' variant='bad' onClick={close}>
Cancelar
</Button>
<Button fullWidth>Enviar Solicitação</Button>
</div>
</form>
</BaseModal>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { BaseModalProps } from '../base/base-modal.types'

export type ReportAnswerValidationModalProps = {
answer: string
cardId: string
} & Pick<BaseModalProps, 'isOpen' | 'setIsOpen'>
26 changes: 19 additions & 7 deletions src/components/text-area/text-area.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import React from 'react'

import { classNames } from '~/utils/css'

import { Tooltip } from '../tooltip'
import type { TextAreaProps } from './text-area.types'

export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
function TextArea(props, ref) {
const { label, error, id, disabled, rows = 5, ...otherProps } = props
const {
label,
error,
id,
disabled,
rows = 5,
tooltip,
...otherProps
} = props

const errorClassNames = error
? 'border-error-700 focus:border-error-700 focus:ring-error-700'
Expand All @@ -17,12 +26,15 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
aria-disabled={disabled}
className={classNames('w-full', disabled ? 'opacity-50' : '')}
>
<label
htmlFor={id}
className='block text-sm font-medium capitalize text-primary-800'
>
{label}
</label>
<span className='flex'>
<label
htmlFor={id}
className='block text-sm font-medium capitalize text-primary-800'
>
{label}
</label>
{tooltip ? <Tooltip hint={tooltip} /> : null}
</span>
<div className='mt-1 rounded-md shadow-sm'>
<textarea
id={id}
Expand Down
1 change: 1 addition & 0 deletions src/components/text-area/text-area.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type TextAreaProps = {
error?: string
label: string
id: string
tooltip?: string
} & Omit<
React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Visibility } from '@prisma/client'

import type { Option } from '~/components/radio-group'

export const MAX_VALID_ANSWERS_PER_CARD = 5
export const MINIMUM_ACCEPTED_SIMILARITY = 0.9
export const MAX_TOPICS_PER_DECK_AND_USER = 5
export const ITEMS_PER_PAGE = 30
Expand Down
5 changes: 4 additions & 1 deletion src/contexts/create-new-deck/create-new-deck.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export function CreateNewDeckContextProvider(
*/
const [topics, setTopics] = useState<Array<TopicInput>>(deck?.topics || [])
const [cards, setCards] = useState<Array<CardInput>>(
deck?.cards.map(card => card) || [],
(deck?.cards ?? []).map(card => ({
...card,
validAnswers: card.validAnswers.join(';'),
})),
)
const [visibility, setVisibility] = useState(
DECK_VISIBILITY_OPTIONS.find(option => option.value === deck?.visibility) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { BoltIcon } from '@heroicons/react/24/outline'
import { useRouter } from 'next/router'

import { Button } from '~/components/button'
import { api } from '~/utils/api'
import { routes } from '~/utils/navigation'

type AnswerValidationReportsCardProps = {
deckId: string
}

export function AnswerValidationReportsCard(
props: AnswerValidationReportsCardProps,
) {
const { deckId } = props

const router = useRouter()

const {
isError,
isLoading,
data: hasDeckPendingAnswerValidationReports,
} = api.answerValidationReports.hasDeckPendingAnswerValidationReports.useQuery(
{ deckId },
)

if (isLoading) {
return (
<div className='flex animate-pulse rounded-md bg-primary-200 p-20 sm:p-16'>
<span className='sr-only'>Loading...</span>
</div>
)
}

if (isError || !hasDeckPendingAnswerValidationReports) return null

const goToAnswerValidationReports = () => {
router.push(routes.answerValidationReports(deckId))
}

return (
<div className='relative flex flex-col gap-5 rounded-md bg-primary-50 p-5 shadow-md shadow-primary-200 ring-1 ring-primary-900 sm:flex-row'>
<div className='flex flex-1 items-center gap-5'>
<BoltIcon className='h-16 w-16 text-primary-900' />
<div className='flex flex-col text-primary-900'>
<p className='text-lg sm:text-xl'>Melhore o seu Deck agora mesmo!</p>
<p className='max-w-2xl text-base'>
Usuários solicitaram revisão das respostas de alguns dos cards do
seu Deck. Acesse a seção de solicitações para fazer a revisão 🤗
</p>
</div>
</div>
<div className='flex items-end'>
<div className='hidden h-0 sm:flex sm:h-auto sm:items-end'>
<Button onClick={goToAnswerValidationReports}>
Acessar solicitações
</Button>
</div>
<div className='block w-full sm:hidden sm:w-0'>
<Button fullWidth onClick={goToAnswerValidationReports}>
Acessar solicitações
</Button>
</div>
</div>
</div>
)
}
4 changes: 2 additions & 2 deletions src/modules/decks/create/components/cards.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const Cards = () => {
const isCreatingNewCard = newCardModalState.cardIdx === undefined

const modalFieldsValues = isCreatingNewCard
? { answer: '', question: '' }
? { validAnswers: '', question: '' }
: cards[newCardModalState.cardIdx!]

const handleNewCardFormSubmit = (values: CardFormInputValues) => {
Expand Down Expand Up @@ -83,7 +83,7 @@ export const Cards = () => {
<Card
isEditable
isAiPowered={card.isAiPowered}
key={`${card.question}-${card.answer}`}
key={`${card.question}-${card.validAnswers}`}
onDeletePress={() => deleteCard(idx)}
onEditPress={() => {
setNewCardModalState({ isOpen: true, cardIdx: idx })
Expand Down
Loading

0 comments on commit c0855a9

Please sign in to comment.